Compiler-verification of pin assignments

This project uses a novel approach to selecting microprocessor peripherals from their pin numbers.

A typical microprocessor has multiple different features available on each pin. However, very rarely is every feature available on every pin, and the mapping between these features and their pins is only discoverable through a very long table in the data sheet. For instance, we might find that OC1 is available on pin 3.

The result is typically that the source code does not contain any reference to pin 3 at all, and only refers to the underlying hardare, OC1. This is bad for maintainability, as it tells the programmer nothing about how to check the wiring, or what to change should the wiring change.

Obviously then, we want a function to convert pin numbers into their corresponding hardware names, and this is not a hard task. But this creates a new danger - it encourages the programmer to change pin a pin to one that no longer supports the features needed! This kind of mistake can be caught at run-time, but when programming takes a few minutes, or getting feedback is difficult, this is not acceptable.

C++11 introduces a new keyword called constexpr. When attached to a variable, this tells the compiler that its value must be calculated at compile-time. The trick then, is to produce a function that for a valid pin, is calculable at compilation-time, and for an invalid pin, is not calculable at compilation-time. We capitalize on the fact that the compiler only cares about the compile-time-calculability of the code-path it takes:

// note: no constexpr
int failure(const char* msg) { report_error(msg); while(1); }

constexpr int must_be_even(int i) {
    return i % 2 == 0
        ? i                     // calculable at compile-time
        : failure("Not even");  // not calculable at compile-time
}

This almost works as intended:

constexpr int ok            = must_be_even(2);
constexpr int compile_error = must_be_even(3);
int           ok            = must_be_even(2);
int           runtime_error = must_be_even(3);

There is a isocpp paper [N3583] and a follow-up paper that details possible language changes and workaround to the problem of this third line. The solution opted for was to encourage writing these as:

constexpr int ok            = must_be_even<2>();
constexpr int compile_error = must_be_even<3>();
int           compile_error = must_be_even<2>();
int           compile_error = must_be_even<3>();
[N3583]Scott Schurr, Exploring constexpr at Runtime, 2013. https://isocpp.org/files/papers/n3583.pdf

API documentation

namespace io

Devices

These variables just turn the long #defined names into C++ references of the approapriate types

constexpr p32_oc &oc1 = *reinterpret_cast<p32_oc*>(_OCMP1_BASE_ADDRESS)
constexpr p32_oc &oc2 = *reinterpret_cast<p32_oc*>(_OCMP2_BASE_ADDRESS)
constexpr p32_oc &oc3 = *reinterpret_cast<p32_oc*>(_OCMP3_BASE_ADDRESS)
constexpr p32_oc &oc4 = *reinterpret_cast<p32_oc*>(_OCMP4_BASE_ADDRESS)
constexpr p32_timer &tmr1 = *reinterpret_cast<p32_timer*>(_TMR1_BASE_ADDRESS)
constexpr p32_timer &tmr2 = *reinterpret_cast<p32_timer*>(_TMR2_BASE_ADDRESS)
constexpr p32_timer &tmr3 = *reinterpret_cast<p32_timer*>(_TMR3_BASE_ADDRESS)
constexpr p32_timer &tmr4 = *reinterpret_cast<p32_timer*>(_TMR4_BASE_ADDRESS)
constexpr p32_timer &tmr5 = *reinterpret_cast<p32_timer*>(_TMR5_BASE_ADDRESS)
constexpr p32_i2c &i2c1 = *reinterpret_cast<p32_i2c*>(_I2C1_BASE_ADDRESS)
constexpr p32_i2c &i2c2 = *reinterpret_cast<p32_i2c*>(_I2C2_BASE_ADDRESS)
constexpr p32_cn &cn = *reinterpret_cast<p32_cn*>(_CN_BASE_ADDRESS)

Helpers

These functions allow conversion between devices and relevant configuation needed to use them elsewhere. These prevent a device having to be referred to by name in more than one place.

These should all be evaluated at compile time!

constexpr int irq_for(const p32_timer &tmr)

Get the interrupt bit number for a given timer.

constexpr int irq_for(const p32_cn &cn)

Get the interrupt bit number for the change notice hardware.

constexpr int vector_for(const p32_timer &tmr)

Get the interrupt vector number for a given timer.

constexpr int vector_for(const p32_cn &cn)

Get the interrupt vector number for the change notice hardare.

constexpr p32_timer &timer_for(uint8_t pin)

Get the timer connected to a given CK (clock) pin.

constexpr p32_oc &oc_for(uint8_t pin)

Get the output compare (PWM) connected to a given pin.

constexpr p32_i2c &i2c_for(uint8_t scl, uint8_t sda)

Get the I2C connected to a given pair of pins.

Compile-time Helpers

Like the above functions, but with arguments re-expresed as template parameters to force errors at compile time. These make it impossible to choose an invalid pin.

template <uint8_t pin>
p32_timer &timer_for()
template <uint8_t pin>
p32_oc &oc_for()
template <uint8_t scl, uint8_t sda>
p32_i2c &i2c_for()

Functions

template <typename T>
T failed(const char *)

Helper function for when an invalid argument is supplied.