Templates are a powerful feature of the C++ language, but their syntax can be complex. Here I will continue with the second part of the article.
Although the examples I provide are for the Arduino Uno and Adafruit Feather M0, the concepts will work with any platform and with all modern C++ compilers.
Recap of the First Part
In the first part of the article, you learned how to write function templates.
template<uint8_t bitIndex, typename Type> constexpr Type oneBit() { return static_cast<Type>(1) << bitIndex; } template<uint8_t bitIndex, typename Type> inline void setBit(Type &value) { value |= oneBit<bitIndex, Type>(); } template<uint8_t bitIndex, typename Type> inline void clearBit(Type &value) { value &= ~(oneBit<bitIndex, Type>()); }
You also learned how to use them.
void setup() { setBit<5>(DDRB); clearBit<5>(PORTB); } void loop() { }
We have seen that the template code produces a highly-optimized firmware.
// This is the compiled setup() code... 1aa: 25 9a sbi 0x04, 5 ; 4 1ac: 2d 98 cbi 0x05, 5 ; 5
I will now demonstrate how you use template classes in embedded code, then explain template specialisation.
What are Template Classes?
Template classes extend templates to C++ classes. This works similar to function templates, which I introduced in the first part.
While the compiler generates code for function templates when the function is called, code generation happens when an instance of a template class is created. It is best to assume that it occurs on first use, for both function templates and template classes.
Template Class Syntax
The syntax for template classes is similar to that of function templates. Prefix class
with the template
keyword and its parameter list:
template<typename Type> class Example { // ... };
You can use the template parameters, through the whole class definition, for variables and methods.
template<typename Type> class Example { public: Example(Type value) : _value(value) { } Type getValue() const { return _value; } void setValue(const Type &value) { _value = value; } private: Type _value; };
As you can see in this example, the template parameter Type
is used in the whole class, for the instance variable _value
, the constructor and both access methods.
Template Class Usage
You can use a template class like any other class, but you must provide all* template parameters.
* Since C++17, template parameters can be implicitly deduced from the constructor as you learned for function templates in the first part. I will assume you have an old compiler for this article.
In the next example we use our Example
class in various ways to demonstrate the syntax.
#include "Example.hpp" int main() { Example<int> intExample(100); int a = intExample.getValue(); intExample.setValue(100); auto doubleExample = new Example<double>(10.0); auto doubleValue = doubleExample->getValue(); doubleExample->setValue(doubleValue + 15.0); delete doubleExample; struct MyValue { int a; bool b; }; Example<MyValue> myExample({40, true}); auto myValue = myExample.getValue(); myValue.a -= 10; myValue.b = false; myExample.setValue(myValue); }
- In line 4 we create a new instance of
Example
on the stack as a local variable. It usesint
as a template parameter. - The second instance in line 8 is created on the heap using
new
withdouble
as template parameter. - The last example demonstrates that you can use any complex type for the template.
MyValue
works like any primitive data type with our class.
Practical Use Cases
Do you remember the “Sender” example we discussed in the first article? While it worked well, the code is difficult to use because the template parameter list must be repeated for each function call.
Improving the Sender Example
We can use a template class to easily solve this problem. You have to pass the template parameters only once to use the interface.
#pragma once // Immediate Example -> This will get better, do not use! template<uint8_t ioPortAddr, uint8_t ioDirAddr, uint8_t dataMask, uint8_t clockMask> class Sender { public: void initialize() { _SFR_IO8(ioDirAddr) |= (dataMask|clockMask); _SFR_IO8(ioPortAddr) &= ~(dataMask|clockMask); } void sendBit(bool oneBit) { if (oneBit) { _SFR_IO8(ioPortAddr) |= dataMask; } else { _SFR_IO8(ioPortAddr) &= ~dataMask; } _SFR_IO8(ioPortAddr) |= clockMask; _SFR_IO8(ioPortAddr) &= ~clockMask; } void sendByte(uint8_t data) { for (uint8_t i = 0; i < 8; ++i) { sendBit((data & 0b1u) != 0); data >>= 1; } } };
#pragma once // Immediate Example -> This will get better, do not use! namespace Sender { template<uint8_t ioPortAddr, uint8_t ioDirAddr, uint8_t dataMask, uint8_t clockMask> void initialize() { _SFR_IO8(ioDirAddr) |= (dataMask|clockMask); _SFR_IO8(ioPortAddr) &= ~(dataMask|clockMask); } template<uint8_t ioPortAddr, uint8_t dataMask, uint8_t clockMask> void sendBit(bool oneBit) { if (oneBit) { _SFR_IO8(ioPortAddr) |= dataMask; } else { _SFR_IO8(ioPortAddr) &= ~dataMask; } _SFR_IO8(ioPortAddr) |= clockMask; _SFR_IO8(ioPortAddr) &= ~clockMask; } template<uint8_t ioPortAddr, uint8_t dataMask, uint8_t clockMask> void sendByte(uint8_t data) { for (uint8_t i = 0; i < 8; ++i) { sendBit<ioPortAddr, dataMask, clockMask>((data & 0b1u) != 0); data >>= 1; } } }
We replace the namespace
with a template class and convert the functions to class methods. This serves to simplify the code and improve readability.
Our main issue here was the ugly use of the functions, and this problem is solved with the class template as well.
#include "Sender.hpp" Sender<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00100000u, 0b00010000u> gSender; const char data[] = "Hello World!"; void setup() { gSender.initialize(); } void loop() { for (uint8_t dataByte : data ) { gSender.sendByte(dataByte); } }
#include "Sender.hpp" const uint8_t cSpiPortAddr = _SFR_IO_ADDR(PORTB); const uint8_t cSpiDirAddr = _SFR_IO_ADDR(DDRB); const uint8_t cSpiDataMask = 0b00100000u; const uint8_t cSpiClockMask = 0b00010000u; const char data[] = "Hello World!"; void setup() { Sender::initialize<cSpiPortAddr, cSpiDirAddr, cSpiDataMask, cSpiClockMask>(); } void loop() { for (uint8_t dataByte : data ) { Sender::sendByte<cSpiPortAddr, cSpiDataMask, cSpiClockMask>(dataByte); } }
In the new version, all of the ugliness is found in line 3, where we declare the gSender
instance. The rest of the code looks clean, which is one of the main goals if you write an interface.
Now let’s compile this new implementation for Arduino Uno and compare the results.
Implementation | Firmware Size | RAM Usage |
---|---|---|
Maco Code | 532 bytes | 23 bytes |
Function Templates | 532 bytes | 23 bytes |
Class Template | 532 bytes | 23 bytes |
This is no surprise because all three firmware binaries are byte equal. Even if we use different implementations, we obtain the same result. However, we already gained better code quality and more flexibility.
If we use multiple instances of the sender, the advantage of a template class becomes obvious.
#include "Sender.hpp" Sender<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00100000u, 0b00010000u> gSenderA; Sender<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00001000u, 0b00000100u> gSenderB; Sender<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000010u, 0b00000001u> gSenderC; const uint8_t cDataSize = 16; const char cDataA[cDataSize] = "Hello World! "; const char cDataB[cDataSize] = "Templates for "; const char cDataC[cDataSize] = "flexible code. "; void setup() { gSenderA.initialize(); gSenderB.initialize(); gSenderC.initialize(); } void loop() { for (uint8_t i = 0; i < cDataSize-1; ++i ) { gSenderA.sendByte(cDataA[i]); gSenderB.sendByte(cDataB[i]); gSenderC.sendByte(cDataC[i]); } }
Accessing Pins / Hardware Abstraction
If you have experience with the Arduino development environment, you may be familiar with code similar to this:
const uint8_t cLedPin = 13; void setup() { pinMode(cLedPin, OUTPUT); } void loop() { digitalWrite(cLedPin, HIGH); digitalWrite(cLedPin, LOW); }
You probably never had a reason to question this interface, which is an abstract hardware access layer. This code works with any Arduino compatible board and there is no difference between the AVR and SAM architecture.
Now let’s compile this simple code and have a look at what we get.
00000206 <main>: ; Regular initialisation ; setup() ------------------------------------------------------------------------------- ; pinMode()... 28c: ed e9 ldi r30, 0x9D ; 157 28e: f0 e0 ldi r31, 0x00 ; 0 290: 24 91 lpm r18, Z 292: e9 e8 ldi r30, 0x89 ; 137 294: f0 e0 ldi r31, 0x00 ; 0 296: 84 91 lpm r24, Z 298: 88 23 and r24, r24 29a: 99 f0 breq .+38 ; 0x2c2 <main+0xbc> 29c: 90 e0 ldi r25, 0x00 ; 0 29e: 88 0f add r24, r24 2a0: 99 1f adc r25, r25 2a2: fc 01 movw r30, r24 2a4: e8 59 subi r30, 0x98 ; 152 2a6: ff 4f sbci r31, 0xFF ; 255 2a8: a5 91 lpm r26, Z+ 2aa: b4 91 lpm r27, Z 2ac: fc 01 movw r30, r24 2ae: ee 58 subi r30, 0x8E ; 142 2b0: ff 4f sbci r31, 0xFF ; 255 2b2: 85 91 lpm r24, Z+ 2b4: 94 91 lpm r25, Z 2b6: 8f b7 in r24, 0x3f ; 63 2b8: f8 94 cli 2ba: ec 91 ld r30, X 2bc: e2 2b or r30, r18 2be: ec 93 st X, r30 2c0: 8f bf out 0x3f, r24 ; 63 ; loop() -------------------------------------------------------------------------------- 2c2: c0 e0 ldi r28, 0x00 ; 0 2c4: d0 e0 ldi r29, 0x00 ; 0 2c6: 81 e0 ldi r24, 0x01 ; 1 2c8: 0e 94 70 00 call 0xe0 ; 0xe0 <digitalWrite.constprop.0> 2cc: 80 e0 ldi r24, 0x00 ; 0 2ce: 0e 94 70 00 call 0xe0 ; 0xe0 <digitalWrite.constprop.0> 2d2: 20 97 sbiw r28, 0x00 ; 0 2d4: c1 f3 breq .-16 ; 0x2c6 <main+0xc0> 2d6: 0e 94 00 00 call 0 ; 0x0 <__vectors> 2da: f5 cf rjmp .-22 ; 0x2c6 <main+0xc0>
000000e0 <digitalWrite.constprop.0>: e0: e1 eb ldi r30, 0xB1 ; 177 e2: f0 e0 ldi r31, 0x00 ; 0 e4: 94 91 lpm r25, Z e6: ed e9 ldi r30, 0x9D ; 157 e8: f0 e0 ldi r31, 0x00 ; 0 ea: 24 91 lpm r18, Z ec: e9 e8 ldi r30, 0x89 ; 137 ee: f0 e0 ldi r31, 0x00 ; 0 f0: e4 91 lpm r30, Z f2: ee 23 and r30, r30 f4: 09 f4 brne .+2 ; 0xf8 <digitalWrite.constprop.0+0x18> f6: 3c c0 rjmp .+120 ; 0x170 <digitalWrite.constprop.0+0x90> f8: 99 23 and r25, r25 fa: 39 f1 breq .+78 ; 0x14a <digitalWrite.constprop.0+0x6a> fc: 93 30 cpi r25, 0x03 ; 3 fe: 91 f0 breq .+36 ; 0x124 <digitalWrite.constprop.0+0x44> 100: 38 f4 brcc .+14 ; 0x110 <digitalWrite.constprop.0+0x30> 102: 91 30 cpi r25, 0x01 ; 1 104: a9 f0 breq .+42 ; 0x130 <digitalWrite.constprop.0+0x50> 106: 92 30 cpi r25, 0x02 ; 2 108: 01 f5 brne .+64 ; 0x14a <digitalWrite.constprop.0+0x6a> 10a: 94 b5 in r25, 0x24 ; 36 10c: 9f 7d andi r25, 0xDF ; 223 10e: 12 c0 rjmp .+36 ; 0x134 <digitalWrite.constprop.0+0x54> 110: 97 30 cpi r25, 0x07 ; 7 112: 91 f0 breq .+36 ; 0x138 <digitalWrite.constprop.0+0x58> 114: 98 30 cpi r25, 0x08 ; 8 116: a1 f0 breq .+40 ; 0x140 <digitalWrite.constprop.0+0x60> 118: 94 30 cpi r25, 0x04 ; 4 11a: b9 f4 brne .+46 ; 0x14a <digitalWrite.constprop.0+0x6a> 11c: 90 91 80 00 lds r25, 0x0080 ; 0x800080 <__TEXT_REGION_LENGTH__+0x7e0080> 120: 9f 7d andi r25, 0xDF ; 223 122: 03 c0 rjmp .+6 ; 0x12a <digitalWrite.constprop.0+0x4a> 124: 90 91 80 00 lds r25, 0x0080 ; 0x800080 <__TEXT_REGION_LENGTH__+0x7e0080> 128: 9f 77 andi r25, 0x7F ; 127 12a: 90 93 80 00 sts 0x0080, r25 ; 0x800080 <__TEXT_REGION_LENGTH__+0x7e0080> 12e: 0d c0 rjmp .+26 ; 0x14a <digitalWrite.constprop.0+0x6a> 130: 94 b5 in r25, 0x24 ; 36 132: 9f 77 andi r25, 0x7F ; 127 134: 94 bd out 0x24, r25 ; 36 136: 09 c0 rjmp .+18 ; 0x14a <digitalWrite.constprop.0+0x6a> 138: 90 91 b0 00 lds r25, 0x00B0 ; 0x8000b0 <__TEXT_REGION_LENGTH__+0x7e00b0> 13c: 9f 77 andi r25, 0x7F ; 127 13e: 03 c0 rjmp .+6 ; 0x146 <digitalWrite.constprop.0+0x66> 140: 90 91 b0 00 lds r25, 0x00B0 ; 0x8000b0 <__TEXT_REGION_LENGTH__+0x7e00b0> 144: 9f 7d andi r25, 0xDF ; 223 146: 90 93 b0 00 sts 0x00B0, r25 ; 0x8000b0 <__TEXT_REGION_LENGTH__+0x7e00b0> 14a: f0 e0 ldi r31, 0x00 ; 0 14c: ee 0f add r30, r30 14e: ff 1f adc r31, r31 150: ee 58 subi r30, 0x8E ; 142 152: ff 4f sbci r31, 0xFF ; 255 154: a5 91 lpm r26, Z+ 156: b4 91 lpm r27, Z 158: 9f b7 in r25, 0x3f ; 63 15a: f8 94 cli 15c: 81 11 cpse r24, r1 15e: 04 c0 rjmp .+8 ; 0x168 <digitalWrite.constprop.0+0x88> 160: 8c 91 ld r24, X 162: 20 95 com r18 164: 28 23 and r18, r24 166: 02 c0 rjmp .+4 ; 0x16c <digitalWrite.constprop.0+0x8c> 168: ec 91 ld r30, X 16a: 2e 2b or r18, r30 16c: 2c 93 st X, r18 16e: 9f bf out 0x3f, r25 ; 63 170: 08 95 ret
The compiled firmware requires 736 bytes flash and 9 bytes of RAM. You do not need to understand the assembler code to see its complexity and length. In this case, I removed all common parts and split it into two listings to keep the article short.
A Different Approach
This code complexity comes from the lookup tables which are compiled into every firmware. We can simplify this by wrapping the required code in a class.
class Pin13 { private: constexpr static uint8_t _mask = 0b00100000u; public: void configureAsOutput() { DDRB |= _mask; } void setHigh() { PORTB |= _mask; } void setLow() { PORTB &= ~_mask; } }; Pin13 cLedPin; void setup() { cLedPin.configureAsOutput(); } void loop() { cLedPin.setHigh(); cLedPin.setLow(); }
Let us compile this example and check the results. Now it only uses 450 bytes flash and the same 9 bytes of RAM. You can see the reason for this reduction in the generated code.
00000124 <main>: ; Regular initialisation ; setup() ------------------------------------------------------------------------------- ; cLedPin.configureAsOutput(); 1aa: 25 9a sbi 0x04, 5 ; 4 ; loop() -------------------------------------------------------------------------------- 1ac: c0 e0 ldi r28, 0x00 ; 0 1ae: d0 e0 ldi r29, 0x00 ; 0 ; cLedPin.setHigh(); 1b0: 2d 9a sbi 0x05, 5 ; 5 ; cLedPin.setLow() 1b2: 2d 98 cbi 0x05, 5 ; 5 ; loop() end 1b4: 20 97 sbiw r28, 0x00 ; 0 1b6: e1 f3 breq .-8 ; 0x1b0 <main+0x8c> 1b8: 0e 94 00 00 call 0 ; 0x0 <__vectors> 1bc: f9 cf rjmp .-14 ; 0x1b0 <main+0x8c>
Because our example only works with pin 13 of the hardware, it lacks the required flexibility. We can solve this with several class declarations.
#pragma once class Pin0 { // ... }; class Pin1 { }; // ... repeat for Pin2 ... Pin11 ... class Pin12 { private: constexpr static uint8_t _mask = 0b00010000u; public: void configureAsOutput() { DDRB |= _mask; } void setHigh() { PORTB |= _mask; } void setLow() { PORTB &= ~_mask; } }; class Pin13 { private: constexpr static uint8_t _mask = 0b00100000u; public: void configureAsOutput() { DDRB |= _mask; } void setHigh() { PORTB |= _mask; } void setLow() { PORTB &= ~_mask; } };
Using a single template class, we can make this even easier to understand:
#pragma once #include <Arduino.h> template<uint8_t ioPortAddr, uint8_t ioDirAddr, uint8_t mask> class PinT { public: void configureAsInput() { _SFR_IO8(ioDirAddr) &= ~mask; } void configureAsOutput() { _SFR_IO8(ioDirAddr) |= mask; } bool getInput() { return ((_SFR_IO8(ioPortAddr) & mask) != 0); } void setHigh() { _SFR_IO8(ioPortAddr) |= mask; } void setLow() { _SFR_IO8(ioPortAddr) &= ~mask; } }; using Pin0 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000001u>; using Pin1 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000010u>; using Pin2 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000100u>; using Pin3 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00001000u>; using Pin4 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00010000u>; using Pin5 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00100000u>; using Pin6 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b01000000u>; using Pin7 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b10000000u>; using Pin8 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000001u>; using Pin9 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000010u>; using Pin10 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000100u>; using Pin11 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00001000u>; using Pin12 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00010000u>; using Pin13 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00100000u>;
We built a table of declarations, but this one does not take space in the firmware. The using
keyword will create a simpler name for the declaration on the right. There is no change to our main code, but you can now access any pin we declared.
You may be familiar with typedef
declarations. using
is the modern alternative with a clean and simple syntax.
Our class has no instance variables, so we declare its methods static to allow a different usage.
#pragma once #include <Arduino.h> template<uint8_t ioPortAddr, uint8_t ioDirAddr, uint8_t mask> class PinT { public: inline static void configureAsInput() { _SFR_IO8(ioDirAddr) &= ~mask; } inline static void configureAsOutput() { _SFR_IO8(ioDirAddr) |= mask; } inline static bool getInput() { return ((_SFR_IO8(ioPortAddr) & mask) != 0); } inline static void setHigh() { _SFR_IO8(ioPortAddr) |= mask; } inline static void setLow() { _SFR_IO8(ioPortAddr) &= ~mask; } }; using Pin0 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000001u>; using Pin1 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000010u>; using Pin2 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000100u>; using Pin3 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00001000u>; using Pin4 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00010000u>; using Pin5 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00100000u>; using Pin6 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b01000000u>; using Pin7 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b10000000u>; using Pin8 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000001u>; using Pin9 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000010u>; using Pin10 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000100u>; using Pin11 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00001000u>; using Pin12 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00010000u>; using Pin13 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00100000u>;
Using static methods, we can declare variables for the pins but also use it directly.
#include "Pins.hpp" void setup() { Pin13::configureAsOutput(); } void loop() { Pin13::setHigh(); Pin13::setLow(); }
Combine the Pins and Sender Example
We have an abstract layer that can be used to access the pins of our board. Configuring the pins was the last issue of the sender example. To solve this, we combine both examples.
#include "Sender.hpp" #include "Pins.hpp" Sender<Pin13, Pin12> gSenderA; Sender<Pin11, Pin10> gSenderB; Sender<Pin9, Pin8> gSenderC; const uint8_t cDataSize = 16; const char cDataA[cDataSize] = "Hello World! "; const char cDataB[cDataSize] = "Templates for "; const char cDataC[cDataSize] = "flexible code. "; void setup() { gSenderA.initialize(); gSenderB.initialize(); gSenderC.initialize(); } void loop() { for (uint8_t i = 0; i < cDataSize-1; ++i ) { gSenderA.sendByte(cDataA[i]); gSenderB.sendByte(cDataB[i]); gSenderC.sendByte(cDataC[i]); } }
#pragma once template<typename DataPin, typename ClockPin> class Sender { public: void initialize() { DataPin::configureAsOutput(); ClockPin::configureAsOutput(); } void sendBit(bool oneBit) { if (oneBit) { DataPin::setHigh(); } else { DataPin::setLow(); } ClockPin::setHigh(); ClockPin::setLow(); } void sendByte(uint8_t data) { for (uint8_t i = 0; i < 8; ++i) { sendBit((data & 0b1u) != 0); data >>= 1; } } };
#pragma once #include <Arduino.h> template<uint8_t ioPortAddr, uint8_t ioDirAddr, uint8_t mask> class PinT { public: inline static void configureAsInput() { _SFR_IO8(ioDirAddr) &= ~mask; } inline static void configureAsOutput() { _SFR_IO8(ioDirAddr) |= mask; } inline static bool getInput() { return ((_SFR_IO8(ioPortAddr) & mask) != 0); } inline static void setHigh() { _SFR_IO8(ioPortAddr) |= mask; } inline static void setLow() { _SFR_IO8(ioPortAddr) &= ~mask; } }; using Pin0 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000001u>; using Pin1 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000010u>; using Pin2 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000100u>; using Pin3 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00001000u>; using Pin4 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00010000u>; using Pin5 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00100000u>; using Pin6 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b01000000u>; using Pin7 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b10000000u>; using Pin8 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000001u>; using Pin9 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000010u>; using Pin10 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000100u>; using Pin11 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00001000u>; using Pin12 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00010000u>; using Pin13 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00100000u>;
If we use the PinX
classes to configure Sender
, the code becomes clearer and more readable. It will give you a well-optimized, small firmware as a result.
To support different platforms, you can simply include different versions of Pins.hpp
into your project.
Try to get rid of the Pin
class using oneBit
Issues of this Solution
- You lose the runtime flexibility compared to the Arduino
digitalWrite()
interface, so you cannot use a file from an SD card to configure the pin numbers for signals. - There is no shared interface defining the methods of the
Pin
class. The names and syntax of the methods are only verified through its usage.
About the Shared Interface
If you regularly write desktop code, you would introduce a shared abstract interface:
#pragma once // Do not use - only for demo purposes class Pin { public: virtual void configureAsInput() = 0; virtual void configureAsOutput() = 0; virtual bool getInput() = 0; virtual void setHigh() = 0; virtual void setLow() = 0; };
#pragma once // Do not use - only for demo purposes #include "Pin.hpp" #include <Arduino.h> template<uint8_t ioPortAddr, uint8_t ioDirAddr, uint8_t mask> class PinT : public Pin { public: void configureAsInput() override { _SFR_IO8(ioDirAddr) &= ~mask; } void configureAsOutput() override { _SFR_IO8(ioDirAddr) |= mask; } bool getInput() override { return ((_SFR_IO8(ioPortAddr) & mask) != 0); } void setHigh() override { _SFR_IO8(ioPortAddr) |= mask; } void setLow() override { _SFR_IO8(ioPortAddr) &= ~mask; } }; using Pin0 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000001u>; using Pin1 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000010u>; using Pin2 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00000100u>; using Pin3 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00001000u>; using Pin4 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00010000u>; using Pin5 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b00100000u>; using Pin6 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b01000000u>; using Pin7 = PinT<_SFR_IO_ADDR(PORTD), _SFR_IO_ADDR(DDRD), 0b10000000u>; using Pin8 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000001u>; using Pin9 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000010u>; using Pin10 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00000100u>; using Pin11 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00001000u>; using Pin12 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00010000u>; using Pin13 = PinT<_SFR_IO_ADDR(PORTB), _SFR_IO_ADDR(DDRB), 0b00100000u>;
#pragma once // Do not use - only for demo purposes template<typename DataPin, typename ClockPin> class Sender { public: DataPin _data; ClockPin _clock; public: void initialize() { _data.configureAsOutput(); _clock.configureAsOutput(); } void sendBit(bool oneBit) { if (oneBit) { _data.setHigh(); } else { _data.setLow(); } _clock.setHigh(); _clock.setLow(); } void sendByte(uint8_t data) { for (uint8_t i = 0; i < 8; ++i) { sendBit((data & 0b1u) != 0); data >>= 1; } } };
You can use the abstract Pin
interface directly, without knowing the actual implementation at compile time.
void sendPulse(Pin &pin) { pin.setHigh(); pin.setLow(); }
This is the proper approach if you have a fast CPU and plenty of RAM. On a microcontroller, where you count every single byte, the benefits are too few to justify the introduction of an abstract interface.
Conclusion and Preview
At this point you should be able to write function templates and template classes. You know how to use them for various situations in embedded code. There are two topics left I will explain in the next part:
- Templates with default arguments
- Template specialisation
Do you have any questions, did you miss information, or simply want to provide feedback? Add a comment below or ask a question on Twitter!
Acknowledgements
Thanks to Saar, Zak and Keith for all the valuable contributions!
Learn More

How and Why to Avoid Preprocessor Macros

Guide to Modular Firmware

C++ Templates for Embedded Code

How to Deal with Badly Written Code

Write Less Code using the “auto” Keyword

This is so brilliant, thank you so much. Having a modern C++ interface to using AVR pins – and doing so efficiently – is so exciting. Cheers!
Thank you!
Just keep the downsides in mind: This is a static approach where configuration changes require to recompile the firmware. It is great if you need a very fast and small firmware, but you lose the run-time flexibility. Nevertheless, it is a good example to demonstrate the power of templates. 🙂
FYI :
Your pins.hpp fails to compile.
Searching I found that it is dependent on gcc version.
Error is :
..\LR1.h:41:20: error: ‘reinterpret_cast(43)’ is not a constant expression
using Pin0 = PinT;
Was not (yet) able to make a working version myself.
Which compiler version do you use? And for which platform do you try to compile the code?
I am compiling for Arduino_Nano
gcc version is :
avr-gcc (GCC) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
sorry for not giving this info in the first remark.
1/ I am compiling for AVR Arduino Nano
2/ Compiler used :
avr-gcc (GCC) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
So, do you use the latest version of the Arduino IDE? The latest version is 1.8.10.
Actually not. I am using eclipse based sloeber development tool, which is using the Arduino toolset.
But I just verified, on Arduino IDE 1.8.10 I do have the same issue.