✍️ 필사 모드: Embedded Rust 2026 Deep Dive — no_std, Embassy, esp-rs, RP2350, probe-rs, and Rust on Microcontrollers in Practice
EnglishPrologue — The "Is Embedded Rust Usable Yet?" Question is Settled
Five years ago this post would have opened with "embedded Rust is still experimental, but…" Embassy in 2021 was alpha, the RP2040 HAL hadn't settled, ESP32 needed a forked LLVM, and probe-rs was a niche companion to OpenOCD.
The landscape in May 2026 is different.
- Embassy 0.6 is the de facto default async runtime on microcontrollers. New projects rarely default to the main-loop-plus-interrupt-handlers pattern anymore.
- RP2350 (Raspberry Pi Pico 2, dual Cortex-M33 plus dual RISC-V Hazard3) has stable HAL coverage under
rp235x-hal/embassy-rp. esp-rshas been elevated to Espressif's official top-tier SDK. Xtensa (ESP32, ESP32-S3) still needs a forked LLVM, but the RISC-V line (ESP32-C3, C6, H2, P4) builds with upstream Rust.probe-rshas effectively replaced OpenOCD. Onecargo runand you get flash plus RTT logs plus backtraces in a single terminal.defmtlogging is faster than printf, and the formatter runs on the host — the firmware binary does not carry the format strings.- Non-async embedded Rust is still alive. Skipping Embassy is not a sin. It just isn't what most new projects pick.
In short, embedded Rust is production-ready in 2026. Apollo3, Arduino R4, conference badges, home automation, art installations — all real domains shipping real code. This post walks from the basics of no_std all the way to your first Embassy blinky on an RP2040, in one pass.
1. The Landscape — Why Rust is Reaching the MCU Now
1.1 Five Pressures on Embedded Firmware
Anyone writing microcontroller firmware deals with five pressures simultaneously.
- Memory — environments like 32 KB RAM, 256 KB flash, no room for the standard library.
- Determinism — you do not allocate inside an ISR. It wrecks real-time guarantees.
- Hardware abstraction — you want to treat GPIO, UART, SPI, I2C, and timers without chip-specific code.
- Concurrency — sensor polling, UART RX, log transmission, display refresh, all running together with no OS.
- Debugging — JTAG/SWD, RTT, semihosting. Printing is hard. Endless blink-the-LED debugging.
1.2 Why C/C++ Was the Answer for 60 Years
C/C++ actually has a decent answer to all five. static variables plus interrupt handlers plus either an RTOS or a superloop, plus vendor SDKs (ST's HAL, Espressif's ESP-IDF, Nordic's nRF Connect SDK). The whole industry has run on this for six decades.
Two problems remain.
- Memory safety — null pointers, bad casts, buffer overruns. One bad line of firmware turns into infinite LED-blink debugging.
- Impoverished concurrency model — RTOS tasks plus mutexes plus queues are standard, but priority inversion, deadlocks, and priority inheritance traps never end.
1.3 How Rust Answers
Rust has a language-level answer to each of these pressures.
| Pressure | Rust's answer |
|---|---|
| Memory | #![no_std] — build without the standard library, use core and alloc only |
| Determinism | Ownership and borrowing express alloc-free state machines naturally |
| Hardware abstraction | The embedded-hal trait ecosystem |
| Concurrency | async/await plus the Embassy runtime (or RTIC) |
| Debugging | defmt plus probe-rs — RTT/SWD setup in one line |
What is decisively different from five years ago is that each answer now has working code and real chip support. The rest of this post unpacks those five answers one by one.
2. The Essence of no_std — How Far Does core Alone Get You?
2.1 Environments Where std Cannot Follow
On desktop and server, Rust's std runs on top of an OS. std::fs is the filesystem, std::net is the TCP stack, std::thread is OS threads, std::sync::Mutex is a pthread mutex. None of that exists on a microcontroller.
The #![no_std] attribute tells the compiler "this crate does not use std." Instead, it uses core, a smaller standard library.
| Area | In std | In core |
|---|---|---|
Primitive types (u8, f32, …) | yes | yes |
Option, Result | yes | yes |
Iterator trait | yes | yes |
Vec, String, HashMap | yes | no (separate alloc) |
println! | yes | no |
std::fs, std::net | yes | no |
Box, Rc, Arc | yes | no (separate alloc) |
| Threads, sync primitives | yes | no |
Even with only core you keep the richness of slices, iterators, traits, and macros. Only types that need dynamic allocation drop off.
2.2 The Decision to Pull in alloc
If your microcontroller has enough RAM — RP2040 has 264 KB SRAM, ESP32-S3 has 512 KB — you can add the alloc crate and bring Box, Vec, and String back. You must designate a global allocator.
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
use embedded_alloc::Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
#[entry]
fn main() -> ! {
// Reserve an 8 KB heap statically.
{
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 8 * 1024;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
let mut v: Vec<u32> = Vec::new();
v.push(42);
loop {}
}
The community-wide recommendation is "never allocate inside an ISR." Allocation in the main loop is fine; interrupts only touch pre-allocated buffers.
2.3 panic_handler Is Mandatory
A no_std binary must specify what to do on panic. In std you get automatic abort plus backtrace, but no such mechanism exists on a microcontroller.
use panic_halt as _; // panic -> infinite loop
// or
use panic_probe as _; // panic -> report via RTT, then halt (with probe-rs)
// or
use panic_reset as _; // panic -> watchdog reset
Production firmware usually picks panic_reset plus a "save last panic info to non-volatile memory and report after reboot" pattern.
2.4 no_main and the Entry Point
A no_std crate is usually no_main as well. Instead of the standard fn main entry, the chip-specific boot sequence has to take control first.
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use panic_halt as _;
#[entry]
fn main() -> ! {
// `!` return means it never returns. Infinite loop or `wfi`.
loop {}
}
The cortex_m_rt::entry macro creates the reset handler and plugs in the right boot sequence (stack pointer, zeroing BSS, copying initialized RAM data). The RP2040 has rp2040-hal, the ESP32 has esp-hal, each doing the equivalent.
3. PAC, HAL, BSP — Three Layers of Abstraction
The embedded Rust ecosystem is made of three layers of crates. Higher layers are more abstract; lower layers are more chip-specific.
3.1 PAC (Peripheral Access Crate)
The lowest layer. Auto-generated from the chip vendor's SVD (System View Description) XML by svd2rust. It exposes register bit-fields as types.
// Using rp2040-pac
use rp2040_pac as pac;
let peripherals = pac::Peripherals::take().unwrap();
let sio = peripherals.SIO;
// Set GPIO25 as output and drive it high.
sio.gpio_oe_set.write(|w| unsafe { w.bits(1 << 25) });
sio.gpio_out_set.write(|w| unsafe { w.bits(1 << 25) });
PAC-only code reads like the datasheet itself. Fast but unreadable and not portable across chips.
3.2 HAL (Hardware Abstraction Layer)
A safe abstraction on top of the PAC. rp2040-hal, embassy-rp, stm32f4xx-hal, esp-hal, nrf52840-hal, and so on.
// Using rp2040-hal
use rp2040_hal::gpio::Pins;
use embedded_hal::digital::OutputPin;
let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
let mut led = pins.gpio25.into_push_pull_output();
led.set_high().unwrap();
Thanks to the OutputPin trait, the same code keeps working when the chip changes.
3.3 embedded-hal — The Abstraction Contract
The reason every HAL exposes the same shape of API is the embedded-hal trait crate. As of 2026, embedded-hal 1.0 is stable and embedded-hal-async 1.0 lands alongside it.
// Core traits from embedded-hal 1.0
pub trait OutputPin {
type Error;
fn set_low(&mut self) -> Result<(), Self::Error>;
fn set_high(&mut self) -> Result<(), Self::Error>;
}
pub trait SpiDevice<Word = u8>: ErrorType {
fn transaction(&mut self, operations: &mut [Operation<'_, Word>]) -> Result<(), Self::Error>;
}
pub trait I2c<A = SevenBitAddress, Word = u8>: ErrorType {
fn transaction(&mut self, address: A, operations: &mut [Operation<'_, Word>]) -> Result<(), Self::Error>;
}
Sensor driver crates (such as bme280, mpu6050, ssd1306, embedded-graphics) are written generically over these traits. A single BME280 driver works on RP2040, STM32, ESP32, and nRF52.
3.4 BSP (Board Support Package)
A crate that pins down the pin mapping, power, and LED macros for a specific board (Raspberry Pi Pico, Adafruit Feather, STM32 Nucleo, etc.). Examples: rp-pico, feather_rp2040, stm32f4-discovery.
// Using rp-pico BSP — the LED pin already has a name.
use rp_pico::hal::Clock;
use rp_pico::hal::pac;
use rp_pico::hal::gpio::Pins;
let pac = pac::Peripherals::take().unwrap();
let sio = rp_pico::hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
let mut led = pins.led.into_push_pull_output();
led.set_high().unwrap();
The naming pins.led matters — the BSP knows the Pico wires its LED to GPIO25.
4. Embassy — Async by Default in Embedded Rust
4.1 Why Async on a Microcontroller?
Concurrency on a microcontroller traditionally took one of three forms.
- Superloop — write
loop { check_a(); check_b(); check_c(); }. Simple, but one long task starves the others. - Interrupt handler plus main loop — ISR starts work, main loop processes results. Shared state is tricky.
- RTOS tasks — FreeRTOS, ChibiOS, Zephyr. Context-switch cost, N times the stack memory, learning curve for primitives.
Embassy is a fourth answer. Cooperative concurrency expressed in async/await, with a runtime that wakes on top of interrupts.
4.2 The Core Idea of Embassy
// Two tasks running concurrently. Not threads — async tasks.
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
loop {
led.set_high();
Timer::after_millis(500).await; // Other tasks run during these 500 ms.
led.set_low();
Timer::after_millis(500).await;
}
}
#[embassy_executor::task]
async fn echo(mut uart: BufferedUart<'static, UART0>) {
let mut buf = [0u8; 64];
loop {
let n = uart.read(&mut buf).await.unwrap();
uart.write_all(&buf[..n]).await.unwrap();
}
}
The interesting part is Timer::after_millis(500).await. At an await point, the task yields and another task can run. When the timer interrupt fires 500 ms later, the Embassy runtime wakes the task back up.
uart.read(&mut buf).await works the same way — the task sleeps until a UART RX interrupt arrives. In the meantime the runtime runs other tasks, and once idle it sleeps the core with wfi (Wait For Interrupt).
4.3 Stack Memory — The Decisive Difference vs RTOS
RTOS tasks each have their own stack. Five tasks at 4 KB each gives you 20 KB. That hurts on small microcontrollers.
Embassy's async tasks share a single stack. The state of each task is compressed into a compiler-generated state-machine struct. As a result, ten tasks frequently fit in 1-2 KB of stack.
RTOS: [Stack T1: 4KB] [Stack T2: 4KB] [Stack T3: 4KB] [Stack T4: 4KB]
Embassy: [Shared Stack: 4KB] + [Task1 state: 64B] [Task2 state: 96B] [Task3 state: 32B]
4.4 Spawner and main
Standard Embassy main-function boilerplate.
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let led = Output::new(p.PIN_25, Level::Low);
spawner.spawn(blink(led)).unwrap();
}
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
loop {
led.toggle();
Timer::after_millis(500).await;
}
}
The embassy_executor::main macro makes main an async function and injects a Spawner argument. Tasks register through spawner.spawn(...).
4.5 RTIC vs Embassy — The Two Main Choices
RTIC (Real-Time Interrupt-driven Concurrency) is another embedded concurrency framework that predates Embassy. It schedules priority-based interrupt-driven tasks, with resource locking statically verified at compile time.
| Aspect | Embassy | RTIC |
|---|---|---|
| Model | async/await plus executor | Priority-based interrupt scheduling |
| Learning curve | If you know async, immediate | Macros and resource model to learn |
| Priority guarantees | Cooperative — all tasks equal | Priority-based, compile-time verified |
| Memory | Smaller (shared stack) | Per-priority stack |
| Popularity | The default for new projects, 2024-2026 | When safety-critical or hard real-time guarantees are required |
Pick Embassy first for new projects. Pick RTIC for hard real-time (automotive, avionics) where priority verification is mandatory.
5. Per-Target Status as of May 2026
5.1 RP2040 / RP2350 — Raspberry Pi Pico Family
- RP2040 — Dual Cortex-M0+, 264 KB SRAM.
embassy-rpis stable. Covers Pico 1 and Pico W. - RP2350 — Dual Cortex-M33 plus dual RISC-V Hazard3 (choose at boot), 520 KB SRAM, secure boot, ARM TrustZone.
rp235x-hal1.0 andembassy-rprp235x support are stable. - Bluetooth and Wi-Fi (Pico W, Pico 2 W) —
cyw43crate plusembassy-net(smoltcp-based TCP/IP). Even BLE works over PIO plus SPI. - PIO — the signature feature of the RP series. A lightweight stateful I/O machine. Write PIO assembly in Rust with
pio-procplusembassy-rp::pio.
// Snippet: Wi-Fi on Pico W (abridged)
use cyw43_pio::PioSpi;
use embassy_net::Stack;
let mut control = cyw43::new(state, pwr, spi, fw).await;
control.init(clm).await;
control.join_wpa2("SSID", "PASSWORD").await.unwrap();
5.2 ESP32 (esp-rs)
Espressif elevated Rust to its official top-tier SDK in 2023. As of 2026, esp-hal 1.0 is stable.
- RISC-V chips (ESP32-C3, C6, H2, P4) — built with upstream Rust toolchain. No
espupneeded. - Xtensa chips (ESP32, ESP32-S2, ESP32-S3) — still need a forked LLVM. Auto-configured by
espup install. embassy-executorplusesp-hal-embassy— run Embassy on ESP32 as-is.- Wi-Fi / BLE —
esp-wificrate. Wi-Fi 6 and BLE 5.3. Integrates withembassy-net.
# Cargo.toml — for ESP32-C6
[dependencies]
esp-hal = { version = "1.0", features = ["esp32c6"] }
esp-hal-embassy = { version = "0.6", features = ["esp32c6"] }
embassy-executor = { version = "0.6", features = ["task-arena-size-20480"] }
esp-wifi = { version = "0.10", features = ["esp32c6","wifi","ble"] }
The ESP32-C6 — RISC-V plus Wi-Fi 6 plus a Thread/Zigbee radio — is the hot spot of embedded Rust in 2026. It is the easiest path to a Matter device written in Rust.
5.3 STM32 (stm32-rs)
STMicroelectronics's STM32 family. The default chipset of the embedded industry.
stm32-rs— a joint project that auto-generates PACs for every STM32 chip from SVD.stm32f4xx-hal,stm32h7xx-hal,stm32l4xx-hal— per-family HALs.embassy-stm32— the unified Embassy HAL. Covers every family, pick a chip withfeatures = ["stm32f407vg"]. The default for new STM32 projects in 2026.
The compelling reason to pick Embassy on STM32 is that the interrupt-to-async-waker plumbing exists for nearly every peripheral. UART, SPI, I2C, ADC, DMA, USB — all async.
5.4 nRF52 / nRF53 / nRF54 — Nordic Semiconductor
The de facto standard for BLE devices.
embassy-nrf— stable for nRF52832, 52840, 5340, and 54L.nrf-softdevice— wrapper that calls Nordic's SoftDevice (BLE stack) from Rust async. Both peripheral and central role.- 2026 update —
trouble(pure Rust BLE host) has matured into a SoftDevice-free alternative. Runs on nRF52, ESP32, and CYW43.
5.5 Others — Briefly
- Ambiq Apollo3 / 4 —
ambiq-hal. Ultra-low-power (uA) MCUs. Popular for wearables and BLE. - Arduino UNO R4 (Renesas RA4M1) —
ra4m1-hal(community). Cortex-M4. Arduino-compatible pinout. - CH32V — WCH's ultra-cheap RISC-V.
ch32-hal. Price tag around 20 cents. - SAMD (Microchip / Atmel) —
atsamd-hal. Covers SAMD21 (Adafruit Feather M0) and SAMD51. - Generic RISC-V —
riscv-rtfor boot, chip-specific HAL separately.
6. Tooling — probe-rs, cargo-embed, defmt
6.1 probe-rs — The Successor to OpenOCD
The era where OpenOCD plus GDB was the standard for embedded debugging is over. probe-rs has effectively replaced it.
probe-rs is a Rust-written driver for debug probes (J-Link, ST-Link, CMSIS-DAP, Raspberry Pi Debug Probe, FTDI, etc.), plus flash algorithms, plus the RTT/SWO host.
# Install
cargo install probe-rs-tools
# List connections
probe-rs list
# Flash, run, and stream RTT logs in one command.
cargo run --release # with runner = "probe-rs run" set in .cargo/config.toml
6.2 Standard .cargo/config.toml
A standard setup for an RP2040 project.
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip RP2040 --protocol swd"
[build]
target = "thumbv6m-none-eabi" # RP2040 (Cortex-M0+)
# target = "thumbv8m.main-none-eabihf" # RP2350 (Cortex-M33)
[env]
DEFMT_LOG = "trace"
Type cargo run --release and you get build, flash, run, RTT logs, and backtraces in one command.
6.3 defmt — Logging Faster Than printf
defmt is a macro like println!, but with a key trick — the format string never lives in the firmware binary, only an index does. The host (probe-rs) reads the .defmt section of the ELF to reconstruct strings.
use defmt::{info, warn, error};
fn read_sensor(i: u32) {
info!("sensor read iteration={}", i);
let value = sensor.read();
if value > 100 {
warn!("sensor value high: {=u32}", value);
} else if value == 0 {
error!("sensor not responding");
}
}
Type hints like {=u32} exist for serialization efficiency. A single log typically serializes to 4-8 bytes. A 1 MHz UART can sustain 100k logs per second.
| Comparison | printf (semihosting) | RTT printf | defmt over RTT |
|---|---|---|---|
| Per-log cost | Tens of µs (blocking) | 1-2 µs | 100-500 ns |
| Firmware size | Format strings included | Included | Index only |
| Compile-time format check | no | no | yes |
6.4 cargo-embed — An Alternative Runner
Besides probe-rs run there is cargo-embed. It provides a TUI with both RTT logs and a GDB debugger in one screen.
cargo install cargo-embed
cargo embed --release
The 2025-2026 trend favors probe-rs run since it is lighter and is now the default for new projects. Use cargo-embed when you want the dashboard.
6.5 GDB Is Still Alive
For situations where GDB is stronger (intricate memory corruption, for example), launch probe-rs gdb as a server and attach with arm-none-eabi-gdb.
probe-rs gdb --chip RP2040
# In another terminal
arm-none-eabi-gdb target/thumbv6m-none-eabi/release/myfirmware
(gdb) target remote :1337
7. Your First Blinky with Embassy — RP2040 Walkthrough
Time for real code. The onboard LED of a Raspberry Pi Pico (GPIO25) blinking every 0.5 seconds.
7.1 Create the Project
cargo new --bin blinky
cd blinky
rustup target add thumbv6m-none-eabi
7.2 Cargo.toml
[package]
name = "blinky"
version = "0.1.0"
edition = "2021"
[dependencies]
embassy-executor = { version = "0.6", features = ["arch-cortex-m","executor-thread","integrated-timers"] }
embassy-time = { version = "0.4" }
embassy-rp = { version = "0.4", features = ["rp2040","time-driver"] }
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
cortex-m-rt = "0.7"
[profile.release]
debug = 2 # keep debug symbols for defmt
lto = true
codegen-units = 1
opt-level = "s"
7.3 .cargo/config.toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip RP2040 --protocol swd"
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
]
[build]
target = "thumbv6m-none-eabi"
[env]
DEFMT_LOG = "trace"
7.4 src/main.rs — First Blinky
#![no_std]
#![no_main]
use defmt::info;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let mut led = Output::new(p.PIN_25, Level::Low);
info!("blinky started");
loop {
led.set_high();
Timer::after_millis(500).await;
led.set_low();
Timer::after_millis(500).await;
}
}
7.5 Build and Flash
Hold BOOTSEL on the Pico while plugging in USB and the board enumerates as a mass storage device. If you also have a probe-rs-compatible debug probe attached (or a Pico Probe firmware on a second Pico), one command does the rest.
cargo run --release
# Compiling blinky ...
# Finished release [optimized + debuginfo] target(s)
# Running `probe-rs run --chip RP2040 --protocol swd target/thumbv6m-none-eabi/release/blinky`
# 0.000000 INFO blinky started
The LED blinks every second. A single log line streams to the host terminal over RTT.
7.6 One Step Up — UART Echo Plus Two Concurrent Tasks
#![no_std]
#![no_main]
use defmt::info;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_rp::peripherals::{PIN_25, UART0};
use embassy_rp::uart::{Async, BufferedUart, Config as UartConfig, InterruptHandler};
use embassy_rp::{bind_interrupts, uart};
use embassy_time::Timer;
use embedded_io_async::{Read, Write};
use {defmt_rtt as _, panic_probe as _};
bind_interrupts!(struct Irqs {
UART0_IRQ => InterruptHandler<UART0>;
});
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// LED task
let led = Output::new(p.PIN_25, Level::Low);
spawner.spawn(blink(led)).unwrap();
// UART echo task — GPIO0 TX, GPIO1 RX, 115200 baud
let mut cfg = UartConfig::default();
cfg.baudrate = 115_200;
let uart = BufferedUart::new(
p.UART0, Irqs, p.PIN_0, p.PIN_1,
cfg, &mut [0u8; 64], &mut [0u8; 64],
);
spawner.spawn(echo(uart)).unwrap();
}
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
loop {
led.toggle();
Timer::after_millis(500).await;
}
}
#[embassy_executor::task]
async fn echo(mut uart: BufferedUart<'static, UART0>) {
let mut buf = [0u8; 32];
info!("echo task ready");
loop {
match uart.read(&mut buf).await {
Ok(n) => {
info!("rx {} bytes", n);
let _ = uart.write_all(&buf[..n]).await;
}
Err(_) => Timer::after_millis(10).await,
}
}
}
The point — blink and echo both stay alive. While one is asleep on Timer::after_millis(500).await, the other can wake on a UART RX interrupt. One core, one stack, two tasks.
7.7 One More defmt Line
info!("rx {} bytes, first={=u8:#04x}", n, buf[0]);
Host terminal output:
0.001234 INFO blinky started
0.001500 INFO echo task ready
0.012345 INFO rx 5 bytes, first=0x48
The 0x48 is ASCII 'H'. That was the entire first embedded Rust project.
8. Real Projects — What People Are Building
8.1 Home Automation / Matter Devices
ESP32-C6 plus esp-hal plus the rs-matter crate. A Rust implementation of Matter (the IoT standard over Thread or Wi-Fi). Since 2024, rs-matter has reached v0.5, and you can write the full path — BLE commissioning, Wi-Fi or Thread, exposing Matter clusters — in Rust.
[BLE commissioning] -> [Wi-Fi/Thread] -> [Matter cluster: OnOff, LevelControl, ...]
embassy-net rs-matter
Apple Home, Google Home, and Amazon Alexa all see the same device.
8.2 Conference Badges
Hackaday Supercon, Defcon, EMF Camp, and other conference badges have been shifting toward Rust. The EMF Camp 2024 badge — RP2040 plus ePaper plus LoRa — was Embassy-based from day one.
The reason: refreshing ePaper, RX on the radio, user input, and battery monitoring all concurrently — async expresses this much more cleanly than the alternatives.
8.3 Interactive Art Installations
Media-art work that ties together thousands of LED matrices, sensor arrays, and Wi-Fi/Art-Net. Rust firmware is becoming standard at Burning Man, teamLab-style projects in Japan, and European festivals. The decisive selling point is escaping the C boilerplate and the memory-corruption debugging.
8.4 Industrial IoT — Vibration / Temperature Monitors
STM32H7 plus LoRa plus a MEMS vibration sensor — FFT the vibration data, transmit wirelessly when a threshold is exceeded. On chips like the STM32H7, you can call DSP instructions through wrappers like cortex-m::asm::sev, or use cmsis-dsp-sys bindings to CMSIS-DSP.
8.5 Non-Volatile Key/Value Storage (sequential-storage)
Persist key/value data in flash so the firmware can keep config and calibration across reboots. Built on the embedded-storage trait, with sequential-storage providing wear leveling. The last 4 KB sector of an RP2040, the EEPROM region of an STM32, and the NVS partition of an ESP32 are all driven by the same API.
9. Compared to C/C++, MicroPython, Zig Embedded, and Ada/SPARK
9.1 C/C++ — Still the Industry Standard
| Aspect | C/C++ | Rust |
|---|---|---|
| Compiler | Available on every chip | Only chips with upstream LLVM support (almost all) |
| Memory safety | You guarantee it | The compiler verifies it |
| Vendor SDK | First-class (ST HAL, ESP-IDF, nRF Connect) | Catching up over 2-3 years (esp-rs is first-class) |
| Concurrency | RTOS plus interrupts | Embassy / RTIC |
| Debugging | OpenOCD plus GDB | probe-rs plus defmt |
| Talent pool | Massive | Growing fast |
| Auto / aerospace certification | MISRA C standard | Ferrocene (Ferrous Systems's certified Rust compiler) |
In short — pick C/C++ if legacy chips, talent pool, or a certified compiler are decisive. Pick Rust if a new chip, a new team, and the value of memory safety plus a concurrency model matter more.
9.2 MicroPython — Entry Barrier and RAM
MicroPython runs an interpreter on ESP32, RP2040, and STM32. The entry barrier is unbeatably low — first LED blink in five minutes.
| Aspect | MicroPython | Rust |
|---|---|---|
| Entry barrier | Very low | Medium to high |
| Execution speed | Interpreted (1/100 to 1/50 of C) | At C performance |
| RAM use | Interpreter and GC overhead, ~64 KB minimum | Near zero overhead |
| Compile-time checks | Almost none | Strong |
| Best fit | Prototyping, education, light logic | Production, real-time, tight resources |
MicroPython will not die — it is too strong in education and prototyping. But to turn a prototype into a product, the rewrite is usually into Rust or C/C++.
9.3 Zig Embedded
Zig treats embedded as a first-class citizen from day one. comptime is more powerful than macros, the memory model is explicit, and the C ABI interop is natural.
But as of May 2026, the ecosystem depth is far behind Rust. There is no unified async runtime like Embassy, no static verification like RTIC, no library of more than a hundred sensor driver crates. Zig is appealing on small firmware; for big projects Rust is ahead.
9.4 Ada / SPARK
The standard for decades in automotive, aerospace, and nuclear. Static verification through the SPARK subset is powerful.
Versus Rust — memory safety, both. Concurrency safety, Rust ahead. Industry certification depth, Ada/SPARK ahead. Talent pool growth, Rust faster. With Ferrocene certifying the Rust compiler to ISO 26262 / IEC 61508 (ASIL D in 2023), Rust is moving into safety-critical territory too.
10. The Hard Parts — Honest Traps
10.1 The Pressure of the 'static Lifetime
Embassy tasks must be 'static — that is, you cannot pass a stack-borrowed reference to a task. The common pattern is to initialize once in static memory via the static_cell crate.
use static_cell::StaticCell;
static UART_BUF: StaticCell<[u8; 256]> = StaticCell::new();
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let buf: &'static mut [u8; 256] = UART_BUF.init([0; 256]);
// Now buf is 'static and can be passed to spawn.
}
The pattern feels awkward for the first month. By month two it is natural.
10.2 The Interrupt Binding Macro
The macro that wires peripheral interrupt handlers into the Embassy runtime, bind_interrupts!, has an odd-looking shape that differs slightly per chip.
bind_interrupts!(struct Irqs {
UART0_IRQ => embassy_rp::uart::InterruptHandler<UART0>;
USBCTRL_IRQ => embassy_rp::usb::InterruptHandler<USB>;
});
At first this looks "why does it look like this," but unpacked it is just the safe registration "when the USB interrupt fires, call Embassy's USB handler."
10.3 Xtensa Rust Still Needs a Forked LLVM
The original ESP32 and ESP32-S2/S3 are Xtensa cores. Xtensa is not upstream LLVM; Espressif maintains a fork. espup install handles the setup, but CI like GitHub Actions has one extra step.
The RISC-V chips (C3, C6, H2, P4) build with upstream Rust and do not have this issue. Prefer the RISC-V line for new ESP projects is the conventional wisdom in 2026.
10.4 Debug Build Size
cargo build (debug) has no LTO and opt-level 0, so the code can balloon 4 to 10 times and may not fit in flash. Embedded Rust is almost always written against cargo build --release. Keep debug symbols on with debug = 2 in release so that defmt and backtraces still work.
10.5 PACs for Unknown Chips
The ecosystem is active but does not cover everything. SiPEED MAIX (Kendryte K210), Bouffalo BL602, and similar boards may have a PAC but only a partial HAL. Outside the major lines (RP2xxx, ESP32, STM32, nRF5x, SAMD) you may have to write a HAL on top of the PAC yourself.
11. A Learning Path — Where to Start
11.1 Week Zero — Buy a Pico
Fastest start: Raspberry Pi Pico 2 W (~12) plus two USB-C cables. Around $25 all in. Buying the debug probe separately is the key — it is what unlocks flash plus RTT debugging from one command.
For the ESP32 line, ESP32-C6 DevKit (~$15) is a great choice. C6 has Wi-Fi 6 plus BLE 5 plus Thread.
11.2 Weeks 1-2 — embedded-hal Traits
embedded-hal::digital::OutputPin, embedded-hal::spi::SpiDevice, embedded-hal::i2c::I2c, embedded-hal-async::*. The first one to two weeks should focus on the shape of these traits. After that, the BME280 driver, the SSD1306 OLED driver, and the MPU6050 IMU driver all look the same.
11.3 Weeks 3-4 — Embassy's Async
Embassy's official examples folder is the fastest learning material. Open examples/rp, examples/esp32c6, or examples/stm32f4 — whichever matches your chip — and read end to end. Each example is 100-200 lines, short, with repeated patterns.
11.4 Months 1-2 — First Project
One small meaningful project. Recommended — ePaper clock plus Wi-Fi NTP sync, BME280 environment sensor over BLE or Wi-Fi, or rotary encoder plus OLED menu UI. Projects between 100 and 500 lines yield the best learning efficiency.
11.5 Months 3-6 — Depth
At this stage pick one direction for depth.
- Matter device — using
rs-matter. Actually register with Apple or Google Home. - Own PCB plus own firmware — KiCad for the board, Rust for the firmware.
- Low power — uA-level sleep plus interrupt wake-up plus battery life math.
- DMA plus high-speed peripherals — SPI DMA pushing close to 1 Gbps.
- PIO (Pico) or RMT (ESP32) — custom protocols (WS2812, IR, 1-wire).
Epilogue — Rust Made Embedded More Honest
The one-line summary of embedded Rust — the compiler asks "whose memory is this?" with you. Code where an interrupt and the main loop touch the same buffer fails to compile. An async task that borrowed a peripheral keeps another task from using it concurrently. Static RAM is verified to be initialized in exactly one place.
This sounds small, but if you have ever spent half your firmware-debugging time on "why won't the LED blink" only to discover it was memory corruption, it is decisive.
C/C++ are not going away. Sixty years of code, talent, vendor SDKs, and automotive certification live there. What is changing is that Rust is becoming the default on new chips. And once it becomes the default — the next generation of developers will not go back to C.
Embassy's async was an "interesting experiment" five years ago. In 2026 it is "why would you write it any other way?" RTIC, superloops, and RTOSes are still alive — but the default choice for new projects is Embassy.
probe-rs and defmt have dragged embedded debugging from the 1990s into the 2020s. Once you taste the workflow of one cargo run --release doing build, flash, and logging, you cannot go back to OpenOCD.
The big picture — embedded Rust in 2026 has passed the stage of "is it usable?" and reached "why aren't you using it?" The next five years will be Ferrocene penetrating certification-heavy areas like automotive, medical, and aerospace.
14-Item Checklist
- Are
#![no_std]and#![no_main]enabled? - Is a
panic_handlerdeclared (panic_halt/panic_probe/panic_reset)? - Are debug symbols kept in
Cargo.toml's[profile.release]withdebug = 2? - Does
.cargo/config.toml'srunneruseprobe-rs run --chip ...? - Are you using HAL/BSP first instead of code that touches the PAC directly?
- Are sensor and display drivers used generically against
embedded-haltraits? - When an Embassy task needs
'static, do you handle it withstatic_cell/StaticCell? - Are you avoiding dynamic allocation inside interrupt handlers?
- Are
defmt::info!,warn, anderrorlevels filtered appropriately for production? - If using Wi-Fi or BLE, have you decided between
embassy-net,trouble, oresp-wifi? - For non-volatile storage, are you abstracting via
sequential-storage/embedded-storage? - Where possible, have you preferred RISC-V (C3, C6, H2) over Xtensa (ESP32, S2, S3) to reduce toolchain burden?
- Does CI run
cargo build --releasepluscargo clippy --target ...? - Do you have a separate debug probe (Pico Probe / J-Link / ST-Link)? It is the heart of RTT debugging.
Ten Anti-Patterns
- Driving GPIO with bit-shifts on the PAC only — loses both readability and portability.
- Calling a large synchronous blocking function from inside an Embassy task — starves other tasks.
- Dynamic allocation like
Vec::pushinside an ISR — wrecks real-time guarantees. - Shipping production firmware with
panic_halt— should reset via watchdog instead. - Flashing the
cargo build(debug) firmware as-is — both size and speed suffer. - Spraying
defmt::info!calls without an RTT host attached — the RTT buffer can fill up and stall the firmware. - Cramming every feature into a single Xtensa chip (ESP32) — RISC-V has far less toolchain burden.
- Sharing the same mutable variable between an interrupt and the main loop without a mutex — Rust will block you, but code that bypassed safety with
unsafeis dangerous. - Forgetting to expose debug probe pins on a prototype PCB — one bad flash and USB DFU may not be enough to recover.
- 100 percent Rust purism — if a vendor BLE stack (SoftDevice) or ESP-IDF Wi-Fi is more stable, wrap it and use it.
Next Post Preview
Candidates for the next post: Matter device in Rust — ESP32-C6 plus rs-matter full stack, Embassy plus LoRa (SX126x) for a long-range sensor network, probe-rs deep dive — writing a debug probe of your own.
"Embedded Rust is the act of making the compiler your ally. Whose memory it is, what an interrupt wakes, where a resource is released — the code says it out loud."
— Embedded Rust 2026 deep dive, end.
References
- Embassy — Modern Embedded Rust
- Embassy GitHub — embassy-rs/embassy
- Embassy Book — Async Rust on microcontrollers
- embedded-hal — The Embedded Rust Working Group
- The Embedded Rust Book
- The Rust Embedded WG
- probe-rs — Modern debugging for embedded Rust
- probe-rs GitHub — probe-rs/probe-rs
- defmt — Efficient logging for embedded systems
- defmt GitHub — knurling-rs/defmt
- Knurling tools — Ferrous Systems
- esp-rs — Espressif Rust support
- esp-hal Documentation
- The Rust on ESP Book
- espup — ESP toolchain installer
- esp-wifi — Wi-Fi and BLE for ESP32 in Rust
- rp2040-hal — Raspberry Pi RP2040 HAL
- rp235x-hal — Raspberry Pi RP2350 HAL
- rp-pico BSP
- Raspberry Pi Pico SDK page
- Raspberry Pi Pico 2 — RP2350 product page
- stm32-rs — STM32 PACs
- embassy-stm32 — Embassy STM32 HAL
- nrf-hal — Nordic nRF5x HAL
- embassy-nrf — Embassy nRF HAL
- TrouBLE — Pure Rust BLE host
- nrf-softdevice — SoftDevice wrapper
- RTIC — Real-Time Interrupt-driven Concurrency
- rs-matter — Matter protocol in Rust
- embedded-graphics — UI for small displays
- embedded-storage trait
- sequential-storage — Wear-leveled KV on flash
- smoltcp — TCP/IP for embedded Rust
- Ferrocene — Certified Rust compiler
- Ambiq Apollo HAL
- ch32-hal — WCH CH32V
- atsamd-hal — Microchip SAMD
- The Discovery book — STM32F3
- Awesome Embedded Rust
- Matrix room — Rust Embedded
- Embassy chat — embassy-rs Matrix
- Hackaday Supercon badges
- EMF Camp 2024 badge — tildagon
현재 단락 (1/483)
Five years ago this post would have opened with "embedded Rust is still experimental, but…" Embassy ...