Specializing C++ templates on AVR pins
AVR is a popular processor core for microcontrollers, probably most well known from the ATmega chips on the classic Arduino boards. There's an excellent, free software C and C++ compiler toolchain available for them, which also usually sits behind the well-known Arduino IDE. AVR is a Harvard architecture, which means that code and data memory (i.e. flash and RAM) have separate address spaces. On these small, 8-bit chips there is very little RAM space (can be as little as 128 bytes on ATtiny devices). On the other hand, the amount of flash is usually much less restrictive.
Sometimes you want the microcontroller to drive multiple instances of the same external peripheral. Let's say for example this peripheral uses one GPIO pin and two instances differ only in the pin they are connected to. This means that the code in the microcontroller for peripheral 1 differs from code for peripheral 2 only by the address of the special function register (SFR) it uses and a bit mask. A naive way of implementing this in clean C++ would be something like the following:
#include <avr/io.h> class DIO { public: PORT_t * const port; const int pin_bm; DIO(PORT_t* port_, int pin_bm_) : port(port_), pin_bm(pin_bm_) {} void setup(void) const { port->DIR |= pin_bm; } // other methods would go here ... }; DIO dio1(&PORTB, PIN4_bm); DIO dio2(&PORTB, PIN5_bm); int main(void) { dio1.setup(); dio2.setup(); // other code would go here ... while (1); }
Note that I'm using the nice PORT_t
struct and the bit mask constants provided by avr-gcc headers instead of messing around with register addresses and such.
Unfortunately, this isn't optimal. Even though the class members are declared as const
, compiler stores them in RAM. This means that each instance of DIO
class costs 4 precious bytes of RAM that, baring a stray cosmic particle, will never ever change during the course of the program.
Compiling the code above for the ATtiny416 with avr-gcc 5.4.0 confirms this:
Program Memory Usage : 204 bytes 5,0 % Full Data Memory Usage : 8 bytes 3,1 % Full
We could store member variables as data in program space. This is possible by using some special instructions and variable attributes (PROGMEM
, pgm_read_byte()
etc.), but it's kind of ugly and doesn't play nicely with the C language. Another way is to actually have copies of the binary code for each peripheral, but only change the constants in each copy.
Obviously we don't want to have multiple copies in the source as well. In C the usual approach is to do something rude with C preprocessor and macros. Luckily, C++ offers a nicer way via templates. We can make a template class that takes non-type parameters and specialize that class on the register and bitmap it should use. Ideally, we would use something simple like this:
template <PORT_t* port, int pin_bm> class DIO { public: void setup(void) { port->DIR |= pin_bm; } }; DIO<(&PORTB), PIN4_bm> dio1; DIO<(&PORTB), PIN5_bm> dio1;
Sadly, this does not work. Behind the scenes, PORTB
is a macro that casts a constant integer to the PORT_t struct via a pointer:
#define PORTB (*(PORT_t *) 0x0620)
&PORTB
evaluates back to an integral constant, however the compiler isn't smart enough to realize that and balks at seeing a structure and a pointer dereference:
main.cpp(8,7): error: a cast to a type other than an integral or enumeration type cannot appear in a constant-expression DIO<(&PORTB), PIN4_bm> dio1; main.cpp(8,7): error: '*' cannot appear in a constant-expression main.cpp(8,7): error: '&' cannot appear in a constant-expression
I found a thread on the AVR freaks forum which talks about this exact problem. The discussion meanders around the usefulness of software abstractions and the ability of hardware engineers to follow orders, but fails to find a solution that does not involve a soup of C preprocessor macros.
Fortunately, there does seem to exist an elegant C++ solution. The compiler's restriction can be worked around without resorting to preprocessor magic and while still using the convenient constants defined in the avr-gcc headers. We only need to evaluate the macro in another expression and do the cast to PORT_t
inside the template:
template <int port_addr, int pin_bm> class DIO { public: void setup(void) { port()->DIR |= pin_bm; } static PORT_t* port() { return (PORT_t*) port_addr; } }; static const int PORTB_ADDR = (int) &PORTB; DIO<PORTB_ADDR, PIN4_bm> dio1; DIO<PORTB_ADDR, PIN5_bm> dio2;
This compiles and works fine with avr-gcc. As intended, it uses zero bytes of RAM. Interestingly enough, it also significantly reduces the code size, something I did not expect:
Program Memory Usage : 108 bytes 2,6 % Full Data Memory Usage : 0 bytes 0,0 % Full
I'm not sure why the first version using member variables produces so much more code. A brief glance at the disassembly suggests that the extra code comes from the constructor. I would guess that using a code-free constructor with initializer lists should generate minimal extra instructions. Apparently that is not so here. In any case, I'm not complaining. I'm also sure that in a non-toy example, the template version will use more flash space. After all, It has to duplicate the code for all methods.
You might also wonder how Arduino does this with the digitalWrite()
function and friends. They store a number of data tables in program memory and then have preprocessor macros to read them out using pgm_read_byte()
. As I mentioned above, this works, but isn't nicely structured and it's pretty far from clean, object-oriented C++ programming.