esp-hal icon indicating copy to clipboard operation
esp-hal copied to clipboard

Spike: Gpio pins as a statically typed heterogeneous lists using frunk

Open Ben-PH opened this issue 2 years ago • 6 comments

Using frunk, and the following macro I managed to put together...

(You could think of it as a tuple, idexable by type)

macro_rules! create_pins {
    ($head:expr, $($tail:expr),+) => {
        HCons<GpioPin<Unknown, $head>, create_pins!($($tail),*)>
    };
    ($single:expr) => {
        HCons<GpioPin<Unknown, $single>, HNil>
    };
}

macro_rules! create_pins_head {
    ($head:expr, $tail:expr) => {
        HCons<GpioPin<Unknown, $head>, $tail>
    };
}

Using the macro like so:

type HetPinList = create_pins!(0, 1, 2, 3);

...expands to:

type HetPinList = HCons<GpioPin<Unknown, 0>,
                  HCons<GpioPin<Unknown, 1>,
                  HCons<GpioPin<Unknown, 2>,
                  HCons<GpioPin<Unknown, 3>,
                  HNil>>>>;

...which provides this handy interface

...so lets use it in our IO struct:

/// General Purpose Input/Output driver
pub struct IO<T: HList>
{
    _io_mux: IO_MUX,
    pins: T
}

impl<T: HList> IO<T> {
    /// Construct the initial list of pins
    pub fn new(gpio: GPIO, io_mux: IO_MUX) -> !/*IO<create_pins!(1, 2, 3)>*/ {
        let pins = todo!(); // construct the actual pins
        let io = IO {
            _io_mux: io_mux,
            pins,
        };
        io
    }
}

impl<T: HList + Plucker<GpioPin<Unknown, { PIN_NO }>, HList>> IO<T> {
    /// Claim a pin
    pub fn pin<const PIN_NO: u8>(&mut self) -> GpioPin<Unknown, PIN_NO> {
        let (pin, rest) = self.pins.pluck();
        self.pins = rest;
        pin
    }
}

note: the pins field is now private, replaced by a special getter, and instead of let pin18 = io.gpio18, you do let pin18 = io.pin::<18>()... though this depends on IO::pin taking &mut self, which might be difficult.

another example: The ability to construct a USB like so, as the type system should be able to infer that the pins need to be pins 18, 19, and 20 (esp32s3):

    let usb = USB::new(
        peripherals.USB0,
        io.pluck(),
        io.pluck(),
        io.pluck(),
        &mut system.peripheral_clock_control,
    );

That's unlikely, though, as the pin-list is a generic, changing it's monomorphised type each time you remove a pin

Possible pros that I see so far:

  • The possibility of compile-time, integer indexing to select pins
  • "auto-indexing" where the type system can infer which pin to take, such as when picking pins for Usb::new (each pin-arg can use only one pin)
  • It almost feels like GpioPins are a perfect use-case for Heterogeneous lists: Too different to be in an array without the overhead of dynamic dispatch (array of trait objects), and yet GpioPins are a series of the same thing, just with seperate, well defined, roles.

Possible cons:

  • A very FP-based design. Those not familiar with FP style lists might have a tough time getting to grips with this
  • Performance traps: Could this cause an explosion in the code size or compile time due to the huge(?i think?) leverage of compile-time generics

I'll continue investigating this...

Ben-PH avatar Aug 19 '23 12:08 Ben-PH

We also have type-erasure for gpio pins: https://github.com/esp-rs/esp-hal/blob/422eb2118676d4c1c938fa6cb2b8211fb177c232/esp32c3-hal/examples/blinky_erased_pins.rs#L68-L76

bjoernQ avatar Aug 20 '23 01:08 bjoernQ

...some developments which I'll put in a PR shortly, but it essentially means that you can rely on the type-system to

  • the IO struct encapsulates which pins are available at compile time using the type system
  • where the type-system constrains which pin can be used, the type-system will auto-magically select that pin
  • (To be confirmed): gets optimised away at compile time to not affect code-size, or run-time

The benefits to how code is written:

  • no partial-move problems: you can use helper methods to initialize pins
  • no conflicts with the borrow-checker: the IO struct can hand over ownership of individual pins (updating its type as it does so)

to illustrate, the blinky example could be re-written as so:

    // Initialize a blinky-pin using pin#4
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let (blinker, io) = Blinker::<4>::initialize(io);

    // One job of the clock system is to manage short delays...
    let mut delay = Delay::new(&clocks);

    blinker.blink_loop(&mut delay, 500u16);

...with the impl-detail defined as such:

struct Blinker<const PIN: u8> {
    pin: GpioPin<Output<PushPull>, PIN>
}

impl Blinker {
    fn initialize<T, U>(io: IO<T>) -> (Self, IO<U>)
        where T: HList, U: HList
    {
        let (led, io): ( GpioPin<Unknown, PIN>, _) = io.pins.pluck();
        let mut led = led.into_push_pull_output();
        led.set_high().unwrap()
        (led, io)
    }

    fn toggle(&mut self) {
        self.pin.toggle().unwrap();
    }
    
    fn blink_loop(self, delay: &mut Delay, rest_period: u16) -> ! {
        loop {
            self.toggle().unwrap();
            delay.delay_ms(rest_period);
        }
    }
}

This is a very basic e.g., will be working on a more complex one that should hopefully leverage its advantages in more detail.

Ben-PH avatar Aug 21 '23 11:08 Ben-PH

Progress report:

This syntax is close to viable. Struggling with managing the behavior marker traits (this comes out as needing the TogglableOutPutPin), but the core principals are proving viable:

    // Initialize a blinky-pin using pin#4
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let (pin, io) = io.init_pin(|pin| { 
        let pin = pin.into_push_pull_output();
        pin.set_high().unwrap();
        pin
    });
    let blinker = Blinker::<4>{pin};

    // One job of the clock system is to manage short delays...
    let mut delay = Delay::new(&clocks);

    let _ = blinker.blink_loop(&mut delay, 500u16);

...the next goal is to be able to have something like this (single-line initialization):

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let (blinker, io) = io.init_pin(|pin| { 
        let pin = pin.into_push_pull_output();
        pin.set_high().unwrap();
        Blinker::<4>{pin}
    });

    // One job of the clock system is to manage short delays...
    let mut delay = Delay::new(&clocks);

    let _ = blinker.blink_loop(&mut delay, 500u16);
    unreachable!()

Or a means to very easily define the construction on (in this e.g.) Blinky in terms of "taking an io, plucking a pin, initializing said pin, then returning Self alongside the remaning IO". That can be done, but the generics start looking a bit crazy and unweildy.

Ben-PH avatar Aug 22 '23 18:08 Ben-PH

This works:

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let (blinker, _io) = Blinker::initialize(io);

    // One job of the clock system is to manage short delays...
    let mut delay = Delay::new(&clocks);

    let _ = blinker.blink_loop(&mut delay, 500u16);
    unreachable!()

with this:

struct Blinker {
    pin: GpioPin<Output<PushPull>, 4>
}

impl Blinker
{
     fn initialize<T, Remaining>(io: IO<T>) -> (Self, IO<T::Remainder>)
     where 
         T: Plucker<GpioPin<Unknown, 4>, Remaining> 
     {
         let (pin, io) = io.pluck_pin();
         let mut pin = pin.into_push_pull_output();
         pin.set_high().unwrap();
         (Self{pin}, io)
     }
 
     fn toggle(&mut self) {
         self.pin.toggle().unwrap();
     }
     
    fn blink_loop(mut self, delay: &mut Delay, rest_period: u16) -> ! {
        loop {
            self.toggle();
            delay.delay_ms(rest_period);
        }
    }
}

...if you want to be generic on the pin for the `Blinker struct, it's a pain, because you need to tell the initializer all the trait constraints that apply:

struct Blinker<const PIN: u8> {
    pin: GpioPin<Output<PushPull>, PIN>
}

impl<const PIN: u8> Blinker<PIN>
{
     fn initialize<T, Remaining>(io: IO<T>) -> (Self, IO<T::Remainder>)
     where T: Plucker<GpioPin<Unknown, PIN>, Remaining>,
           GpioPin<Unknown, PIN>: GpioProperties,
           GpioPin<Output<PushPull>, PIN>: GpioProperties,
           <GpioPin<Output<PushPull>, PIN> as GpioProperties>::PinType: IsOutputPin,
           <GpioPin<Unknown, PIN> as GpioProperties>::PinType: IsOutputPin,
     {

... but that works :/

Ben-PH avatar Aug 22 '23 20:08 Ben-PH

#748

Ben-PH avatar Aug 22 '23 21:08 Ben-PH

progress update:

In my personal project, for setting up a mouse wheel, I used to have this:

 struct WheelEncoder {
    encoder_a: GpioPin<Input<PullUp>, 35>,
    encoder_b: GpioPin<Input<PullUp>, 36>,
    _gnd: GpioPin<Output<PushPull>, 0>,
    value: u8,
    state: bool,
    prev_state: bool,
    scroll_val: i8,
}

 
    // in top-level IO-ownership scope (usually main function):
    let encoder_a = io.pins.gpio35.into_pull_up_input();
    let encoder_b = io.pins.gpio36.into_pull_up_input();
    let mut wheel_gnd = io.pins.gpio0.into_push_pull_output();
    let _ = wheel_gnd.set_low();
    let mut wheel = WheelEncoder::new(encoder_a, encoder_b, wheel_gnd);

...now it's just this, and it can be wherever you want it:

let (mut wheel, _io) = WheelEncoder::hl_new(io, 0, true, true, 0);

To do this, I've fleshed out a derive macro with two key features:

  • list construction can be opted out per field
  • pin-initialization can be encoded directly into the struct definition

...the ergonomics/design of the derive-macro is still very pre-release. Next progress report should include a way to specify a method to initialize the struct: e.g. "call Self::new(x, y, z)"

#[derive(frunk::ListBuild)]
struct WheelEncoder {
    #[list_build_ignore]
    value: u8,
    #[list_build_ignore]
    state: bool,
    // into pull-up input
    #[plucker(GpioPin<Unknown, 35>, map=pin_a.into_pull_up_input())]
    pin_a: GpioPin<Input<PullUp>, 35>,
    // into pull-up input
    #[plucker(GpioPin<Unknown, 36>, map=pin_b.into_pull_up_input())]
    pin_b: GpioPin<Input<PullUp>, 36>,
    // into output, set to low.
    #[plucker(GpioPin<Unknown, 0>, map={ let res = pin_gnd.into_push_pull_output(); res.set_low(); res })]
    pin_gnd: GpioPin<Output<PushPull>, 0>,
    #[list_build_ignore]
    prev_state: bool,
    #[list_build_ignore]
    scroll_val: i8,
}

commit in which this works: https://github.com/Ben-PH/frunk-contrib/tree/eb45b29d988e764db1eb7c71fe0aaf4ed7fef7b1

Ben-PH avatar Sep 03 '23 14:09 Ben-PH

Thanks for investigating this @Ben-PH! I think we're moving in a direction where we want fewer generics and less magic where possible in esp-hal, which I believe this kind of approach would move us in the opposite direction. Therefore I will close this for now.

MabezDev avatar May 20 '24 09:05 MabezDev