Getting Started with Rust on ESP32: no_std, esp-hal, and Embedded Rust Development
Every ESP32 tutorial on this site so far has used Arduino, ESP-IDF C, MicroPython, or CircuitPython — and for most projects, one of those four is still the right choice. But there's a fifth option that's matured fast over the last couple of years and is worth knowing about: Rust, running directly on the ESP32's Xtensa or RISC-V cores with no operating system underneath it (a "no_std" embedded target). Rust on ESP32 gives you memory safety guarantees the C ecosystem can't, a modern package manager and tooling story, and increasingly solid peripheral support through Espressif's own official HAL. This guide covers what embedded Rust on ESP32 actually looks like, how the toolchain differs from a normal Rust install, and whether it's worth the learning curve for your next project.
Why Rust for Embedded, and Why Now
Rust's headline feature is that its borrow checker eliminates whole classes of bugs — use-after-free, data races, buffer overruns — at compile time instead of letting them show up as a mysteriously crashing firmware three months into a deployed project. For embedded work specifically, where a memory bug can mean a device silently misbehaving in the field with no debugger attached, that guarantee is worth more than it is on a desktop application.
Espressif has invested directly in Rust support since 2021, funding a dedicated embedded Rust team and publishing official crates (esp-hal, esp-wifi, esp-idf-svc) rather than leaving it entirely to community reverse-engineering. That's a meaningfully different situation than most other MCU families, where Rust support is community-maintained and can lag hardware releases by a long time.
Two Different Approaches: no_std vs. std (ESP-IDF)
This is the first fork in the road and it matters a lot for what your code will look like.
no_std (esp-hal)std (esp-idf-hal / esp-idf-svc) Runs on top ofNothing — bare metal, or a lightweight async executor like EmbassyFreeRTOS, via ESP-IDF (same base as the C SDK) Binary sizeSmall, fast bootLarger, includes ESP-IDF's full runtime WiFi/BLE supportVia esp-wifi, actively developed but youngerMature, wraps the same ESP-IDF WiFi/BLE stack C developers use Standard libraryNo — no heap allocation by default without an allocator crate, no threads, no filesystemYes — full Rust std, threads, sockets, files Best forTight, deterministic firmware; sensor nodes; low-power designsAnyone porting existing ESP-IDF C project logic, or wanting familiar std ergonomicsFor a first project, std mode via esp-idf-hal is the gentler on-ramp, since it feels much closer to normal application Rust — you get Vec, String, threads, and a familiar println!. The no_std path is more work up front but produces smaller, more predictable firmware and is where most serious embedded Rust development on ESP32 is heading long-term, especially paired with the Embassy async framework.
Setting Up the Toolchain
ESP32's original chips use the Xtensa instruction set, which historically needed a custom LLVM fork (Espressif maintains esp-rs/rust-build for this). Newer ESP32 variants — the C3, C6, and H2 — use standard RISC-V cores, which means they work with upstream stable Rust and the normal rustup toolchain, no fork required. This is a genuinely important distinction when picking hardware for a Rust project.
- ESP32-C3 / C6 / H2 (RISC-V): install the riscv32imc-unknown-none-elf target via standard rustup, no custom toolchain needed — the simplest path into embedded Rust on this hardware.
- ESP32 / S2 / S3 (Xtensa): requires espup, Espressif's installer tool, which fetches the patched Xtensa-enabled Rust toolchain and sets up the right environment variables. Run espup install once, then source the generated export file in each new shell.
- Flashing: espflash is the standard tool, playing the same role esptool.py plays for C/Arduino projects — it flashes the compiled ELF and can also open a serial monitor.
- Project scaffolding: esp-generate or the older cargo-generate templates spin up a working no_std project with the right .cargo/config.toml target and linker settings already in place, which saves a lot of trial and error on your first project.
A Minimal Blinky in esp-hal
Here's roughly what a no_std GPIO blink loop looks like using the current esp-hal API — the API has changed release to release as the ecosystem matures, so treat this as illustrative rather than copy-paste stable across versions:
#![no_std] #![no_main] use esp_hal::{delay::Delay, gpio::{Io, Level, Output}, prelude::*}; #[entry] fn main() -> ! { let peripherals = esp_hal::init(esp_hal::Config::default()); let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); let mut led = Output::new(io.pins.gpio2, Level::Low); let delay = Delay::new(); loop { led.toggle(); delay.delay_millis(500); } }Compare that structure to Arduino's setup()/loop() pattern — the concepts map over directly, but peripheral access goes through Rust's ownership system: once you've taken a GPIO pin into an Output, the compiler prevents you from also using it elsewhere as an input, which is exactly the class of "two parts of the code disagree about a pin's mode" bug that's easy to introduce in C.
Async with Embassy
The other major reason to reach for Rust on ESP32 specifically is Embassy, an async embedded framework that lets you write concurrent firmware — a WiFi task, a sensor polling task, an LED animation task — as separate async fn tasks cooperatively scheduled without an RTOS. It's a genuinely different programming model from Arduino's single loop or FreeRTOS's preemptive tasks in ESP-IDF, and it maps well onto typical maker projects that are mostly waiting on I/O (a sensor read, a network response) rather than doing constant computation.
What You Give Up
- Library ecosystem: Arduino's library ecosystem is enormous — a display driver, a sensor breakout, a protocol implementation almost always already exists. Rust's embedded-hal-based driver ecosystem (the embedded-graphics, various sensor crates) is smaller and growing, but you will hit gaps where you're porting a driver yourself.
- Community troubleshooting: when something goes wrong with Arduino or ESP-IDF C, a forum search usually turns up someone with the exact same problem. Rust's embedded community is smaller, so expect more time reading source and datasheets directly.
- Compile times: Rust's compiler does much more work at build time than a typical C compile, which means slower iteration loops, especially noticeable on first builds and with LTO enabled for release binaries.
Is It Worth It?
For a quick sensor-to-MQTT project or a one-off weekend build, Arduino or MicroPython will get you there faster. Rust on ESP32 earns its keep on projects where reliability matters over months of unattended operation, where you're building something complex enough that memory bugs become a real risk (custom protocol parsers, anything juggling multiple concurrent I/O streams), or where you're already a Rust developer elsewhere and want one toolchain across your stack. It's not a wholesale replacement for the existing ESP32 workflows on this site, but it's a serious option now, not an experiment, and the RISC-V ESP32-C3/C6 boards in particular make a genuinely low-friction entry point since they need no custom toolchain fork at all.
Related Guides
- ATS Mini V4: Flashing Custom Firmware From Your Phone (ats-mini vs H.J. Berndt)
- H.J. Berndt Firmware for the ATS Mini V4: The In-Depth Guide
- Watchdog Timers for Arduino and ESP32: Hardware WDT, Task Watchdogs, and Recovering from Hangs
- How to Program Addressable LED Strips: WS2812B Patterns, Effects, and Power Design
- How to Install Klipper on Any 3D Printer: Complete Setup Guide
- I2C vs SPI vs UART: How to Choose and Use Serial Communication Protocols
- How to Use Sensors with Arduino and ESP32: Temperature, Distance, Load, Current, and Hall Effect
- How to Control Motors with Arduino and ESP32: Stepper, DC, and Servo Drivers