Difference between revisions of "Arduino-like pin definitions in C++"
From Just in Time
(→TL;DR) |
|||
(35 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | + | [[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. | ||
− | + | 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: | |
− | + | <source lang="cpp"> | |
− | + | #define LED_DDR DDRC | |
− | + | #define LED_PORT PORTC | |
+ | #define LED_PIN 5 | ||
+ | // set up | ||
+ | LED_DDR |= _BV(LED_PIN); | ||
+ | |||
+ | // assert pin | ||
+ | LED_PORT |= _BV(LED_PIN); | ||
+ | |||
+ | // clear pin | ||
+ | LED_PORT &= ~_BV(LED_PIN); | ||
+ | </source> | ||
+ | into code like the following, while maintaining the performance of the C-style original: | ||
+ | <source lang="cpp"> | ||
+ | PIN_TYPE( D, 6) led1; | ||
+ | |||
+ | make_output( led1); | ||
+ | set( led1); | ||
+ | clear( led1); | ||
+ | </source> | ||
+ | |||
+ | ==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–confusingly–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"> | ||
+ | PORTC |= 1 << 5; // set pin 5 in port C | ||
+ | </source> | ||
+ | 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—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"> | ||
+ | #define LED_PORT PORTC | ||
+ | #define LED_PIN 5 | ||
+ | |||
+ | #define FIREWORKS_PORT PORTB | ||
+ | #define FIREWORKS_PIN 1 | ||
+ | </source> | ||
+ | And then use it in their code: | ||
+ | <source lang="cpp"> | ||
+ | LED_PORT |= 1 << LED_PIN; // this is probably safe | ||
+ | </source> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <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> | ||
− | + | 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"> | ||
+ | #define LED_DDR DDRC | ||
+ | #define LED_PORT PORTC | ||
+ | #define LED_PIN 5 | ||
− | + | #define FIREWORKS_DDR DDRB | |
− | + | #define FIREWORKS_PORT PORTB | |
− | + | #define FIREWORKS_PIN 1 | |
− | + | void setup() | |
+ | { | ||
+ | FIREWORKS_PORT &= ~(1<<FIREWORKS_PIN); // make sure that the output pin is low | ||
+ | LED_DDR |= 1 << LED_PIN; | ||
+ | FIREWORKS_DDR |= 1 << FIREWORKS_PIN; | ||
+ | } | ||
− | ==TL;DR== | + | void warn_user() |
+ | { | ||
+ | LED_PORT |= 1 << LED_PIN; // this is probably safe | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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=== | ||
+ | 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<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"> | ||
+ | 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!<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: | 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: | ||
Line 21: | Line 169: | ||
// declare a single pin | // 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 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( counter, D, 2, 3); // D2, D3 and D4 are a counter | ||
− | DECLARE_PIN_GROUP( rotary_encoder, D, 5, 2); // D5, D6 are some | + | DECLARE_PIN_GROUP( rotary_encoder, D, 5, 2); // D5, D6 are some input, e.g. from a quadrature rotary encoder |
void do_stuff() | void do_stuff() | ||
Line 41: | Line 189: | ||
// output the result to the bits of the counter | // output the result to the bits of the counter | ||
write( counter, read(rotary_encoder)); | write( counter, read(rotary_encoder)); | ||
− | if (read(rotary_encoder) == | + | if (read(rotary_encoder) == 0b10) |
{ | { | ||
set( led1); | set( led1); | ||
Line 73: | Line 221: | ||
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. | 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 | + | 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—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"> | <source lang="cpp"> | ||
− | # | + | #include "avr_utilities/pin_definitions.hpp" |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
</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 [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 g |