Strongly Typed Integers and Registers in C++

Today we have a guest post from Klemens Morgenstern, an embedded C++ consultant. You can learn more about Klemens on his website.


Type Safe Aliases

While type safety is a major help in avoiding stupid mistakes, it is not applied as widely as it could be. Consider the following example:

struct rectangle {
    int h;
    int w;

    rectangle(int height, int width) : h(height), w(width) {}
};

This will easily lead to mistakes, since a user of the Rectangle class might mix up the order and put width first. Thus, a way to declare this explicitly is preferable:

struct rectangle {
    int h;
    int w;
    rectangle(height height_, width width_) : h(height_.value), w(width_.value) {}
};

Generally speaking, we would then need to implement the height and width classes.

Note: The constructor has to be declared explicit to avoid implicit conversions from int.

struct height {
    int value;
    explicit height(int value) : value(value) {}
};

struct width {
    int value;
    explicit width(int value) : value(value) {}
};

With those declarations we now have to use explicit types when declaring a rectangle:

rectangle r{height(42), width(3)};

When combined with operator overloads, this pattern can be used for quite complex models, like SI Unit libraries.

If we only ever use integers there is a simpler approach we can take: using enum class with an explicit underlying type.

An enum class has the same behavior we expect from the struct example above. It also supports static_cast, allowing us to skip the .value member access.

enum class height : int {};
enum class width : int {};

struct rectangle {
    int h;
    int w;
    rectangle(height height_, width width_) : 
        h(static_cast<int>(height_)), w(static_cast<int>(width_)) {}
};

// height(42) is a cast, not a constructor call here
rectangle r{height(42), width(3)};

Register Access

One common source of errors in embedded development is register access, since registers are generally untyped. There are some ways to prevent these errors, like creating functions around the register accesses to prevent faulty usage. In my experience, this is only a partial solution, since there will be usages the library author has not anticipated.

First, let’s recap the common access patterns and see if we can make them type-safe.

This is a typical register definition (taken from an STM32):

typedef struct
{
  volatile uint32_t SR;     /* ADC status register,                         Address offset: 0x00 */
  volatile uint32_t CR1;    /* ADC control register 1,                      Address offset: 0x04 */      
  volatile uint32_t CR2;    /* ADC control register 2,                      Address offset: 0x08 */
    //..there are more, but left out for brevity sake
} ADC_t;

To access the hardware peripherals you get #define statements, like these:

#define ADC1                ((ADC_t *) 0x40012000)
#define ADC2                ((ADC_t *) 0x40012100)
#define ADC3                ((ADC_t *) 0x40012200)

We can access the registers for each peripheral like this:

ADC1->CR2 |= 0b11; //enable the ADC continuous
ADC1->CR2 &= (ADC1->CR2 & ~0b1); //disable again

This approach works, but raw bit operations are opaque for the person reading the code. You would usually use a preprocessor symbol for each field in the register:

#define  ADC_CR2_ADON                        ((uint32_t)0x00000001)        /* A/D Converter ON / OFF */
#define  ADC_CR2_CONT                        ((uint32_t)0x00000002)        /* Continuous Conversion */

ADC1->CR2 |= (ADC_CR2_ADON | ADC_CR2_CONT);
ADC1->CR2 &= ~ADC_CR2_ADON;

To make spotting errors easier, the ADC_CR2 register name is added to the symbols as a prefix, ideally so the following errors can be easily spotted:

ADC1->CR1 |= (ADC_CR2_ADON | ADC_CR2_CONT);
ADC1->CR1 &= ~ADC_CR2_ADON;

But these errors do not generate compiler errors. They are just int operations, and the compiler doesn’t know that we’re using CR2 register fields with the CR1 register. This is a major source of hard-to-detect bugs.

Type Safe Registers

In order to avoid improper register accesses, we first need to change the types in the struct. We’ll define unique types for each register using the enum class approach:


enum class sr_t : uint32_t {};
enum class cr1_t : uint32_t {};
enum class cr2_t : uint32_t {};

struct adc_t
{
  volatile sr_t  sr;     /* ADC status register,                         Address offset: 0x00 */
  volatile cr1_t cr1;    /* ADC control register 1,                      Address offset: 0x04 */      
  volatile cr2_t cr2;    /* ADC control register 2,                      Address offset: 0x08 */
    //..
};

static auto &adc1 = *(adc_t *)0x40012000;
static auto &adc2 = *(adc_t *)0x40012100;
static auto &adc3 = *(adc_t *)0x40012200;

Since we want to have the field constant typed, we declare them as global constants.

constexpr static auto adc_cr2_adon = cr2_t(0x00000001u); /* A/D Converter ON / OFF */
constexpr static auto adc_cr2_cont = cr2_t(0x00000002u); /* Continuous Conversion */

In order to use our new register types, we need to add operators for the required bitwise operations:

constexpr cr2_t operator~(cr2_t value) 
{
    return cr2_t(~static_cast<std::uint32_t>(value));
}

constexpr cr2_t operator|(cr2_t rhs, cr2_t lhs) {
    return cr2_t(static_cast<std::uint32_t>(rhs) | static_cast<std::uint32_t>(lhs));
}

constexpr cr2_t operator&(cr2_t rhs, cr2_t lhs) {
    return cr2_t(static_cast<std::uint32_t>(rhs) & static_cast<std::uint32_t>(lhs));
}

// not constexpr because it's volatile
inline volatile cr2_t& operator|=(volatile cr2_t &rhs, cr2_t lhs) 
{
    *reinterpret_cast<volatile std::uint32_t*>(&rhs) |= static_cast<std::uint32_t>(lhs); 
    return rhs;
}

inline volatile cr2_t& operator&=(volatile cr2_t &rhs, cr2_t lhs) 
{
    *reinterpret_cast<volatile std::uint32_t*>(&rhs) &= static_cast<std::uint32_t>(lhs); 
    return rhs;
}

We can use the same semantics as we did before:

adc1.cr2 |= (adc_cr2_adon | adc_cr2_cont);
adc1.cr2 &= ~adc_cr2_adon;

But now incorrect usage will generate an error:

adc1.cr1 |= (adc_cr2_adon | adc_cr2_cont);
adc1.cr1 &= ~adc_cr2_adon;

/*error: no match for 'operator|=' (operand types are 'volatile cr1_t' and 'cr2_t')
     40 |     adc1.cr1 |= (adc_cr2_adon | adc_cr2_cont); 
*/

The disadvantage of this approach is that we need to redeclare all those operators, since an enum class cannot be a template. However, it is simple to implement them with a macro.

Note: We have not talked about shift-operators, but they can be overloaded just like the others we used.

The huge advantage of this very simple approach over functions or a Domain-Specific EmbeddedLanguage [DSEL] is that this code looks exactly the same as the code you would write in C. We have simply added type checking to an established pattern. You also have the power to assert a certain style of register access. For example, you could delete operator &= and require assignments to be done like this adc1.cr2 = adc1.cr2 & ~adc_cr2_adon.

Using C++ Without the Heap

Want to use C++, but worried about how much it relies on dynamic memory allocations? Our course provides a hands-on approach for learning a diverse set of patterns, tools, and techniques for writing C++ code that never uses the heap.

Learn More on the Course Page

Share Your Thoughts

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