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:
- Install probe-rs: this tool will allow flashing the firmware and provide a debug link.
- Add the micro-controller as a rust target. In my case
rustup target add thumbv7em-none-eabi
- Clone the embassy repository
git clone https://github.com/embassy-rs/embassy.git
- Find an example matching you micro-controller in the examples directory. Run
cargo run --release --bin blinky
to run the example which just blink a LED
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 !