The Rusty Clams

Rust adventures !

Rust Embassy (P1) - Setup

05 Aug 2025

Introduction

I have been using Rust mainly to build tools, but the embedded ecosystem looks very interesting and powerful.

So I have decided to build a Rust application for the new Semtech LR2021 chip, a versatile chip dedicated to IoT supporting protocols such as LoRa, BLE, Zigbee and much more.

There are multiple approach to this: going bare-metal using directly the embedded-hal or using more high-level approach such as RTIC, Tock or Embassy, just to name a few. I have decided to start with embassy mainly because it looks very well documented and the async/await approach looked really nice.

And before we start, a short disclaimer: I am not a professional Rust developer nor an embedded firmware engineer, these posts are just a report of my experience trying to write some embedded software. If you find some error or have better solution compare to what I did, feel free to open issues or do some pull request on github.

Running the embassy example

The embassy book is absolutely fantastic to get started: it provides clear explanation on what to install and how to run the examples.

In my case the LR2021 chip is connected to a Nucleo board L476RG, and an example is available for this series of micro-controller, but looking at the list of supported chip, this is not a surprise.

Running the example was exceptionally smooth, just follow the book:

And that’s it !

Well not exactly: first try failed in the middle of the programming phase with the error An error with the flashing procedure has occured. I am used to better error message with the Rust compiler, but I also noticed that the --chip option was using a different version than my chip. This is controlled by an option in .cargo/config.toml.

Second try: fail again with first a warning WARN probe_rs::probe::stlink: send_jtag_command 242 failed: SwdApFault. Looking at the Cargo.toml file I notice a dependency on stm32l4r5zi: changing it to stm32l476rg and now there is no more errors !

Going back at the book I notice that all this is fully explained, of course: RTFM !!!

No error, but I still do not see my LED blinking: looking at the code it uses the pin PB14, but looking at the documentation, the led on my board is on pin PA5.

    let mut led = Output::new(p.PB14, Level::High, Speed::Low);

Changing to the correct pin and now it works \o/

Creating a repository

Now we have a nice environment to build and run firmware, so it is time to start our own repository. Once again the book has us covered with a section detailing how to create a new project from scratch.

The short version is: copy the Cargo.toml, the .cargo/config.toml, the build.rs and the example file. Add a [patch.crates-io] section in Cargo.toml and for each dependencies add the link to the embassy git and the revision that you are using when you cloned the embassy repository like embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "7703f47c1ecac029f603033b7977d9a2becef48c" }.

This worked like a charm, running again the cargo run --release works immediately.

Tweaking the example

It’s all cool and nice that this is working, but we have not done much Rust for the moment. Let’s first have a look at the example:

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    info!("Hello World!");

    let mut led = Output::new(p.PB14, Level::High, Speed::Low);

    loop {
        led.set_high();
        Timer::after_millis(300).await;
        led.set_low();
        Timer::after_millis(300).await;
    }
}

The init function allows to create a peripheral structure that we can then use: in this case configure a pin as Output to drive the LED. The code is really clean but it is a bit too simple to see the advantage of using async/await. So let’s add a simple feature: control the blinking speed with the user button, this will likely be useful when writing a true application.

First I create a static variable with Atomicu8 type to allow sharing cleanly a variable betwen two tasks:

static BLINK_MODE: AtomicU8 = AtomicU8::new(0);

Since we are in Rust and I love enums, I create a BlinkMode enum with a function giving a delay in ms and another to iterate through the different mode every time the button is pressed.

#[derive(Format)]
enum BlinkMode {
    Fast = 0, Medium = 1, Slow = 2
}

impl BlinkMode {
    /// Return the delay in ms associated with a blinking mode
    fn delay_ms(&self) -> u64 {
        match self {
            BlinkMode::Fast   => 100,
            BlinkMode::Medium => 500,
            BlinkMode::Slow   => 2500,
        }
    }

    /// Return the delay in ms associated with a blinking mode
    fn next(&mut self) {
        *self = match self {
            BlinkMode::Fast   => BlinkMode::Medium,
            BlinkMode::Medium => BlinkMode::Slow,
            BlinkMode::Slow   => BlinkMode::Fast,
        }
    }
}

The [derive(Format)] will allow to display which mode is selected in an info! macro call.

Now we can move the blinking led logic to a dedicated task, which read the BlinkMode value at the beginning of each loop:

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {

    loop {
        let mode : BlinkMode = BLINK_MODE.load(Ordering::Relaxed).into();
        let delay = mode.delay_ms();
        led.set_high();
        Timer::after_millis(delay).await;
        led.set_low();
        Timer::after_millis(delay).await;
    }
}

And finally we can update the main with a loop waiting on a button pressed:

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    info!("Hello from blinky_mode");

    // Spawn a blink task
    let led = Output::new(p.PA5, Level::High, Speed::Low);
    spawner.spawn(blink(led)).unwrap();

    // Get an interrupt on the button pin to wait
    let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);
    let mut mode = BlinkMode::Fast;

    loop {
        button.wait_for_low().await;
        mode.next();
        info!("Button pressed => {}s", mode);
        BLINK_MODE.store(mode.to_u8(), Ordering::Relaxed);
        button.wait_for_high().await;
    }
}

For the button I used ExtiInput to be able to use interrupt to wait on button change.

First impressions

Overall I am quite impressed by how smooth and clean this is going. I know this is a very basic firmware, but even simple things typically requires more complex setup in a typical C firmware environment. And also the code is a lot more readable than what I am used to.

Embassy and Rust embedded-hal are providing very nice abstractions to access hardware and the ecosystem seems already quite mature: providing this kind of abstraction over so many platform is really impressive.

Next step: using the SPI to access the LR2021 chip !