Embedded Rust Concepts
Embedded Rust brings Rust's compile-time ownership, borrow checking, and zero-cost type-state abstractions to bare-metal microcontrollers (such as ARM Cortex-M, RISC-V, and ESP32) that run without an underlying operating system (#![no_std] and #![no_main]).
+-------------------------------------------------------------------+
| Embedded Rust Crate Hierarchy |
| |
| 4. Application Firmware (`#![no_std]`, `#![no_main]`, `#[entry]`)|
| | |
| 3. Portable Drivers (Written against `embedded-hal` traits!) |
| | |
| 2. HAL Crate (`stm32f4xx-hal`, `esp-hal`, `rp-hal`) |
| | |
| 1. PAC — Peripheral Access Crate (`svd2rust` MMIO registers) |
| | |
| 0. Architecture Crate (`cortex-m`, `riscv`) & Hardware |
+-------------------------------------------------------------------+
The Embedded Rust Software Stack
| Layer | Role | Ecosystem Examples |
|---|---|---|
| Architecture / Runtime | Startup reset vector, .bss/.data RAM initialization, CPU interrupt control, and inline assembly instructions (wfi, nop, bkpt). |
cortex-m, cortex-m-rt, riscv, riscv-rt |
| PAC (Peripheral Access Crate) | Auto-generated from vendor CMSIS-SVD XML files (svd2rust); provides raw volatile register bitfield access (dp.USART1.dr.write(...)). |
stm32f4, nrf52840-pac, rp2040-pac |
| HAL (Hardware Abstraction Layer) | Wraps raw PAC registers into safe, high-level Rust types enforcing valid pin modes and clock trees at compile time. | stm32f4xx-hal, embassy-stm32, esp-hal |
embedded-hal Traits |
Vendor-neutral traits (OutputPin, SpiDevice, I2c, DelayNs) so a single sensor driver crate works across STM32, nRF52, RP2040, and Linux! |
embedded-hal, embedded-hal-async, embedded-io |
Compile-Time Pin State Verification (Type-State GPIO Pattern)
In C firmware, nothing prevents code from reading a GPIO pin configured as an output or writing to an unconfigured pin at runtime. Embedded Rust HALs encode hardware peripheral states as Zero-Sized Type (ZST) generic parameters, consuming the old pin state by value (self) when reconfiguring and returning a new typed pin with zero bytes of RAM overhead:
use core::marker::PhantomData;
// Zero-Sized Type (ZST) hardware mode markers:
pub struct Disabled;
pub struct InputPullUp;
pub struct PushPullOutput;
/// Represents GPIO Pin `P` in hardware state `Mode`.
/// Because `PhantomData<Mode>` is zero-sized, `GpioPin` occupies 0 bytes in RAM!
pub struct GpioPin<const P: u8, Mode> {
_mode: PhantomData<Mode>,
}
impl<const P: u8> GpioPin<P, Disabled> {
pub const fn new() -> Self {
Self { _mode: PhantomData }
}
/// Consumes `GpioPin<P, Disabled>` and reconfigures hardware registers for InputPullUp.
pub fn into_pull_up_input(self) -> GpioPin<P, InputPullUp> {
GpioPin { _mode: PhantomData }
}
/// Consumes `GpioPin<P, Disabled>` and reconfigures hardware registers for PushPullOutput.
pub fn into_push_pull_output(self) -> GpioPin<P, PushPullOutput> {
GpioPin { _mode: PhantomData }
}
}
impl<const P: u8> GpioPin<P, InputPullUp> {
/// Only an `InputPullUp` pin can be read!
pub fn is_high(&self) -> bool {
true
}
}
impl<const P: u8> GpioPin<P, PushPullOutput> {
/// Only a `PushPullOutput` pin can be driven high or low!
pub fn set_high(&mut self) -> String {
format!("GPIO pin {P} driven HIGH")
}
}
fn main() {
assert_eq!(std::mem::size_of::<GpioPin<13, Disabled>>(), 0);
let pin13 = GpioPin::<13, Disabled>::new();
// `pin13.set_high()` here would fail to compile because `pin13` is `Disabled`!
// Transition `pin13` into `PushPullOutput` by consuming `Disabled`:
let mut led_pin = pin13.into_push_pull_output();
println!("{}", led_pin.set_high());
}
GPIO pin 13 driven HIGH
Singleton Peripherals and Interrupt Safety (critical-section)
Hardware peripherals exist at fixed singleton physical addresses. Embedded HALs expose a Peripherals::take() -> Option<Peripherals> method that uses an atomic flag (or critical section) to yield the peripheral struct at most once to main, giving your code exclusive &mut ownership of each peripheral without global static mut data races.