Which types to use for peripherals in function arguments

I am struggling with the rust type system and its use for embassy peripherals.

In this specific case, I'd like to move a peripheral ref into a struct that then provides some functionality, using a method ("setup"). Here, it is an SPI device and some GPIO pins for chip select etc.

The options I see (but fail to implement) are:

(1) use some "supertype" for the function parameter (maybe this is the wrong way to think about it, coming from a C++ and Python background).

(2) use some generic implementation with types that implement the required traits

For (1), I tried AnyPin, but did not manage to produce error-free code. I failed completely with finding the type for the Spi instance.

For (2), although I guess that this could work and be cool because it would produce code for exactly the instance of Pins and Spi that I pass to Foo::setup, I failed to figure out how to make my struct and its methods generic over the Spi instance and Pin functions.

Maybe I am not thinking the right way about the problem. Anyway, I would appreciate some hints on how this should be done the rust/embassy way.

Here is a MWE of the problem. Specifically, I'd like to pass the spi instance and some gpio outputs to Foo::setup.

#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]

use panic_probe as _;
use defmt_rtt as _;

use embassy_executor::Spawner;
use embassy_stm32::gpio::{AnyPin, Level, Output, Speed};
use embassy_stm32::spi::Spi;
use embassy_stm32::dma::NoDma;
use embassy_stm32::Config;
use embassy_stm32::time::Hertz;


pub struct Foo<'a> {
    // spi: embassy_stm32::spi::Spi<'a, T, Tx, Rx>,         <=== what should this type be ?
    cs: Option<embassy_stm32::gpio::Output<'a, AnyPin>>,    <=== and here?
}

impl<'a> Foo<'a> {
    pub fn new() -> Self {
        Self {
            // spi: None,
            cs: None,
        }
    }

    pub fn setup(&mut self,
                 // spi: embassy_stm32::spi::Spi<'a, T, Tx, Rx>,  // <=====
                 cs: embassy_stm32::gpio::Output<'a, AnyPin>,     // <=====
    ) {
        // self.spi = spi;
        self.cs = Some(cs);
    }
}

// ------------------------------ main -------------------------------

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let mut config = Config::default();
    config.rcc.hse = Some(Hertz(8_000_000));
    config.rcc.sys_ck = Some(Hertz(48_000_000));
    config.rcc.pclk1 = Some(Hertz(24_000_000));
    let perip = embassy_stm32::init(config);

    let mut spi_config = embassy_stm32::spi::Config::default();
    spi_config.frequency = Hertz(1_000_000);
    let mut spi = Spi::new_txonly(
        perip.SPI1, perip.PA5,  perip.PA7,
        NoDma, NoDma,
        spi_config
    );
    let mut cs = Output::new(perip.PA4, Level::High, Speed::VeryHigh);

    let mut foo = Foo::new();
    foo.setup(&spi, perip.PA4);   <=== this is the call I'd like to have
    // let mut foo = Foo::new(&mut spi, perip.PA4);  <=== or this

    // ....
}

I am also trying something like this:

pub struct Foo<'a, T, U>
where T: embassy_stm32::spi::Instance,
      U: embassy_stm32::gpio::Pin {
    spi: &'a T,
    cs: &'a U,
}

impl<'a, T, U> Foo<'a, T, U>
where
    T: embassy_stm32::spi::Instance,
    U: embassy_stm32::gpio::Pin {
    pub fn new(spi: &mut T, cs: &mut U) -> Self {
        Self {
            spi,
            cs,
        }
    }
}

but here I cannot move the SPI directly into the struct and there type of the pin also seems wrong...

Do you need to pass references? If you are fine with transfering ownership, you can do something like this:

type SpiType = Spi<'static, SPI1, NoDma, NoDma>;
type SpiCsType = Output<'static, PA4>;

struct Foo {
    spi: SpiType,
    cs: SpiCsType,
}

impl Foo {
    pub fn new(spi: SpiType, cs: SpiCsType) -> Self {
        Self { spi, cs }
    }
}

in main:

let mut device = Foo::new(spi, cs);
let mut buf = [0x0Au8; 4];
device.cs.set_low();
unwrap!(device.spi.blocking_transfer_in_place(&mut buf));
device.cs.set_high();

Transferring ownership is fine, but the type should be generic and allow any available SPI instance to be used, and any available output pin. This means I cannot hard code PA4 and SPI1 into the type.

What I would like is something like that (here with the change that I pass the data and clock pins, instead of CS):

UPDATE: this seems to work...

pub struct Foo<T, U, V: >
where T: embassy_stm32::spi::Instance,
      U: embassy_stm32::spi::MosiPin<T>,
      V: embassy_stm32::spi::SckPin<T>,

{
    spi: T,
    mosi: U,
    clk: V,
}

impl<T, U, V> Foo<T, U, V>
where
    T: embassy_stm32::spi::Instance,
    U: embassy_stm32::spi::MosiPin<T>,
    V: embassy_stm32::spi::SckPin<T>,
{
    pub fn new(spi: T, mosi: U, clk: V, ) -> Self {
        Self { spi, mosi, clk, }
    }

}

I would suggest using traits defined by embedded-hal to make it platform-agnostic. Here is an example.

struct Foo<SPI, CS> {
    spi: SPI,
    cs: CS,
}

impl<SPI, CS> Foo<SPI, CS>
where
    SPI: embedded_hal::spi::SpiBus,
    CS: embedded_hal::digital::OutputPin,
{
    pub fn new(spi: SPI, cs: CS) -> Self {
        Self { spi, cs }
    }
}

This provides more flexibility and can accommodate a variety of SPI and Output implementations that adhere to the embedded-hal traits.

You can learn more about embedded-hal and portability here.

@lonesometraveler I will for sure look into the embedded-hal crate.

Nevertheless, I would like to understand how to store the type returned by Spi::new_tx_only. It really bothers me that I am unable to specify the correct type. let mut spi = Spi::new_txonly(...)obviously works, but what if I want to store this same value in a field of struct Foo?

#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]

use panic_probe as _;
use defmt_rtt as _;

use embassy_executor::Spawner;
use embassy_stm32::gpio::{Pin, AnyPin, Level, Output, Speed};
use embassy_stm32::spi::{Spi, MosiPin, SckPin};
use embassy_stm32::dma::NoDma;
use embassy_stm32::{Config, Peripheral};
use embassy_stm32::time::Hertz;
use core::marker::PhantomData;

pub struct Foo <T, U, V>
    where
        T: embassy_stm32::spi::Instance,
        U: embassy_stm32::spi::MosiPin<T>,
        V: embassy_stm32::spi::SckPin<T>,
{
    phantom1: PhantomData<T>,
    phantom2: PhantomData<U>,
    phantom3: PhantomData<V>,
    // spi: <=== what is the type I need here ?
}

impl<T, U, V> Foo<T, U, V>
    where
        T: embassy_stm32::spi::Instance,
        U: embassy_stm32::spi::MosiPin<T>,
        V: embassy_stm32::spi::SckPin<T>,
{
    pub fn new(spi: T, mosi: U, clk: V) -> Foo<T, U, V>
    {
        let mut spi_config = embassy_stm32::spi::Config::default();
        spi_config.frequency = Hertz(1_000_000);
        // What I would like is have 'spi' in 'Self'
        let mut spi = Spi::new_txonly(
            spi, clk, mosi,
            NoDma, NoDma,
            spi_config
        );
        Foo::<T, U, V> { phantom1: PhantomData,
                         phantom2: PhantomData,
                         phantom3: PhantomData,
                         // spi: spi,
        }
    }
}


// ------------------------------ main -------------------------------

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let mut config = Config::default();
    config.rcc.hse = Some(Hertz(8_000_000));
    config.rcc.sys_ck = Some(Hertz(48_000_000));
    config.rcc.pclk1 = Some(Hertz(24_000_000));
    let perip = embassy_stm32::init(config);

    let mut spi_config = embassy_stm32::spi::Config::default();
    spi_config.frequency = Hertz(1_000_000);

    let mut foo = Foo::new(perip.SPI1, perip.PA7, perip.PA5);
    // ....
}

If you prefer a specific type, it would be Spi<'static, SPI1, NoDma, NoDma> as I previously recommended. If you aim for abstraction and portability, use the embedded-hal traits.

1 Like