Traits for wake-up pins

This is a real example from an embedded project I’ve been working on. The goal was to convert a potential run time error into a compile time error. If you haven’t done so already, please take a look at Traits templates 101, which introduces some of the ideas discussed below.

The scenario

The device is a Silicon Labs Mighty Gecko Zigbee doodad which needs to enter the microprocessor’s deep sleep state (a mode called EM4H – H for “hibernate”) for long periods of time in order to conserve energy. The chip is pretty much dead in this state, but can run the RTC. Aside from regular wake-ups driven by the RTC, the device also needs to wake up when the user presses a button.

This is a very simple thing to do in principle. You have to configure a GPIO pin as an input, you have to enable interrupts from that input, and you have to enable the EM4 wake-up capability for that pin. There are a few other bits and pieces, but those are the essentials.

As it turns out, there are only a very limited range of pins on the processor which can actually wake it up from EM4. Any pin can be used to generate interrupts, but only certain specific pins can bring the device out of hibernation. Worse, enabling the EM4 wake-up capability for a pin requires setting a bit in some other register whose bit index is not remotely related to the pin index.

Situations like this are very common in embedded development. I guess there were compromises when designing the silicon, or there was no decent coffee on hand, or something. You needs to read the datasheet very carefully to make sure you select an appropriate pin for the button and the correct EM4 enable bit.

Assuming the board design is correct, it would be very easy indeed to configure the wrong pin or something in the firmware, and you wouldn’t know about this until your button didn’t do anything three months from now when the board is finally ready. Or later refactoring, pin re-assignments, or something might cause problems. This might be relatively easy to fix, but we can do a little better. And yes, of course unit testing is a thing. Testing does present challenges when you get to down to the metal, so anything else that helps is good.

Step1: Create a compile time lookup table

We will use a simple custom trait class to create a lookup table which captures the relevant information from the data sheet.

Create the primary template

template <GPIO_Port_TypeDef PORT, 
    uint32_t PIN, bool VALUE = false> 
struct GPIOEM4Index
{
    static_assert(VALUE, "Given port and 
        pin are not allowed for EM4 wake-up.");
};

This template is parametrised on three non-type values. The first is a member of the enumeration GPIO_Port_TypeDef, which is defined in the vendor support library (EMLIB). The second is the index of a pin. The last is a dummy boolean value. The template, when it is instantiated, uses a static assertion to force a compilation error with a hopefully useful message. The boolean parameter prevents the assertion from giving errors even when not instantiated. I’m not sure if this is generally necessary, but it was with the IAR compiler (EDIT: This also appears to be true of gcc).

Create the lookup table with template specialisations

template <> struct GPIOEM4Index<gpioPortF,  2U> 
{ static constexpr uint32_t exti_level = GPIO_EXTILEVEL_EM4WU0; };  
template <> struct GPIOEM4Index<gpioPortF,  7U> 
{ static constexpr uint32_t exti_level = GPIO_EXTILEVEL_EM4WU1; };  
template <> struct GPIOEM4Index<gpioPortD, 14U> 
{ static constexpr uint32_t exti_level = GPIO_EXTILEVEL_EM4WU4; };  
template <> struct GPIOEM4Index<gpioPortA,  3U> 
{ static constexpr uint32_t exti_level = GPIO_EXTILEVEL_EM4WU8; };  
template <> struct GPIOEM4Index<gpioPortB, 13U> 
{ static constexpr uint32_t exti_level = GPIO_EXTILEVEL_EM4WU9; };  
template <> struct GPIOEM4Index<gpioPortC, 10U> 
{ static constexpr uint32_t exti_level = GPIO_EXTILEVEL_EM4WU12; }; 

The table is a list of template specialisations for each pin that supports the wake-up feature. The table creates a compile time map from (port, pin) pairs to particular values of a constant which I have called exti_level – this corresponds to the name of the register to which this datum relates. GPIO_EXTILEVEL_EM4WU0 and so on are constants defined in the vendor support library.

This map was obtained from a quick look at the datasheet. As you can see, there are just six pins which can be used to wake the device from EM4H. Wake-up source 0 is tied in the hardware to pin PF2, WU1 to PF7, and so on…

GPIOEM4Index is a simple example of a trait class. I was going to say “type trait”, but it is parametrised on multiple non-type parameters, rather than a single type parameter. It hardly matters in practice, and it works in essentially the same way.

Step 2: Use the lookup table at compile time

In my software, a digital input is an instance of a class called GPIOInput. We don’t care about it’s internals here, but it just wraps a few calls to functions in EMLIB. Each input is configured by passing a structure to its constructor. One of these:

struct GPIOInputConfig
{
    GPIO_Port_TypeDef port;
    uint32_t          pin; // MISRA didn't like uint8_t
    bool              wakeup;
    uint32_t          exti_level;
    ... // Other stuff we don't need here
};

// In class GPIOInput
// GPIOInput(const GPIOInputConfig& conf);

I’ve omitted all the stuff about pull-up/pull-down behaviour, which edges cause interrupts, and so on. You can easily extend the code to handle these later.

The configuration for a particular pin can be defined as follows. The structure button_conf is a compile time constant, and is passed to the GPIOInput constructor at some point in the firmware.

static constexpr GPIOInputConfig button_conf = 
{ 
    BTN_PORT, 
    BTN_PIN,
    true,
    GPIOEM4Index<BTN_PORT, 
        BTN_PIN>::exti_level
};

BTN_PORT and BTN_PIN are #defines generated by the Simplicity Studio configuration tool, but could just as easily be hand-written names, or explicit hard-coded values. The point to note is this: if the (BTN_PORT, BTN_PIN) combination does not appear in the lookup table, the code will simply not compile. And it will tell you why not. I love this. Using this code, it is now more difficult to create an invalid pin configuration for this feature. We have made a potential run time problem into a potential compile time problem.

But the code could stand some improvement. There are at least two obvious problems.

  1. BTN_PORT and BTN_PIN are duplicated. This is a potential source of error, such as editing only one of the duplicates. I know so because I made just such an error while developing this code. In fact, you could forget to use GPIOEM4Index altogether.
  2. GPIOInput can be used for pins which are digital inputs, but which do not need to wake the device up from hibernation. The code won’t compile. We can just pass 0 instead of invoking the GPIOEM4Index template, but this is not very satisfactory, and kind of undermines our efforts.

Remove the duplication of names

The solution to this is to create another template:

template <GPIO_Port_TypeDef PORT, 
          uint32_t PIN,
          bool EM4WU>
struct GPIOInputConfigT : GPIOInputConfig
{
    constexpr GPIOInputConfigT() 
    : GPIOInputConfig{PORT, PIN, EM4WU, 
          GPIOEM4Index<PORT, PIN>::exti_level} 
    {
    }
};

And we use it like this:

static constexpr GPIOInputConfigT 
    <BTN_PORT, BTN_PIN, true> button_conf;
  • GPIOInputConfigT inherits from GPIOInputConfig and adds nothing more than a default constructor which initialises all the values in the base class.
  • Note that the constructor is marked constexpr so that it can “run” at compile time.
  • The values we want are all passed as non-type template parameters and forwarded directly to the base struct.
  • The invocation of GPIOEM4Index in internalised, and the duplication of names is avoided by using this template. You also can’t forgot to use it now.

Isn’t that neat? The code for the button’s configuration is now both smaller and better.

You may be slightly worried that button_conf is now an instance of a derived struct, so that when we pass it to the GPIOInput constructor, we will slice the object. There is nothing to slice off anyway, so we lose nothing. There are probably other ways to achieve the same result, such as a constexpr function call. But this works just fine.

Inputs that can’t (or won’t) wake up the device

GPIOInputConfigT is nice, but makes the second issue worse: we can no longer pass 0 for the exti_level value for the pins which don’t have or don’t need this feature. The code won’t compile.

You may have noticed the boolean wakeup value in the configuration structure. This is intended to tell the GPIOInput object whether or not to enable the EM4 wake-up feature for its pin. The solution to our problem involves testing this value at compile time. It is passed to the template as EM4WU:

constexpr GPIOInputConfigT() 
: GPIOInputConfig{PORT, PIN, EM4WU} 
{
    if constexpr (EM4WU)
    {
        em4_exti_level = GPIOEM4Index<PORT, PIN>::exti_level; 
    }
}
  • The constructor is re-written to use the “if constexpr” construction.
  • This evaluates the test at compile time and either generates the dependent code or doesn’t, depending on the value.
  • We check the value of EM4WU, a boolean. If it is true, we invoke GPIOEM4Index. If not we do nothing. Problem solved.
  • It might make sense to set a default of 0 or something here or in the base struct declaration, but it doesn’t really matter since the flag is false. The aggregate initialisation of GPIOInputConfig should do this for us anyway.

Now we can use GPIOInputConfigT to create constexpr configurations for all of our digital inputs whether they need the EM4 wake-up feature or not.

Note that none of this requires us to statically initialise our actual GPIOInput instances. We can create them, or not, as necessary at run time. If you should need to select the port and pin dynamically (unlikely), then this technique is not going to help so much. But you’d assert on a lookup table at run time, right?

I can think of other improvements, such as changing the wake-up flag from a bool to an enumeration, and similarly with the pin index. And some better names might be in order.

Conclusion

That might have seemed like a lot of typing (it took under twenty minutes to get this working), but the benefit is that we have made configuring digital inputs a little bit safer in the face of board revisions which change the pins around, and of many others sources of discrepancy between what we intended and what we did.

The ability to convert run time faults into compile faults is one of my favourite advantages of using C++ for embedded software development. Just try doing that with C. All the magic happens in the compiler, and it has literally cost nothing in terms of image size or performance. Since it is all done in terms of compile-time constants, we don’t even have to optimise to make the template stuff evaporate: no actual code is generated.

Much as I rail against the apparent growing obsession with template-meta-program-all-the-things, little tricks like this are just awesome. Does this count as TMP anyway? I don’t know. Maybe. Entry level. All that matters is it isn’t very hard to understand and it puts power in your hands.

If you work on a lot of devices using the same hardware, you could move the code into a little header-only library called “Mighty Gecko FootGun Controls” or whatever, and it the effort would pay for itself in no time.

Published by unicyclebloke

Somewhat loud, red and obnoxious.

One thought on “Traits for wake-up pins

Leave a comment