Rust on the ESP32: esp-idf-hal, esp-hal and embassy without drama

How to program the ESP32 in Rust: the two paths (esp-idf-hal with std and esp-hal no_std), the espup toolchain, examples and when it is worth it.

Also available in: EN, ES

Series Introduction to the ESP32 Part 14 View the series →
Rust on the ESP32

Rust on the ESP32 stopped being an experiment a while ago. Today the esp-rs ecosystem is stable and surprisingly pleasant, with one huge advantage: the compiler saves you from memory and concurrency bugs that cost you an afternoon of debugging in C.

The trade-off: you have to learn Rust and set up a separate toolchain.

Two paths, not one

PathWhat it isBest for
esp-idf-halRust on top of ESP-IDF, with stdReusing ESP-IDF, Wi-Fi, larger projects
esp-halno_std, without ESP-IDFTotal control, small binaries, async
embassyAsync framework on esp-halElegant concurrent tasks

The rule: if you want easy Wi-Fi and Espressif APIs, esp-idf-hal. If you want to squeeze everything and do not mind no_std, esp-hal + embassy.

Toolchain

The official installer is espup:

cargo install espup
espup install          # installs Xtensa and RISC-V targets
. $HOME/export-esp.sh  # or the script it generates

And to flash, cargo-espflash:

cargo install cargo-espflash
cargo espflash flash --monitor

Current HAL version: esp-hal 1.2.0.

use esp_idf_svc::hal::delay::FreeRtos;
use esp_idf_svc::hal::gpio::PinDriver;
use esp_idf_svc::hal::peripherals::Peripherals;

fn main() -> anyhow::Result<()> {
    esp_idf_svc::sys::link_patches();
    let peripherals = Peripherals::take()?;
    let mut led = PinDriver::output(peripherals.pins.gpio2)?;

    loop {
        led.set_high()?;
        FreeRtos::delay_ms(1000);
        led.set_low()?;
        FreeRtos::delay_ms(1000);
    }
}

Notice the detail: the compiler forces you to declare the pin as an output before writing. In C, a forgotten pinMode is a silent bug; here it does not compile.

Async with embassy

The best part of the no_std path is embassy: real asynchronous tasks, with no loop() and no hand-written state machines.

#[embassy_executor::task]
async fn blink(mut led: Output<'static, Gpio2>) {
    loop {
        led.toggle();
        Timer::after_millis(500).await;
    }
}

Two tasks “sleeping” at the same time without blocking each other: that is the modern way to write concurrent firmware.

Real advantages

  • Safety: no use-after-free, no data races, no null.
  • Honest concurrency: if it compiles, it usually does not stomp memory.
  • Small binaries in no_std.
  • cargo: dependencies, tests and docs in one place.

Downsides

  • Learning curve for Rust (borrows, async, no_std).
  • Younger ecosystem than Arduino: fewer ready-made libraries.
  • Separate toolchain: espup, targets, a specific Rust version.
  • Fewer examples and tutorials than in C/C++.

When to choose it?

Yes:

  • You care about reliability and want fewer weird bugs.
  • You already know Rust (or want to learn it properly).
  • You need real async or very small binaries.
  • It is a long-term project where maintenance matters.

No:

  • It is your first contact with microcontrollers: start with Arduino or MicroPython.
  • You need a specific library that only exists in C++.
  • You are in a hurry.

Summary

  • Two paths: esp-idf-hal (with std, on ESP-IDF) and esp-hal (no_std), plus embassy for async.
  • Toolchain with espup and flashing with cargo-espflash.
  • The compiler prevents errors that are silent in C.
  • esp-hal is on 1.2.0 and the ecosystem is now mature.
  • It is worth it if you care about maintenance and safety.

Next step: a change of scene with the IDEs: which one to use and why. Or go back to the series hub.

Related posts

↑↓ navigate ↵ open Esc close