Actions

Difference between revisions of "Arduino-like pin definitions in C++"

From Just in Time

 
(33 intermediate revisions by the same user not shown)
Line 1: Line 1:
{{WIP}}
+
[[File:Lotsapins.jpg|right|300px]]
 +
In all non-trivial AVR projects, at some point in the software you need to define which pin is connected to what other device. Let's start with a manifesto:
 +
* Hard coding pin definitions–just using bit numbers, and port names at the places in code where you use pins–is evil
 +
* Raw AVR pin definitions, using the preprocessor to define ports and bit indexes can become cumbersome, makes for less readable ''set'' and ''reset'' functions and blocks optimization opportunities
 +
* Arduino style pin definitions using functions like ''digitalWrite'' facilitate readable code, but are really, really slow.
  
We regularly design "bare" AVR devices (meaning: non-Arduino). While we're doing that, we often need to do some of the following:
+
This page describes our [https://github.com/DannyHavenith/avr_utilities pin-definition library]. This library allows for Arduino-like syntax for pin usage, while keeping the performance of 'manual' pin manipulations in C. In short, it turns code like this:
* When designing a single-sided PCB, completely re-assign many pins in order to avoid bridges.
+
<source lang="cpp">
* Create a library for commonly used components like a hd44780 LCD display, or NRF24L01+ transceiver, making the pins that connect to these devices configurable.
+
#define LED_DDR        DDRC
* Implement non-trivial signaling protocols while keeping the source code readable.
+
#define LED_PORT      PORTC
 +
#define LED_PIN        5
 +
// set up
 +
LED_DDR |= _BV(LED_PIN);
 +
   
 +
// assert pin
 +
LED_PORT |= _BV(LED_PIN);
  
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.
+
// clear pin
 
+
LED_PORT &= ~_BV(LED_PIN);
In AVR-land, there are two main schools of defining your pins:
+
</source>
* The C-style way, using ''#define''s 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.
+
into code like the following, while maintaining the performance of the C-style original:
* 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.
+
<source lang="cpp">
 
+
PIN_TYPE( D, 6) led1;
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 [https://github.com/DannyHavenith/avr_utilities GitHub], in file [https://github.com/DannyHavenith/avr_utilities/blob/master/avr_utilities/pin_definitions.hpp 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'>
 
#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
+
make_output( led1);
set( led1 | led2);
+
set( led1);
 
+
clear( led1);
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>
 
</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
+
==Pin definitions, the state of the art==
 +
===Raw AVR===
 +
Most microcontroller projects revolve in some way around toggling signals on the output pins or reading values from input pins in some fashion. Concentrating on output for now, changing the output signals is done by manipulating individual bits in the AVR memory space that have been mapped onto these pins. So for example, if I wanted to make sure that pin 28 (the "top right" pin, which&ndash;confusingly&ndash;is pin 19 in Arduino's digital pin functions) of my atmega88 is in a "high" state, I would have to know that that pin is mapped on bit 5 in port C and write the following code:
 
<source lang="cpp">
 
<source lang="cpp">
     LED_PORT |= ( _BV( LED1_PIN)| _BV(LED2_PIN));
+
     PORTC |= 1 << 5; // set pin 5 in port C
 
</source>
 
</source>
...if ''led1'' and ''led2'' are on the same port. If the leds are on different ports, it should have performance equivalent to
+
In other words: If I want to manipulate the output state of a pin, I need to know the port (the address) of the pin and the bit position of that pin in the port. In Arduino land, instead of the or-expression above you would typically use the ''digitalWrite()''-function to manipulate pins if performance is not your highest priority. I'll get to the Arduino style functions later, but this section assumes either raw AVR or performance critical Arduino.
 +
 
 +
For most projects, especially for libraries, hardcoding the pins is not a good idea. You may want to re-route your PCB, or at some later point decide to add extra hardware that requires you to shift your pins a few places to the left or right. Additionally, it would be convenient to give some meaningful name to the pins, so that the reader of the code knows which pin controls the LED and which pin detonates the fireworks&mdash;which you will agree, is a useful distinction to make. Most developers define their pin positions in a separate section of their code, often in some header file:
 
<source lang="cpp">
 
<source lang="cpp">
    LED1_PORT |= _BV( LED1_PIN);
+
#define LED_PORT      PORTC
    LED2_PORT |= _BV( LED2_PIN);
+
#define LED_PIN        5
 +
 
 +
#define FIREWORKS_PORT PORTB
 +
#define FIREWORKS_PIN  1
 
</source>
 
</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:
+
And then use it in their code:
 
<source lang="cpp">
 
<source lang="cpp">
     LED13_PORT |= (_BV( LED1_PIN)| _BV( LED3_PIN));
+
     LED_PORT |= 1 << LED_PIN; // this is probably safe
    LED2_PORT  |= _BV( LED2_PIN);
 
 
</source>
 
</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++ [http://en.wikipedia.org/wiki/Template_metaprogramming 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.
+
If you're writing or using a library for a piece of connected hardware, then typically you'll use defines like the above to tell the library which pins are connected to the hardware. See for example how a semi-randomly chosen library<ref>[http://homepage.hispeed.ch/peterfleury/avr-lcd44780.html Interfacing a HD44780 Based LCD to an AVR]</ref> (for driving HD44780 LCD displays) defines which pins have which function:
 
 
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">
 
#define LCD_PORT  PORTA
 
#define LCD_DATA0_PORT  LCD_PORT
 
#define LCD_DATA1_PORT  LCD_PORT
 
#define LCD_DATA2_PORT  LCD_PORT
 
#define LCD_DATA3_PORT  LCD_PORT
 
#define LCD_DATA0_PIN  0
 
#define LCD_DATA1_PIN  1
 
#define LCD_DATA2_PIN  2
 
#define LCD_DATA3_PIN  3
 
#define LCD_RS_PORT  LCD_PORT
 
#define LCD_RS_PIN  4
 
#define LCD_RW_PORT  LCD_PORT
 
#define LCD_RW_PIN  5
 
#define LCD_E_PORT  LCD_PORT
 
#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) ''#define''s: 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">
 
<source lang="cpp">
#define LED1_PORT PORTB
+
#define LCD_PORT      PORTA
#define LED1_PIN 5
+
#define LCD_DATA0_PORT LCD_PORT
 +
#define LCD_DATA1_PORT LCD_PORT
 +
#define LCD_DATA2_PORT LCD_PORT
 +
#define LCD_DATA3_PORT LCD_PORT
 +
#define LCD_DATA0_PIN  0
 +
#define LCD_DATA1_PIN  1
 +
#define LCD_DATA2_PIN  2
 +
#define LCD_DATA3_PIN 3
  
// ...
+
#define LCD_RS_PORT    LCD_PORT
 +
#define LCD_RS_PIN    4
  
void f()
+
#define LCD_RW_PORT    LCD_PORT
{
+
#define LCD_RW_PIN     5
    // ...
 
    // flash led 1
 
    LED1_PORT &= ~_BV( LED1_PIN);
 
    _delay_ms( 50);
 
     LED1_PORT |= _BV( LED1_PIN);
 
  
     // ...
+
#define LCD_E_PORT     LCD_PORT
}
+
#define LCD_E_PIN      6
 
</source>
 
</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:
+
This is not all of the story. Before we can write to output pins, we need to configure them as such. This is done by setting the appropriate bit in the appropriate data direction (DDR) register. In addition to the output port, we should really define the data direction port for our pins as well. Some libraries, like the one above, work around this extra definition by making use of the fact that there is an easy relation between the address of the PORT register (e.g. PORTB) and its data direction register (DDRB, the relation is &DDRB == &PORTB - 1). But for this discussion, I'm going to define the data direction port separately:
  
 
<source lang="cpp">
 
<source lang="cpp">
#define LED1_DDR DDRB
+
#define LED_DDR        DDRC
#define LED1_PORT PORTB
+
#define LED_PORT      PORTC
#define LED1_PIN 5
+
#define LED_PIN        5
 +
 
 +
#define FIREWORKS_DDR DDRB
 +
#define FIREWORKS_PORT PORTB
 +
#define FIREWORKS_PIN 1
  
void initialize()
+
void setup()
 
{
 
{
    // make the pin for led1 an output
+
  FIREWORKS_PORT &= ~(1<<FIREWORKS_PIN); // make sure that the output pin is low
    LED1_DDR |= _BV( LED1_PIN);
+
  LED_DDR        |= 1 << LED_PIN;
    // do other outputs as well...
+
  FIREWORKS_DDR  |= 1 << FIREWORKS_PIN;
 
}
 
}
  
void f()
+
void warn_user()  
 
{
 
{
    // etc...
+
  LED_PORT |= 1 << LED_PIN; // this is probably safe
 
}
 
}
 
</source>
 
</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.
+
In summary, for each pin function we need to define the pin port, the bit position of that pin in the port and typically also the data direction register. As can be seen above, setting or resetting pins requires code that is not the easiest to read, although I guess you get used to statements like <code>''FIREWORKS_PORT &= ~(1<<FIREWORKS_PIN)''</code> (or the slightly more readable <code>''FIREWORKS_PORT &= ~_BV(FIREWORKS_PIN)''</code>).
  
 
===Arduino===
 
===Arduino===
Line 155: Line 116:
 
   // ...
 
   // ...
 
   // flash led 1
 
   // flash led 1
   digitalWrite(ledPin, LOW);
+
   digitalWrite(led1, LOW);
 
   delay(50);
 
   delay(50);
   digitalWrite(ledPin, HIGH);
+
   digitalWrite(led1, HIGH);
 
   // ...
 
   // ...
 
}
 
}
  
 
</source>
 
</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(ledPin, LOW)''. You just know what that line does.
+
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<ref>[https://code.google.com/p/arduino/source/browse/trunk/hardware/arduino/cores/arduino/wiring_digital.c wiring_digital.c], arduino sources at Google Code</ref> we can see that cost:
+
However, this readability comes at a significant cost. If we look at the implementation of digitalWrite<ref>[https://github.com/arduino/Arduino/blob/master/hardware/arduino/avr/cores/arduino/wiring_digital.c#L138 wiring_digital.c], arduino sources at GitHub</ref> we can see that cost:
  
 
<source lang="cpp">
 
<source lang="cpp">
Line 197: Line 158:
 
</source>
 
</source>
  
This will easily take 50 clock cycles<ref>[http://billgrundmann.wordpress.com/2009/03/03/to-use-or-not-use-writedigital/ "To use or not use digitalWrite"]</ref>! 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).
+
This will easily take 50 clock cycles!<ref>[http://billgrundmann.wordpress.com/2009/03/03/to-use-or-not-use-writedigital/ "To use or not use digitalWrite"]</ref> That's a bit steep, if all we wanted 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).
 +
 
 +
It would be nice if we could combine the readability and the single pin definition of Arduino's digitalWrite()-function with the performance of the raw AVR approach. pin_definitions is a library that offers just that.
 +
 
 +
==Pin_definitions TL;DR==
 +
Pin-definitions is a header-only library, you can find the sources on [https://github.com/DannyHavenith/avr_utilities GitHub], in file [https://github.com/DannyHavenith/avr_utilities/blob/master/avr_utilities/pin_definitions.hpp 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'>
 +
#include "avr_utilities/pin_definitions.hpp"
 +
 
 +
// declare a single pin
 +
PIN_TYPE( D, 6) led1;
 +
PIN_TYPE( B, 0) led2;
 +
 
 +
// 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 input, e.g. from a quadrature rotary encoder
  
==Introducing pin_defnitions.hpp==
+
void do_stuff()
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&mdash;plus I've done my stinking best to not let any compilation errors end up deep inside the source code of this library...
+
{
 +
// 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++ [http://en.wikipedia.org/wiki/Template_metaprogramming 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 will go into the details of the pin_definitions library and how it can be used.
 +
 
 +
==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. pin_definitions.hpp was written with that goal in mind. Although it makes heavy use of template metaprogramming, if you only use the library, you should find it very intuitive&mdash;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===
Line 208: Line 232:
 
</source>
 
</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:
+
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 [http://playground.arduino.cc/Learning/Pins 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">
 
<source lang="cpp">
Line 234: Line 258:
 
</source>
 
</source>
  
''make_output()'' is one of those functions that accepts lists, so the two function calls above can be rewritten as:
+
''make_output()'' is one of those functions that accepts lists, so the two earlier function calls can be rewritten as:
 
<source lang="cpp">
 
<source lang="cpp">
 
make_output( led1 | counter); // turn the led1 pin and the pins of counter into output pins.
 
make_output( led1 | counter); // turn the led1 pin and the pins of counter into output pins.
Line 270: Line 294:
 
}
 
}
  
 +
</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">
 +
#include "avr_utilities/devices/bitbanged_spi.h"
 +
#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
 +