The Rusty Clams

Rust adventures !

Rust Embassy (P5) - A more generic setup

04 Sep 2025

After creating a few more demos with the LR2021 driver, I wanted to handle some different setup without breaking everything.

The first point concern the busy pin: I started the driver with a generic InputPin which requires polling when waiting for a given state. Technically it would be better to use a pin which implement Wait like the interrupt pin ExtiInput on the STM32, but those are not available on all devices and might be limited in number.

The second point is about which kind of SPI to use: the STM32 proposes either an async one using DMA or a blocking one which use register access. Since I wanted a true async driver, I had no choice but using the DMA one. Unfortunately this is quite inefficient when SPI transfer are short, which is most of the use case in the driver where commands rarely take more than 10 bytes.

Finally, the whole board setup can be centralized in one structure to avoid boiler-plate code for each new applications

Driver generic over the busy pin type

Let’s start by addressing the busy pin polling issue. The initial idea was simple:

Simple in principle, except negative bound is not supported yet in Rust.

The trick is to define another trait for the pin with the method wait_ready and an associated type to store the pin type:

trait Sealed{}
#[allow(private_bounds)]
pub trait BusyPin: Sealed {
    type Pin: InputPin;

    #[allow(async_fn_in_trait)]
    async fn wait_ready(pin: &mut Self::Pin, timeout: Duration) -> Result<(), Lr2021Error>;
}

For details about Sealed trait checkout Predrag’s blog. The idea is to prevent any user of the library to implement the trait for their type which could create breaking change if we ever need add methods.

Then we define two zero-size structs, composed only of a PhantomData that we can use to represent type implementing Input+Wait and those implementing only Input.

pub struct BusyBlocking<I> {
    _marker: PhantomData<I>
}
pub struct BusyAsync<I> {
    _marker: PhantomData<I>
}

BusyBlocking implements the BusyPin trait on type implementing Input:

impl<I: InputPin> BusyPin for BusyBlocking<I> {
    type Pin = I;

    /// Poll busy pin until it goes low
    async fn wait_ready(pin: &mut I, timeout: Duration) -> Result<(), Lr2021Error> {
        let start = Instant::now();
        while pin.is_high().map_err(|_| Lr2021Error::Pin)? {
            if start.elapsed() >= timeout {
                return Err(Lr2021Error::BusyTimeout);
            }
            // Timer::after_micros(5).await;
        }
        Ok(())
    }
}

BusyAsync implements the BusyPin trait on type implementing Input+Wait:

impl<I: InputPin + Wait> BusyPin for BusyAsync<I> {
    type Pin = I;

    /// Wait for an interrupt on the busy pin to go low (if not already)
    async fn wait_ready(pin: &mut I, timeout: Duration) -> Result<(), Lr2021Error> {
        // Option 1: Use the Wait trait for more efficient waiting
        if pin.is_high().map_err(|_| Lr2021Error::Pin)? {
            match with_timeout(timeout, pin.wait_for_low()).await {
                Ok(_) => Ok(()),
                Err(_) => Err(Lr2021Error::BusyTimeout),
            }
        } else {
            Ok(())
        }
    }
}

And now in the driver structs, we replace the pin type by a type implementing BusyPin, and we use the associated type for the type of busy.

pub struct Lr2021<O,SPI, M: BusyPin> {
    /// Reset pin  (active low)
    nreset: O,
    /// Busy pin from the LR2021 indicating if the LR2021 is ready to handle commands
    busy: M::Pin,
    /// SPI device
    spi: SPI,
    /// NSS output pin
    nss: O,
    /// Buffer to store SPI commands/response
    buffer: CmdBuffer,
}

Implementing the SpiBus trait for the STM32 blocking SPI

Due to the orphan rule in Rust it is not possible to directly implement a trait for a type when both are defined in external crates (this prevents potential conflicting trait implementation).

But this is not a real issue in our case: the standard workaround is to simply use a new-type wrapping the STM32 type, this way the type is now local to the crate.

pub struct SpiWrapper(pub Spi<'static,embassy_stm32::mode::Blocking>);

The implementation of the SpiBus Trait is actually quite simple: we need to implement 5 methods (flush, write, read, transfer, transfer_in_place) and those methods already exists in the base type:

impl<W: embassy_stm32::spi::Word> embedded_hal_async::spi::SpiBus<W> for SpiWrapper {
    async fn flush(&mut self) -> Result<(), Self::Error> {
        Ok(())
    }

    async fn write(&mut self, words: &[W]) -> Result<(), Self::Error> {
        self.0.blocking_write(words)
    }

    async fn read(&mut self, words: &mut [W]) -> Result<(), Self::Error> {
        self.0.blocking_read(words)
    }

    async fn transfer(&mut self, read: &mut [W], write: &[W]) -> Result<(), Self::Error> {
        self.0.blocking_transfer(read, write)
    }

    async fn transfer_in_place(&mut self, words: &mut [W]) -> Result<(), Self::Error> {
        self.0.blocking_transfer_in_place(words)
    }
}

On top of that the ErrorType Trait also needs to be implemented:

impl embedded_hal_1::spi::ErrorType for SpiWrapper {
    type Error = embassy_stm32::spi::Error;

And that’s it, nothing more to do. In my application I can now create an instance of the driver for both SPI flavor.

For the blocking case:

    let spi = SpiWrapper(Spi::new_blocking(p.SPI1, p.PA5, p.PA7, p.PA6, spi_config));
    let mut lr2021 = Lr2021::new(nreset, busy, spi, nss);    

And I can define a Lr2021Stm32 type like this:

pub type Lr2021Stm32 = Lr2021<Output<'static>,SpiWrapper, BusyAsync<ExtiInput<'static>>>;

For the async case:

    let spi = Spi::new(
        p.SPI1, p.PA5, p.PA7, p.PA6, p.DMA1_CH3, p.DMA1_CH2, spi_config,
    );
    let mut lr2021 = Lr2021::new(nreset, busy, spi, nss);    

And the Lr2021Stm32 type is now

pub type Lr2021Stm32 = Lr2021<Output<'static>, Spi<'static,Async>, BusyAsync<ExtiInput<'static>>>;

Board Init structure

For this project where I write multiple application for the same board with always the same setup, and needing the same kind of feature (Led control, user button), it is practical to have a common init structure to abstract all the pin mapping, device instance and common task.

The structure itself

Inside the struct, I need the driver instance, the uart, the interrupt pin and a trigger pin (used to start TX/RX with precise timing, independant of the SPI access).

    pub struct BoardNucleoL476Rg {
        pub lr2021: Lr2021Stm32,
        pub irq: ExtiInput<'static>,
        pub trigger_tx: Output<'static>,
        pub uart: Uart<'static, Async>
}

The fact that all those devices are now owned by the struct will not create problem with the borrow checker later because we can do some partial destructuring when needed to transfer ownership.

Leds and User button

The leds and button don’t need to be exposed directly, we can simply define some methods to change the led mode or get the button events.

static BUTTON_PRESS: WatchButtonPress = Watch::new();
static LED_RED_MODE: SignalLedMode = Signal::new();
static LED_GREEN_MODE: SignalLedMode = Signal::new();

impl BoardNucleoL476Rg {

    pub fn get_button_evt() -> ButtonRcvr {
        BUTTON_PRESS.receiver().unwrap()
    }

    pub fn led_red_set(mode: LedMode) {
        LED_RED_MODE.signal(mode)
    }

    pub fn led_green_set(mode: LedMode) {
        LED_GREEN_MODE.signal(mode)
    }
}

The init method

For the init method we can pass the embassy spawner as reference to start some task related to the leds and button.

    pub async fn init(spawner: &Spawner) -> BoardNucleoL476Rg {
        // Leds & buttons
        let led_red = Output::new(p.PC1, Level::High, Speed::Low);
        let led_green = Output::new(p.PC0, Level::High, Speed::Low);
        let button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);

        // Start the tasks
        spawner.spawn(blink(led_red, &LED_RED_MODE)).unwrap();
        spawner.spawn(blink(led_green, &LED_GREEN_MODE)).unwrap();
        spawner.spawn(user_intf(button, &BUTTON_PRESS)).unwrap();
        LED_RED_MODE.signal(LedMode::Off);
        LED_GREEN_MODE.signal(LedMode::Off);

        // Create instance of SPI, UART, ...
        // ...
        BoardNucleoL476Rg{lr2021, irq, uart, trigger_tx}
    }

I’ll just add here how to configure the Nucleo L476RG to use the highest clock speed (80MHz) because by default the board is at 4MHz and the only example I found where in C. It is not complicated, but there is no convenience method and you need to set the PLL settings manually.

        let mut config = embassy_stm32::Config::default();

        // Configure the system clock to run at 80MHz
        // STM32L476RG has a 16MHz HSI (High Speed Internal) oscillator
        // PLL formula: (HSI * PLLN) / (PLLM * PLLR) = (16MHz * 10) / (1 * 2) = 80MHz
        config.rcc.hsi = true;
        config.rcc.pll = Some(embassy_stm32::rcc::Pll {
            source: embassy_stm32::rcc::PllSource::HSI,     // Use HSI as PLL source
            prediv: embassy_stm32::rcc::PllPreDiv::DIV1,    // PLLM = 1
            mul: embassy_stm32::rcc::PllMul::MUL10,         // PLLN = 10
            divp: None,                                     // PLLP not used
            divq: None,                                     // PLLQ not used
            divr: Some(embassy_stm32::rcc::PllRDiv::DIV2),  // PLLR = 2
        });
        config.rcc.sys = embassy_stm32::rcc::Sysclk::PLL1_R;
        let p = embassy_stm32::init(config);

Usage

Now I can cleanup all my applications and remove more than 50 lines of initialization in each of them.

An application which controls leds based on button action can now looks like this:

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let board = BoardNucleoL476Rg::init(&spawner).await;

    let uart = board.uart;

    let mut button_press = BoardNucleoL476Rg::get_button_evt();
    let mut role = BoardRole::Rx;
    BoardNucleoL476Rg::led_green_set(LedMode::BlinkFast);

    let mut cnt : u8 = 0;

    loop {
        match button_press.changed().await {
            ButtonPressKind::Short => {
                role.toggle();
                let mode = if role == BoardRole::Rx {
                    LedMode::BlinkFast
                } else {
                    LedMode::BlinkSlow
                };
                BoardNucleoL476Rg::led_green_set(mode);
            }
            ButtonPressKind::Long  => BoardNucleoL476Rg::led_red_set(LedMode::Flash),
            ButtonPressKind::Double => {
                uart.write(&[cnt]).await;
                cnd = cnt.wrapping_add(1);
            }
        }
    }
}

Conclusion

This is overall quite trivial, but I feel this is a good approach where we could even go one step further and define a Board trait allowing to get access to the devices: this way the same application could be easily ported on various board with just setting a feature at compilation for example. If I get a different embedded development board this could be the subject of another blog post.