Traits for STM32 pin functions

This article presents another example of using trait templates to help us not shoot ourselves in the foot with invalid pin assignments. If this means nothing to you please take a look Traits templates 101. If it all looks spookily familiar, perhaps you already read Traits for wake-up pins. The comments below do present essentially the same idea as the article on wake-up pins, but a little repetition of good news never hurt anyone. And, in addition, I consider an alternative using constexpr functions.

Alternate pin functions

You may or may not have used STM32 microprocessors (FYI: they’re grrrrreat!). All that matters here is to understand that, like other device families, they have many GPIO pins and many hardware peripherals: USARTs, SPIs, I2Cs, ADCs, DACs, timers, and so on. Each pin can be used directly for digital input and output, or it can be configured to perform a particular “alternate” function for a particular peripheral, such as the RX function for a USART, MISO for a SPI peripheral, Input/Output Channel 1 for a timer, or whatever.

STMicroelectronics Discovery Board (STM32F407VG)

Now it turns out that in the STM32 hardware design, only certain specific pins can be used for certain specific functions for certain specific peripherals. There were presumably compromises in the multiplexing in the silicon. Each pin can typically be used for one of several different functions – up to fifteen. That is, the RX function for USART2 can only be performed by one of three (say) particular pins; and pin PA3 can be used to perform one of several alternate functions, of which USART2 RX is but one.

The datasheets for STM32 parts contain large lookup tables detailing every possible alternate function for every pin on the device. Not all variants of the devices have the exactly the same lookup tables (they tend to be subsets of some super-table). When you are configuring a USART, say, you need to be careful to select appropriate pins for TX and RX (and CTS and RTS, if you use control flow). You also need to be careful to configure the correct alternate function index in each case. It would be very easy to make a mistake. I’ve made such mistakes once or twice. It was annoying and embarrassing.

Traits and alternate pin functions

Suppose I have a class USARTDriver which configures and uses a USART/UART peripheral on the STM32. We won’t care about it’s implementation, except that it is agnostic about the particular peripheral and pins it uses. We pass this information to the constructor with an instance of the following structure, which we create at compile time. I’ve omitted the baud rate and other stuff that could be included:

struct USARTConfig
{
    Periph usart;

    Port  tx_port;
    Pin   tx_pin;
    AltFn tx_alt_fn;

    Port  rx_port;
    Pin   rx_pin;
    AltFn rx_alt_fn;
};

  • Periph is an enumeration of all the peripherals on the device. Or we could use the base address for each peripheral’s memory mapped registers. Or something else.
  • Port is an enumeration of all GPIO ports on the device: PA – PK . Or we could use the base address for each port’s memory mapped registers. [STM32 ports are effectively just another kind of peripheral – one with sixteen sub-peripherals (pins) which share the same set of registers.]
  • Pin is an enumeration of possible pin values: P0 – P15 . Or we could just use an integer.
  • AltFn is an enumeration of possible alternate function indices: AF0 – AF15. Or we could just use as integer.
enum class Periph : uint8_t 
    { Usart2, Usart3, Tim2, Tim5, Tim9, /*...*/  };
enum class Port : uint8_t 
    { PA, PB, PC, /*...*/ };
enum class Pin : uint8_t 
    { P0, P1, P2, P3, /*...*/ };
enum class AltFn : uint8_t 
    { AF0, AF1, AF2, AF3, /*...*/ AF7, /*...*/ };

I’ve used my own enumerations in this example in order to completely isolate the vendor library code from the rest of the application. I’ve made them uint8_t explicitly so the compiler doesn’t eat up flash space with zeroes (the default underlying type for scoped enums is int). The driver implementation itself may or may not use the vendor library, but the rest of the application should neither know nor care.

Let’s create a trait class to capture the information about which pins can perform which alternate functions.

Primary template

As with the wake-up pins, we want a primary template for the general case:

enum class PinFn : uint8_t  
    { RX, TX, CH1, CH2, CH3, CH4, MOSI, /*...*/ };

template <Port PORT, Pin PIN, Periph PERIPH, 
    PinFn FUNC, bool DUMMY = false>
struct AltFnMap
{
    static_assert(DUMMY, "This alternate function "
        "combination does not exist.");
};

There is a new enumeration PinFn of all the different kinds of functions that pins can perform, and a template AltFnMap which is parametrised on a particular port, pin, peripheral and pin function combination. Note that the base template will force a compilation error with a hopefully helpful error message. This is slightly better than simply not compiling.

Template specialisations

To capture the lookup table, we simply need to create a template specialisation for each valid combination. This is potentially large, but has no impact at run time. Each specialisation gives us the relevant alternate function index.

// PA2
template <> struct AltFnMap<Port::PA, Pin::P2, Periph::Tim2, PinFn::CH3> 
{ static constexpr AltFn alt_fn = AltFn::AF1; };
template <> struct AltFnMap<Port::PA, Pin::P2, Periph::Tim5, PinFn::CH3> 
{ static constexpr AltFn alt_fn = AltFn::AF2; };
template <> struct AltFnMap<Port::PA, Pin::P2, Periph::Tim9, PinFn::CH1> 
{ static constexpr AltFn alt_fn = AltFn::AF3; };
template <> struct AltFnMap<Port::PA, Pin::P2, Periph::Usart2, PinFn::TX> 
{ static constexpr AltFn alt_fn = AltFn::AF7; };
// ...

// PA3
template <> struct AltFnMap<Port::PA, Pin::P3, Periph::Tim2, PinFn::CH4> 
{ static constexpr AltFn alt_fn = AltFn::AF1; };
template <> struct AltFnMap<Port::PA, Pin::P3, Periph::Tim5, PinFn::CH4> 
{ static constexpr AltFn alt_fn = AltFn::AF2; };
template <> struct AltFnMap<Port::PA, Pin::P3, Periph::Tim9, PinFn::CH2> 
{ static constexpr AltFn alt_fn = AltFn::AF3; };
template <> struct AltFnMap<Port::PA, Pin::P3, Periph::Usart2, PinFn::RX> 
{ static constexpr AltFn alt_fn = AltFn::AF7; };
// ...

And that’s our trait template done. Creating this table would seem to be quite a bit piece of work, and quite error prone, but in theory it would only have to be done once. More later…

Example usage

Using the trait templates to test pin selections – part 1

Now we could (but won’t) create the configuration for my driver instance as follows:

static constexpr Periph DBG_PERIPH  = Periph::Usart2; 
static constexpr Port   DBG_TX_PORT = Port::PA; 
static constexpr Pin    DBG_TX_PIN  = Pin::P2; 
static constexpr Port   DBG_RX_PORT = Port::PA; 
static constexpr Pin    DBG_RX_PIN  = Pin::P3; 

static constexpr USARTConfig debug_usart_conf = 
{
    DBG_PERIPH,

    DBG_TX_PORT,
    DBG_TX_PIN,
    AltFnMap<DBG_TX_PORT, DBG_TX_PIN, DBG_PERIPH, 
        PinFn::TX>::alt_fn,

    DBG_RX_PORT,
    DBG_RX_PIN,
    AltFnMap<DBG_RX_PORT, DBG_RX_PIN, DBG_PERIPH, 
        PinFn::RX>::alt_fn,
};

This looks a bit wordy, but all it does it define some constants. The constants for the peripheral, TX port and so on are really just there to avoid duplication in the definition of debug_usart_conf. You could imagine that these constants are actually created elsewhere, in some global system configuration file. The key thing to note is that if your selected combination of port, pin and peripheral cannot provide either the TX or RX functions, your code will not compile. That’s a nice outcome for not too much work.

It is worth messing about with code like this in the incomparable Compiler Explorer. Even with optimisation turned off, you will see lovely definitions of constant structures and no executable instructions at all except whatever else you put in for your example. But just change PA3 to PA2 and…

<source>: In instantiation of 'struct AltFnMap<(Port)0, (Pin)2, (Periph)0, (PinFn)0, false>':
<source>:104:5:   required from 'constexpr USARTConfig make_config() 
    [with Periph PERIPH = (Periph)0; Port TX_PORT = (Port)0; Pin TX_PIN = (Pin)2; 
     Port RX_PORT = (Port)0; Pin RX_PIN = (Pin)2]'
<source>:109:33:   required from here
<source>:67:19: error: static assertion failed: This alternate function combination does not exist.

That’s clear enough (ARM64 gcc 8.2).

Using the trait templates to test pin selections – part 2

So far, so good, but the way we initialise debug_usart_conf is still prone to error.

  1. We might makes a mistake with the various duplications of names.
  2. We might enter the wrong values from PinFn: TX when you meant RX.
  3. We might forget to use AltFnMap altogether: Just enter AltFn::AF12 directly…

We can easily fix all this by creating another simple template which does all the tedious legwork for us. Like so:

template <Periph PERIPH, Port TX_PORT, Pin TX_PIN, 
    Port RX_PORT, Pin RX_PIN>
struct USARTConfigT : USARTConfig
{
    constexpr USARTConfigT() : USARTConfig 
    { 
        PERIPH, 
        TX_PORT, TX_PIN, AltFnMap<TX_PORT, TX_PIN, 
            PERIPH, PinFn::TX>::alt_fn, 
        RX_PORT, RX_PIN, AltFnMap<RX_PORT, RX_PIN, 
            PERIPH, PinFn::RX>::alt_fn 
    } 
    {}
};

And now creating the configuration structure looks like the following snippet if all the hardware choices are hard-coded at this location in the code. It could hardly be more concise:

static constexpr USARTConfigT<Periph::Usart2, 
    Port::PA, Pin::P2, Port::PA, Pin::P3> 
    debug_usart_conf;

I’m not normally one to ask for changes to the language, but something that might be nice here, at least as a form of documentation, is named template arguments. Or you could just use named constants as before:

static constexpr USARTConfigT<DBG_PERIPH, 
    DBG_TX_PORT, DBG_TX_PIN, DBG_RX_PORT, DBG_RX_PIN> 
    debug_usart_conf;

Using the trait templates to test pin selections – part 3

Although I am perfectly happy that the inheritance usage above works just fine, maybe not everyone agrees. It is theoretically slicing the derived struct when the configuration is passed to the driver’s constructor (though there’s nothing to lose). It also puts all the arguments before the name of the defined constant structure, which may or may not be to your taste – it’s not exactly uniform initialisation.

There is at least one other way to get what we want, which you may prefer – use a template constexpr function instead. This simple example is possible since C++11:

template <Periph PERIPH, Port TX_PORT, Pin TX_PIN, 
    Port RX_PORT, Pin RX_PIN>
constexpr USARTConfig make_config()
{
    return
    { 
        PERIPH, 
        TX_PORT, TX_PIN, AltFnMap<TX_PORT, TX_PIN, 
            PERIPH, PinFn::TX>::alt_fn, 
        RX_PORT, RX_PIN, AltFnMap<RX_PORT, RX_PIN, 
            PERIPH, PinFn::RX>::alt_fn 
    }; 
};

And creating the driver configuration looks as follows:

static constexpr USARTConfig debug_usart_conf = 
    make_config<Periph::Usart2, Port::PA, 
    Pin::P2, Port::PA, Pin::P3>(); 

The outcome is identical. And now everyone is happy. Actually, now that I’ve typed it, I think I prefer the function. 🙂

Can we just forget all about templates?

Since I used a simple constexpr function above to invoke AltFnMap, I wondered if we could go further and avoid using templates at all. Perhaps. I must confess that I am not as familiar with the innards of constexpr functions as I should be, so this is part of my own journey…

We could in principle do something like this:

constexpr AltFn alt_fn_map(Port PORT, Pin PIN, Periph PERIPH, PinFn FUNC)
{
    using PinAltFn = std::tuple<Port, Pin, Periph, PinFn>;

    auto test = PinAltFn{PORT, PIN, PERIPH, FUNC}; 
    // PA2
    if      (test == PinAltFn{Port::PA, Pin::P2, Periph::Tim2, PinFn::CH3})  return AltFn::AF1; 
    else if (test == PinAltFn{Port::PA, Pin::P2, Periph::Tim5, PinFn::CH3})  return AltFn::AF2; 
    else if (test == PinAltFn{Port::PA, Pin::P2, Periph::Tim9, PinFn::CH1})  return AltFn::AF3; 
    else if (test == PinAltFn{Port::PA, Pin::P2, Periph::Usart2, PinFn::TX}) return AltFn::AF7; 

    // PA3
    else if (test == PinAltFn{Port::PA, Pin::P3, Periph::Tim2, PinFn::CH4})  return AltFn::AF1; 
    else if (test == PinAltFn{Port::PA, Pin::P3, Periph::Tim5, PinFn::CH4})  return AltFn::AF2; 
    else if (test == PinAltFn{Port::PA, Pin::P3, Periph::Tim9, PinFn::CH2})  return AltFn::AF3; 
    else if (test == PinAltFn{Port::PA, Pin::P3, Periph::Usart2, PinFn::RX}) return AltFn::AF7; 

    else static_assert("This alternate function combination does not exist.");

    return AltFn::AF0;
}

constexpr USARTConfig make_config(Periph PERIPH, Port TX_PORT, Pin TX_PIN, 
    Port RX_PORT, Pin RX_PIN)
{
    return
    { 
        PERIPH, 
        TX_PORT, TX_PIN, alt_fn_map(TX_PORT, TX_PIN, 
            PERIPH, PinFn::TX), 
        RX_PORT, RX_PIN, alt_fn_map(RX_PORT, RX_PIN, 
            PERIPH, PinFn::RX) 
    }; 
};

static constexpr USARTConfig debug_usart_conf = 
    make_config(Periph::Usart2, Port::PA, 
    Pin::P2, Port::PA, Pin::P2); 

It looks pretty neat and is arguable easier to understand. And perhaps the code is a little shorter than the template version, not that we should obsess over this.

I’ve used a tuple to make a sort of key out of the port, pin, peripheral and alternate function. I wondered if a std::map could be used instead of a chain of conditions, but it seems that this is not possible. There is nothing inherent in maps that make this an impossible dream, so I guess the committee didn’t think about it. Perhaps in a future standard… For now this code is OK (it’s only for compile time), or you could write a constexpr map, or there are sure to be libraries which already have one.

There is also the potential advantage of being able to use the function at runtime. This doesn’t seem useful for embedded, but never say never. That advantage is also a potential pitfall since you might not be evaluating the function at compile time as you intended – the compiler won’t say a thing (it’s not an error). I intensely dislike such hidden “gems”. I gather consteval will fix this in C++20. Why didn’t they just do that C++11? [Honestly, committee guys, I couldn’t care less about coroutines or ranges. How about C++23 focuses entirely on fixing little warts?].

But it doesn’t work!

From what I can tell (I’m using Compiler Explorer), the function works as expected when the arguments are valid, and produces identical output to the templates. But there is no compilation error when the arguments are invalid. It somehow completely ignores the static assertion. This isn’t what I wanted. I understand (I think): alt_fn_map() is a plain old function that just happens to be executable at compile time if conditions are right. There are no template parameters to statically check.

I felt sure that there must be some super-clever trick to make this do what I want. Super-clever tricks are generally anathema to me: writing solid production code is not a parlour game of competitive arcana. However, I did find one idea so simple that it would be churlish to complain. It looks like this:

// Just a wrapper around whatever the implementation defines assert() to be.
inline void assert_helper(bool condition) 
{
    assert(condition);
}

// When false, the ternary operator uses the comma operator to evaluate to 
// false, but only after calling assert_helper(). Note that assert_helper 
// is not constexpr - compilation error! If evaluated at runtime instead, 
// assert() behaves as normal. 
inline constexpr bool constexpr_assert(bool condition) 
{
    return condition ? true : (assert(condition), false);
}

There are other more complicated offerings out there, involving lambdas and perfect forwarding, but I haven’t really understood why they might be better. We can use constexpr_assert() in place of static_assert(), and all is well.

So it does work. And that’s great. But I’m still not sure I prefer it. I’m not sufficiently confident with the dark corners and imponderables yet to be completely comfortable with this approach. What I can say for certain is that templates have been around for a very long time, and will certainly get the job done for about the same amount of effort. And if you are using an earlier standard than C++14, templates are the only game in town.

Creating the lookup tables programmatically

Although this example of using traits to identify errors at compile time is simple, the truth is that the lookup table for the entire device is large. And slightly different devices have slightly different lookup tables. Another device may or may not have UART7 among its collection of peripherals, for example, with commensurate changes throughout the datasheet. Creating and maintaining a full table of template specialisations across the whole STM32F4 family, and then tailoring the table to match the various sub-families which are slightly different from each other, … This looks to be quite a big task. It would definitely be worth having, but that’s quite a lot of typing to double check and triple check against the datasheets.

In Datasheets in databases, I waxed lyrical about the potential benefits of a vendor-provided SQL database which contains literally everything that is useful to know about a given family of microprocessors – everything that can be found in the reference manual and datasheets. This article demonstrates a perfect example of my goal: with such a database, we could very easily write a little script to generate all the template specialisations we need for our particular device. We could even do it as a build step in cases where we are targeting multiple devices with the same code base.

I know of at least one case where someone spent a lot of effort parsing the Reference Manual itself (a PDF) with a script in order to extract some useful information. I don’t know how successful that effort was, but it is not a task I fancy much. Easier to generate the PDF from a more machine readable format (hint).

Conclusion

I realise that this article mostly just repeats the message from Traits for wake-up pins, but I thought an example with a potentially very large lookup table might feel more realistic. In any case, I don’t mind repeating the fact that we can use really simple ideas like this to convert potential run time faults into compile time errors. The C++ compiler is already pretty fussy about static type checking before we do anything at all, but we can extend it with very little effort to enforce arbitrary rules of our own and make it immediately absolutely crystal clear when we have made a silly mistake. That’s worth repeating, no?

Published by unicyclebloke

Somewhat loud, red and obnoxious.

Leave a comment