Use of Generics for Embedded HAL Structs

Viewed 608

I am writing an embedded program for an STM32 microcontroller. I would like to be able to store the GPIO pins that represent buttons in a nice, consolidated struct.

Because embedded-hal encodes the state of a "Pin" struct with generics, each GPIO has a different Rust type, however for my case (buttons) they all implement the embedded_hal::digital::v2::InputPin trait.

I could explicitly specify the type for each member, however I also have to write the pin and port for each button when doing configuration, i.e.:

    let mut middle = gpioa
        .pa6
        .into_pull_up_input(&mut gpioa.moder, &mut gpioa.pupdr);

This means that specifying the type in the struct would cause redundancy, and if ever I need to change a pin in hardware, I will need to change it in code in more than one place.

What is the canonical way to store these pins? Of course I cannot use trait objects as it is in an embedded context.

At the moment I have the following:

use embedded_hal::digital::v2::InputPin;

struct Buttons<M, R, L, U, D>
where
    M: InputPin,
    R: InputPin,
    L: InputPin,
    U: InputPin,
    D: InputPin,
{
    middle: M,
    right: R,
    left: L,
    up: U,
    down: D
}

However this seems like a potential code smell.

Also, if I want to store this button struct inside another struct, the number of generics will most likely be very big in this parent struct unless I explicitly specify (and therefore write twice) the pin type.

Is there a better way to do this which is also efficient?

3 Answers

You can encapsulate the 5 types within a single one using associated types as such:

trait InputPin {}

trait InputPins {
    type M: InputPin;
    type R: InputPin;
    type L: InputPin;
    type U: InputPin;
    type D: InputPin;
}

struct Buttons<Pins: InputPins> {
    middle: Pins::M,
    right: Pins::R,
    left: Pins::L,
    up: Pins::U,
    down: Pins::D
}

Then to instantiate the Buttons you would use a single type that implements all of your necessary types using the InputPins trait.

Given some structs that all implement InputPin,

struct M;
struct R;
struct L;
struct U;
struct D;

you can construct an enum that also implements InputPin:

enum Pin {
    M(M),
    R(R),
    L(L),
    U(U),
    D(D),
}

impl InputPin for Pin {
    type Error = ...;

    fn is_high(&self) -> Result<bool, Self::Error> {
        match self {
            Pin::M(pin) => pin.is_high(),
            Pin::R(pin) => pin.is_high(),
            Pin::L(pin) => pin.is_high(),
            Pin::U(pin) => pin.is_high(),
            Pin::D(pin) => pin.is_high(),
        }
    }

    fn is_low(&self) -> Result<bool, Self::Error> {
        match self {
            Pin::M(pin) => pin.is_low(),
            Pin::R(pin) => pin.is_low(),
            Pin::L(pin) => pin.is_low(),
            Pin::U(pin) => pin.is_low(),
            Pin::D(pin) => pin.is_low(),
        }
    }
}

Then, you can use the Pin enum everywhere without generics.

As this can become cumbersome and repetitive, there is a handy crate to do it for you: enum_dispatch. It even claims to result in better performance than the handwritten code.

You could use into_dynamic. It removes some type information, namely the pin, but still has the port in the type.

atsamd-hal has DynPin, which does fully remove all type information. Something like this is what you need, but I'm not as familiar with the ST HALs.

Related