Actions

Arduino-like pin definitions in C++

From Just in Time

Work in Progress
This page describes a work in progress. All specifications may change and the final product may significantly deviate from what we describe here. It is very well possible that there will be no final product at all.
Warning.png

[[Revision timestamp::20140801161445|]]

Lotsapins.jpg

We regularly design "bare" AVR devices (meaning: non-Arduino). While we're doing that, we often need to do some of the following:

  • When designing a single-sided PCB, completely re-assign many pins in order to avoid bridges.
  • Create a library for commonly used components like a hd44780 LCD display, or NRF24L01+ transceiver, making the pins that connect to these devices configurable.
  • Implement non-trivial signaling protocols while keeping the source code readable.

All of these scenarios require us to allocate pins or groups of pins to functions in such a way that we don't have to re-write our code when that allocation changes for some reason. At the same time we need to keep the code that actually uses these pins as clean as possible.

In AVR-land, there are two main schools of defining your pins:

  • The C-style way, using #defines for the register and bit positions of a pin, which results in code that performs well, but can be somewhat combersome and does not score high on readability.
  • The Arduino way, using digitalWrite( pin, value) and a single integer to designate a pin, which is arguably easier to read and easier to re-allocate, but can take many clock cycles to do something simple like setting a single bit.

This page describes our pin-definition library. This library allows for Arduino-like syntax for pin usage, while keeping the performance of 'manual' pin manipulations in C.

TL;DR

Pin-definitions is a header-only library, you can find the sources on GitHub, in file pin_definitions.hpp. This library is intended to declare AVR pins for functions in an intuitive way. Typical usage is as follows:

<source lang='cpp'>

  1. include "avr_utilities/pin_definitions.hpp"

// declare a single pin DECLARE_PIN( led1, D, 6); DECLARE_PIN( led2, B, 0);

// declare a consecutive range of pins in a single register DECLARE_PIN_GROUP( counter, D, 2, 3); // D2, D3 and D4 are a counter DECLARE_PIN_GROUP( rotary_encoder, D, 5, 2); // D5, D6 are some inpt, e.g. from a quadrature rotary encoder

void do_stuff() { // initialize data direction for the output pins. init_as_output(led1 | led2 | counter);

// start by setting both leds set( led1 | led2);

while (true) { // read the two input bits of the rotary encoder and // output the result to the bits of the counter write( counter, read(rotary_encoder)); if (read(rotary_encoder) == 0b10) { set( led1); reset( led2); } else { set( led2); reset( led1); } } } </source>

On top of this, the library aims at exactly the same performance as hand-written bit manipulations. This means that the line <source lang="cpp">set( led1 | led2)</source> has the same performance as <source lang="cpp">

   LED_PORT |= ( _BV( LED1_PIN)| _BV(LED2_PIN));

</source> ...if led1 and led2 are on the same port. If the leds are on different ports, it should have performance equivalent to <source lang="cpp">

   LED1_PORT |= _BV( LED1_PIN);
   LED2_PORT |= _BV( LED2_PIN);

</source> It will even go one step further. For a line like <source lang="cpp">set( led1 | led2 | led3)</source> if led1 and led3 are on the same port, but led2 is on a different port, it will generate code equivalent to: <source lang="cpp">

   LED13_PORT |= (_BV( LED1_PIN)| _BV( LED3_PIN));
   LED2_PORT  |= _BV( LED2_PIN);

</source> The library will perform this type of optimization regardless the number of pins or pin groups that are combined in a single call. Of course it will also perform the same optimizations for the functions reset(), make_output() and initialize_as_output(), minimizing the clock ticks for the given arguments.

In order to do this, the library makes heavy use of C++ template metaprogramming. This makes the library itself less readable to those that are not familiar with template metaprogramming, but it sure makes (re-)declaration and usage of pins a lot easier to read.

The next Section goes into some of the history of setting pin values on AVRs and Arduino's. The Section after that will go into the details of the pin_definitions library and how it can be used.

Defining pins on AVR and Arduino

AVR C style

Take a look at how a randomly chosen library (for driving HD44780 LCD displays) defines which pins have which function: <source lang="cpp">

  1. define LCD_PORT PORTA
  2. define LCD_DATA0_PORT LCD_PORT
  3. define LCD_DATA1_PORT LCD_PORT
  4. define LCD_DATA2_PORT LCD_PORT
  5. define LCD_DATA3_PORT LCD_PORT
  6. define LCD_DATA0_PIN 0
  7. define LCD_DATA1_PIN 1
  8. define LCD_DATA2_PIN 2
  9. define LCD_DATA3_PIN 3
  10. define LCD_RS_PORT LCD_PORT
  11. define LCD_RS_PIN 4
  12. define LCD_RW_PORT LCD_PORT
  13. define LCD_RW_PIN 5
  14. define LCD_E_PORT LCD_PORT
  15. define LCD_E_PIN 6

</source> There's nothing wrong with that library: this is how almost all AVR code defines their pins. Typically, each pin function is defined by using two (sometimes only one) #defines: one to define the port and one to define which bit of the port is being used. For an overly simple example, suppose we want to use bit 5 of port B to control led1, we'd typically do this:

<source lang="cpp">

  1. define LED1_PORT PORTB
  2. define LED1_PIN 5

// ...

void f() {

   // ...
   // flash led 1
   LED1_PORT &= ~_BV( LED1_PIN);
   _delay_ms( 50);
   LED1_PORT |= _BV( LED1_PIN);
   // ...

} </source>

Unfortunately, this is not enough if you really want to be able to switch pins without changing the application code. In addition to defining the port, you'd also need to set the data direction (DDRx) bit for the LED to become an output:

<source lang="cpp">

  1. define LED1_DDR DDRB
  2. define LED1_PORT PORTB
  3. define LED1_PIN 5

void initialize() {

   // make the pin for led1 an output
   LED1_DDR |= _BV( LED1_PIN);
   // do other outputs as well...

}

void f() {

   // etc...

} </source>

This is getting cumbersome. In practice, I normally forget about the data direction and completely re-write the init function when I re-assign pins. For the rest, I try to maintain both the port #define and the pin #define for medium to large projects, but for the smaller ones I usually hardcode the pins.

Arduino

On Arduino, defining a pin function becomes a lot easier and more readable:

<source lang="cpp"> int led1 = 13; // LED connected to digital pin 13, port B, pin 5

void setup() {

 // make the pin for led1 an output
 pinMode(led1, OUTPUT);
 // do other outputs as well...

}

void f() {

 // ...
 // flash led 1
 digitalWrite(led1, LOW);
 delay(50);
 digitalWrite(led1, HIGH);
 // ...

}

</source> Changing the led pin is a matter of just adapting the initialization of led1. At the same time, making the output pin high or low is about as readable as it could get: digitalWrite(led1, LOW). You just know what that line does.

However, this readability comes at a significant cost. If we look at the implementation of digitalWrite[1] we can see that cost:

<source lang="cpp"> void digitalWrite(uint8_t pin, uint8_t val) {

       uint8_t timer = digitalPinToTimer(pin);
       uint8_t bit = digitalPinToBitMask(pin);
       uint8_t port = digitalPinToPort(pin);
       volatile uint8_t *out;
       if (port == NOT_A_PIN) return;
       // If the pin that support PWM output, we need to turn it off
       // before doing a digital write.
       if (timer != NOT_ON_TIMER) turnOffPWM(timer);
       out = portOutputRegister(port);
       if (val == LOW) {
               uint8_t oldSREG = SREG;
               cli();
               *out &= ~bit;
               SREG = oldSREG;
       } else {
               uint8_t oldSREG = SREG;
               cli();
               *out |= bit;
               SREG = oldSREG;
       }

}

</source>

This will easily take 50 clock cycles[2]! That's a bit steep, if all we want is to change a single bit value. If we know which pin we're going to set at compile time, changing a single pin value should take at most 2 clock cycles (1 on an Attiny).

Introducing pin_definitions.hpp

With AVR-GCC we have a fully functional C++ compiler at our disposal. It should be possible to use this fact to create a library that allows Arduino's intuitive digitalWrite function with native C performance. With this in mind, we wrote pin_definitions.hpp. This pin definition library makes extensive use of meta programming. Its implementation may not be easy to read if you're not familiar with C++ template metaprogramming, but if you only use the library, you should find it very intuitive—plus I've done my stinking best to not let any compilation errors end up deep inside the source code of this library...

Usage

Usage is simple. First include the library header file: <source lang="cpp">

  1. include "avr_utilities/pin_definitions.hpp"

</source>

Then you can declare your pins and pin groups. A pin is a single input- or output pin on the AVR, for example pin B5 (that would be pin number 13 on an Arduino). A pin is identified by a port (depending on your AVR this is any port from port A to port F) and a bit number (0-7) on that port. A pin group is a range of consecutive pins on the same port, for example B0-B4. Pin groups are identified by the port and bit number of the first pin and the pin count:

<source lang="cpp"> DECLARE_PIN( led1, D, 6); // allocate led1 to bit 6 of port D DECLARE_PIN( button1, B, 1); // button1 is connected to B1 DECLARE_PIN_GROUP( counter, D, 2, 3); // D2-D4 form a three bit counter output DECLARE_PIN_GROUP( rotary_encoder, D, 5, 2); // D5, D6 are some input, e.g. from a quadrature rotary encoder </source>

Whenever you declare a pin, you are declaring a variable. The type of this variable is constructed from some C++ template. Both the port and the bit number are encoded in the type, so that the variable itself can be completely empty. In fact, the compiler allocates zero bytes data space for the variable itself (we'll take a look under the hood later on).

Now it's time to configure the data direction registers for those pins that you'll use as outputs this can be done with the make_output() function, somewhat comparable to the pinMode() function on Arduino. <source lang="cpp"> make_output( led1); make_output( counter); </source>

This would be a good time to introduce pin lists. A pin list is a list of pins or pin groups. Most functions that accept a pin or pin group as parameter will also accept a list. Moreover, these functions will optimize their operations for those pins and pin groups that are in the same port. So not only can you use lists as shorthand notation, but it can actually result in shorter or faster code! There are two ways to create pin lists: the list_of() function and, as a shorthand notation, the bitwise-or operator: <source lang="cpp">

   // list_of
   some_function( list_of( led1)(counter)(led2) /* etc... */);
   // bitwise-or operator
   some_function( led1 | counter | led2 /* etc... */);

</source>

make_output() is one of those functions that accepts lists, so the two earlier function calls can be rewritten as: <source lang="cpp"> make_output( led1 | counter); // turn the led1 pin and the pins of counter into output pins. </source>

make_output() takes care not to change other bits in the DDR-registers. To squeeze the last drop of performance out of your initialization you could also use the init_as_output() function, which isn't so squeemish and will just set all other bits to zero in each of the DDR registers it touches. Use init_as_output() only if you know what you're doing. It should typically be used only once in a program.

Now that you've correctly configured the data direction registers, it's time to start reading from and writing to your pins: <source lang="cpp"> // set all bits of led1 and counter to LOW reset( led1 | counter);

// set the three bits of counter to some value write( counter, 0b101);

// set the led1 pin to HIGH write( led1, true);

// same as previous led1 to HIGH set( led1);

// read the two rotary encoder bits and assign to 'value'. uint8_t value = read( rotary_encoder);

uint8_t count = 0; for (;;) { // increase counter if button input is high if (is_set( button1)) ++count;

// same as above if (read( button1)) ++count;

write( counter, count); // since counter is 3-bit, value will be truncated }

</source>

Pins & Libraries

Being able to associate a symbol with a pin is convenient when creating self-contained pieces of firmware, but it becomes a necessity when creating re-usable components, such as the HD44780 LCD display library described earlier. There are many libraries out there that can be used to control devices like LCD-displays, NRF24L01(+) transceivers, WS2811(B) LED strings etc., and all of these somehow need the user of the library to specify the pins to which these devices are connected.

As an example, consider a bit-banging SPI library. Although most AVRs have hardware SPI support, sometimes it's convenient to use other pins for SPI signalling than the one designated pin that is also used by the AVR programmer. SPI is a dead-simple protocol to implement, but if we're moving the bits about ourselves, then it would be nice if it would be done as fast as possible, without the 50-clock delays between pin transitions. An excellent case therefore to demonstrate the use of a fast pin-twiddling library.

This is also a good opportunity to showcase C++'s excellent features for writing re-usable components for constrained devices. The SPI functions are implemented as static member functions of a C++ class template. All member functions are static because a SPI device that is associated with certain pins is always a singleton and we don't want to burden all member function calls with an unnecessary (hidden) this-pointer argument. In fact, the spi class acts more like a templated namespace than a class from which objects would be instantiated.

This class template takes a single template argument: a struct that defines which pins are associated with the three pin functions controlled by this class (mosi, miso and clk). Its use is as follows:

<source lang="cpp">

  1. include "avr_utilities/devices/bitbanged_spi.h"
  2. include "avr_utilities/pin_definitions.hpp"

struct spi_pins { DECLARE_PIN( mosi, B, 0); DECLARE_PIN( miso, B, 1); DECLARE_PIN( clk, B, 2); };

typedef bitbanged_spi<spi_pins> spi; // spi is now a class that uses B0, B1 and B2 as signal lines

// declare chip select lines DECLARE_PIN( select_some_spi_device, C, 5); DECLARE_PIN( select_some_other_spi_device, C, 4);

void init() {

   // initialize all spi pins
   spi::init();
   // Initialize all select pins. These are typically active-low.
   set( select_some_spi_device | select_other_spi_device);
   make_output( select_some_spi_device | select_other_spi_device);

}


void do_stuff( uint8_t value) {

   reset( select_some_spi_device);
   spi::transmit_receive( value);
   set( select_some_spi_device);

}

</source>

We already saw that the DECLARE_PIN macro declares a variable. By using this macro inside a struct, we're declaring member variables of a struct type. This struct type is used to tell the bitbanged_spi template which pins are associated with miso, mosi and clk. The spi library expects the user to set or reset the appropriate chip select lines before sending or receiving data (but it could be extended to do this for the user).

Now let's take a look at the implementation of bitbanged_spi. As stated, this is a class template that takes one argument: a struct that defines its pins:

<source lang="cpp"> template< typename pin_definitions> struct bitbanged_spi { private:

   static pin_definitions pins;
   /// send a byte via mosi, while at the same time listening for a byte at miso.
   /// This implements a mode 0 spi protocol, i.e. clock polarity is 0 (positive) and phase is 0.
   static uint8_t exchange_byte( uint8_t out)
   {
       uint8_t receive = 0;
       for (uint8_t mask = 0x80; mask; mask >>= 1)
       {
           write( pins.mosi, out&mask);
           set( pins.clk);
           if (is_set( pins.miso))
           {
               receive |= mask;
           }
           reset( pins.clk);
       }
       return receive;
   }

public:

   static void init()
   {
       reset( pins.clk);
       make_output( pins.mosi | pins.clk);
   }
   /// send and receive one byte at the same time.
   static uint8_t transmit_receive( uint8_t transmit)
   {
       uint8_t receive = exchange_byte( transmit);
       return receive;
   }
   /// send a buffer of bytes, replacing the contents with bytes received.
   static void transmit_receive( uint8_t *inout_buffer, uint8_t length)
   {
       while (length--)
       {
           *inout_buffer = exchange_byte( *inout_buffer);
           ++inout_buffer;
       }
   }
   /// send a buffer of bytes, not receiving any bytes.
   static void transmit( const uint8_t *out_buffer, uint8_t length)
   {
       while (length--)
       {
           exchange_byte( *out_buffer++);
       }
   }
   static void receive( uint8_t *in_buffer, uint8_t length)
   {
       while (length--)
       {
           *in_buffer++ = exchange_byte( 0);
       }
   }
   /// transmit a 16 bit value, msb first
   static void transmit( uint16_t value)
   {
   	exchange_byte( value >> 8);
   	exchange_byte( value & 0xff);
   }
   // transmit a zero-terminated string of characters.
   static void transmit( const char *text)
   {
       while (*text)
       {
           exchange_byte( *text--);
       }
   }

}; </source>

References

  1. wiring_digital.c, arduino sources at Google Code
  2. "To use or not use digitalWrite"