Low-Level Systems Programming

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.