SFR access
The straightforward way of accessing memory-mapped Special Function Registers (SFR) is by casting the register address to a pointer.
#define sfr(addr) (*(volatile uint8_t*)(addr)) #define SOME_REG sfr(0x81) void c_way() { SOME_REG = 0x11; }
We can also do the same in C++, potentially gaining more type safety and avoiding the standard macro pitfalls. Annoyingly, because constexpr doesn’t allow reinterpret_cast, the usage requires the function call parenthesis, making it arguably uglier.
using Sfr8 = volatile uint8_t &; constexpr Sfr8 sfr8(uint16_t addr) { return *reinterpret_cast<volatile uint8_t *>(addr); } constexpr auto someReg() -> Sfr8 { return sfr8(0x81); } void cpp_way() { someReg() = 0x11; }
Both produce the same assembly output.
c_way(): ldi r24,lo8(17) sts 129,r24 ret cpp_way(): ldi r24,lo8(17) sts 129,r24 ret
Still, both ways are perfectly good for reading and writing whole registers. Most times, however, we need to access only a single bit, making the code slightly less readable and slightly more error-prone. The whole thing gets even more complicated if we need to access an integer that’s only a few bits wide in the middle of the SFR. Now we’re really asking for simple mistakes, and often end up using macros for these cases.
someReg() |= (1 << 2); someReg() &= ~(1 << 2); assert((someReg() & (1 << 2)) == 0); // tinyInt on bits 5-3 of someReg // | . . X X X . . . | // 7 4 3 0 constexpr int tinyIntLsb = 3; constexpr uint8_t tinyIntMask = 0b111 << tinyIntLsb; // Write 5 to tinyInt constexpr uint8_t tinyValueToWrite = 5; someReg() &= (~tinyIntMask); someReg() |= (tinyValueToWrite << tinyIntLsb);
Let’s write some utilities to make this more readable and easier to maintain. As usual, we can do that in multiple ways, with some tradeoffs in each case. I wanted the utility to work with SFR addresses determined both at compile-time and at run-time.
We can start by creating a struct template with the location of the bit/bits as template arguments. We’ll store the SFR address in a member variable. Let’s also add a utility for generating a mask for the bits, which will become useful for fields wider than one bit.
template <uint8_t lsb, uint8_t width = 1> struct RegBits { constexpr explicit RegBits(uint16_t addr_) : addr(addr_) {} [[nodiscard]] static constexpr uint8_t mask() { uint8_t m = 0; for (int i = 0; i < width; i++) m |= static_cast<uint8_t>(1 << (lsb + i)); return m; } const uint16_t addr; }
With the above ready, we can add functions for reading and writing the bits. They’re implemented as assignment and cast operators, to make their usage more natural.
void operator=(uint8_t value) const { if constexpr (width == 1) { auto r = reinterpret_cast<volatile uint8_t *>(addr); if (value != 0) { *r |= static_cast<uint8_t>(1 << lsb); } else { *r &= static_cast<uint8_t>(~(1 << lsb)); } } else { auto r = reinterpret_cast<volatile uint8_t *>(addr); *r = static_cast<uint8_t>((*r & ~mask()) | (value << lsb)); } } operator int() const { auto r = reinterpret_cast<volatile uint8_t *>(addr); return (*r & mask()) >> lsb; }
Now their usage has become very readable and straightforward, and the code still allows the compiler to optimize it pretty well.
constexpr auto TCCR1B() { struct Bits : SfrBase { RegBits<7> ICNC {regAddr}; RegBits<6> ICES {regAddr}; RegBits<3, 2> WGM32 {regAddr}; RegBits<0, 3> CS {regAddr}; }; return Bits {0x81}; }
void func() { TCCR1B().ICES = 1; TCCR1B().WGM32 = 2; }
func(): ldi r30,lo8(-127) ldi r31,0 ld r24,Z ori r24,lo8(64) st Z,r24 ld r24,Z andi r24,lo8(-25) ori r24,lo8(16) st Z,r24
The above experiments can be found on Compiler Explorer: https://godbolt.org/z/qf1nhfdWz
Unit testing
Using these utilities provides us with an interesting possibility. By swapping them for a UT version of the same functions and structures, we can make them read/write a known memory block, accessible from the unit test.
extern uint8_t mock_mem[1024]; //... Sfr8 sfr8(uint16_t addr) { return *(mock_mem + addr); } // ... void operator=(uint8_t value) const { if constexpr (width == 1) { auto r = reinterpret_cast<volatile uint8_t *>(mock_mem + addr); if (value != 0) { *r |= static_cast<uint8_t>(1 << lsb); } else { *r &= static_cast<uint8_t>(~(1 << lsb)); } } else { auto r = reinterpret_cast<volatile uint8_t *>(mock_mem + addr); *r = static_cast<uint8_t>((*r & ~mask()) | (value << lsb)); } } operator int() const { auto r = reinterpret_cast<volatile uint8_t *>(mock_mem + addr); return (*r & mask()) >> lsb; }
In the unit test, we define the mock_mem block, remembering to zero it in every test. The test cases can run code that reads/writes the SFRs, and verify the result. Admittedly, this only allows for testing simple reads/writes, and doesn’t account for any actual side effects of using the SFRs. Still, it’s basically free, and I found it very useful for verifying code that encapsulates access to the components on a microcontroller.
uint8_t mock_mem[1024] = {0}; auto memAt(uint16_t addr) -> int { return static_cast<int>(mock_mem[addr]); } TEST_CASE("AvrTimer8-CTC") { // class AvrTimer8 uses sfr8() and RegBits<> // to access the timer registers AvrTimer8 t0(t0cfg); memset(mock_mem, 0, sizeof(mock_mem)); constexpr auto cfg = AvrTimer8::CTCMode::configure(F_CPU, 4000); t0.apply(cfg); CHECK(memAt(Timer8Regs::TCCR0A) == 0x02); CHECK(memAt(Timer8Regs::TCCR0B) == 0x02); CHECK(memAt(Timer8Regs::OCR0A) == 0xf9); CHECK(memAt(Timer8Regs::OCR0B) == 0x00); CHECK(memAt(Timer8Regs::TIMSK0) == 0x00); }
Full example of the above unit test code can be found on GitHub.