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.

Leave a Reply

Your email address will not be published. Required fields are marked *