Lucky Resistor
Menu
  • Home
  • Learn
    • Learn C++
    • Product Photography for Electronics
      • Required Equipment and Software
    • Soldering for Show
  • Projects
  • Libraries
  • Applications
  • Shop
  • About
    • About Me
    • Contact
    • Stay Informed
  •  
Menu

C++ Templates for Embedded Code (Part 2)

Posted on 2019-07-272022-09-04 by Lucky Resistor

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.

Read Part 1

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 uses int as a template parameter.
  • The second instance in line 8 is created on the heap using new with double 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.

ImplementationFirmware SizeRAM Usage
Maco Code532 bytes23 bytes
Function Templates532 bytes23 bytes
Class Template532 bytes23 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 bit mask in the Pin class using the oneBit function template. This change will make the code safer, because only one bit can be set.

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

It's Time to Use #pragma once

It’s Time to Use #pragma once

In my opinion, preprocessor macros and the outdated #include mechanism are one of the worst parts of the C++ language. It is not just these things are causing a lot of problems, even more, it ...
Read More
Real Time Counter and Integer Overflow

Real Time Counter and Integer Overflow

After writing the article about event-based firmware, I realised there are some misunderstandings about how real-time counters are working and should be used. Especially there is a misconception about an imagined problem if such a ...
Read More
How and Why to Avoid Preprocessor Macros

How and Why to Avoid Preprocessor Macros

While most naming conflicts in C++ can be solved using namespaces, this is not true for preprocessor macros. Macros cannot be put into namespaces. If you try to declare a new class called Stream, but ...
Read More
Guide to Modular Firmware

Guide to Modular Firmware

This article is for embedded software developers with a solid working knowledge of C or C++, but who struggle with large and complex projects. If you learn to develop embedded code, e.g. using the Arduino ...
Read More
Bit Manipulation using Templates

Bit Manipulation using Templates

Did you read my article about C++ templates for embedded code? You learned how to use function templates. This post adds a practical example to this topic. Bit Manipulation You may be familiar with bit ...
Read More
Event-based Firmware (Part 1/2)

Event-based Firmware (Part 1/2)

You start with small, simple firmware. But with each added feature, the complexity grows and grows. Soon, you need a good design in order to maintain the firmware and ensure the code remains clean and ...
Read More

8 thoughts on “C++ Templates for Embedded Code (Part 2)”

  1. Leon Matthews says:
    2019-07-31 at 00:33

    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!

    Reply
    1. Lucky Resistor says:
      2019-07-31 at 11:05

      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. 🙂

      Reply
  2. Herman says:
    2020-01-10 at 16:35

    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.

    Reply
    1. Lucky Resistor says:
      2020-01-10 at 17:36

      Which compiler version do you use? And for which platform do you try to compile the code?

      Reply
      1. Herman says:
        2020-01-10 at 17:54

        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.

      2. Herman says:
        2020-01-11 at 09:56

        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.

      3. Lucky Resistor says:
        2020-01-11 at 10:03

        So, do you use the latest version of the Arduino IDE? The latest version is 1.8.10.

      4. Herman says:
        2020-01-11 at 12:24

        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.

Leave a Reply Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Stay Updated

Join me on Mastodon!

Top Posts & Pages

  • Storage Boxes System for 3D Print
  • Event-based Firmware (Part 1/2)
  • Build a 3D Printer Enclosure
  • Yet Another Filament Filter
  • Circle Pattern Generator
  • Circle Pattern Generator
  • Real Time Counter and Integer Overflow
  • Projects
  • Logic Gates Puzzle 11
  • Units of Measurements for Safe C++ Variables

Latest Posts

  • Better Bridging with Slicer Guides2023-02-04
  • Stronger 3D Printed Parts with Vertical Perimeter Linking2023-02-02
  • Logic Gates Puzzle 1012023-02-02
  • Candlelight Emulation – Complexity with Layering2023-02-01
  • Three Ways to Integrate LED Light Into the Modular Lantern2023-01-29
  • The 3D Printed Modular Lantern2023-01-17
  • Rail Grid Alternatives and More Interesting Updates2022-12-09
  • Large Update to the Circle Pattern Generator2022-11-10

Categories

  • 3D Printing
  • Build
  • Common
  • Fail
  • Fun
  • Learn
  • Projects
  • Puzzle
  • Recommendations
  • Request for Comments
  • Review
  • Software
Copyright (c)2022 by Lucky Resistor. All rights reserved.