Embedded Systems

Rust for Embedded

2019. Microsoft Security Response Center analyses 12 years of CVEs and finds: 70% of vulnerabilities are memory safety bugs in C and C++. On a desktop, a patch ships overnight. In a pacemaker firmware - a patch is a surgical procedure. Rust solves this not with a garbage collector but with the compiler - zero runtime overhead, same safety guarantees.

  • Medtronic insulin pump (2019): CVE allowed remote insulin dose control via radio - buffer overflow in C firmware
  • Tesla Model S (2015): Charlie Miller and Chris Valasek took remote control through the CAN bus - memory corruption in the CAN frame handler
  • Google Android: migrating Bluetooth and WiFi stack modules from C/C++ to Rust reduced memory safety bugs by 68% in two years
  • SPARK/Ada and now Rust are the tools of choice for DO-178C (avionics) and ISO 26262 (automotive) certified firmware

no_std and Ownership Without GC

2019. Microsoft Security Response Center publishes an analysis of 12 years of CVEs. The finding: 70% of vulnerabilities are memory safety bugs in C and C++. Use-after-free, buffer overflow, data race. On a desktop, a patch ships overnight. In a pacemaker firmware or automotive ABS - a patch requires a device recall, re-flashing at a dealer, sometimes surgery. Rust fixes this not with a garbage collector but with a type system - zero runtime overhead.

The Rust standard library assumes an OS: heap allocation, threads, stdin/stdout. A Cortex-M0 with 16 KB of RAM provides none of that. The `#![no_std]` attribute drops the standard library and keeps only `core` - the subset without alloc or OS dependencies. This is not a restriction - it is honesty: the compiler knows exactly what the hardware provides.

Ownership is not an embedded-specific concept. The rule is universal: each value has one owner; when the owner goes out of scope, the value is dropped. No GC. No reference counting. The compiler proves at build time that memory is freed exactly once and there are no dangling pointers. What GC does with 5-50 ms pauses, Rust does at compile time with no runtime cost.

In embedded Rust, `#![no_std]` + `#![no_main]` is the standard starting point. The `panic-halt` crate stops the processor on panic (alternatives: `panic-semihosting` for debug output, `panic-itm` for ITM trace). Without a panic handler the code will not compile - Rust requires an explicit decision.

The borrow checker runs at compile time and tracks lifetimes. Peripheral access through registers is a canonical case: two code paths cannot simultaneously own `GPIOA`. In C, this is enforced by developer discipline - and violated regularly. In Rust, it is a compile error. This is precisely the property required in safety-critical firmware.

What is the purpose of `#![no_std]` in embedded Rust?

unsafe: An Explicit Contract With Hardware

The paradox: the safest systems language for embedded requires `unsafe` for any peripheral access. This is not a weakness - it is honesty. The borrow checker does not know that register `0x4800_0014` is the ODR register of GPIOA, or that writing to it changes the voltage on pin PA5. Such knowledge is outside the language memory model. `unsafe` is an explicit contract: the developer takes responsibility here; the compiler steps aside.

`write_volatile` is not a quirk. The LLVM optimizer sees a variable that is "never read" and eliminates the write as dead code. `volatile` signals: this write has a hardware side effect, do not touch it. In C this is `volatile uint32_t*` - it works, but is easy to forget. In Rust, using `core::ptr::write` instead of `write_volatile` compiles without warnings but silently breaks at `-O2`.

An `unsafe` block does NOT disable the borrow checker or type system. It permits only four things: dereferencing a raw pointer, calling an unsafe function, accessing a mutable static, and implementing an unsafe trait. Everything else follows the same rules.

Minimizing `unsafe` is an architectural principle. The ideal embedded Rust project: one `unsafe` block inside the HAL that wraps registers into a safe API, and all application code in safe Rust. Crates like `stm32f4xx-hal` and `nrf52840-hal` follow this pattern: `unsafe` inside, safe API outside. If `unsafe` spreads across the whole codebase it signals an architectural smell.

Why must hardware register writes use `core::ptr::write_volatile` instead of a plain assignment?

PAC, HAL and embedded-hal Traits

Every microcontroller ships with an SVD file - an XML description of all peripheral registers (addresses, fields, bit widths). The `svd2rust` tool reads the SVD and generates a Rust crate: the PAC (Peripheral Access Crate). The PAC for STM32F411 is the `stm32f4` crate. Inside: type-safe wrappers around every register. Not `*(0x4800_0014) = 1 << 5`, but `gpioa.odr.modify(|_, w| w.odr5().set_bit())`. The `unsafe` is isolated inside generated code.

The HAL (Hardware Abstraction Layer) sits on top of the PAC and adds high-level abstractions. `stm32f4xx-hal` turns raw GPIOA access into an object `PA5<Output<PushPull>>` that carries its operating mode in the type. Calling `read()` on an output-mode pin is a compile error, not a runtime panic. This is the typestate pattern: hardware state is encoded in the type.

`embedded-hal` is a crate of traits: `OutputPin`, `InputPin`, `SpiDevice`, `I2c`, `Serial`. Traits are contracts. The STM32 HAL implements `OutputPin` for its GPIO types. The nRF52 HAL implements the same `OutputPin`. The SSD1306 display driver is written against `SpiDevice<u8>` - and works on any hardware that implements that trait. One driver for a thousand boards. In C this is 200 lines of `#ifdef BOARD_STM32 / #elif BOARD_NRF52`.

The Rust embedded stack: SVD file -> `svd2rust` -> PAC (type-safe registers) -> HAL (high-level abstractions, typestate) -> `embedded-hal` traits (portable API) -> device drivers (hardware-agnostic). Each layer narrows the unsafe surface and raises the abstraction level.

unsafe in embedded Rust means the code is as dangerous as C

unsafe is an explicitly marked, auditable zone isolated inside HAL/PAC layers. All application code remains safe Rust with compile-time guarantees

C has no mechanism to isolate unsafe operations. In Rust, unsafe is an auditable boundary: one can find all 47 lines of unsafe in a project with a single grep and review each one

What is the primary benefit of the typestate pattern in an embedded HAL (e.g., `PA5<Output<PushPull>>`)?

Key Ideas

  • `#![no_std]` drops the standard library, keeping `core` - works without an OS or heap allocation
  • Ownership + borrow checker guarantee memory safety at compile time - GC-level safety with zero runtime pauses
  • `unsafe` is an explicit, auditable boundary, not a safety off-switch: isolated in PAC/HAL, application code stays safe
  • PAC is generated from SVD files via `svd2rust` - type-safe wrappers over every peripheral register
  • HAL builds on top of PAC and encodes peripheral state in types (typestate); incorrect use is a compile error
  • `embedded-hal` traits make device drivers portable: one `SpiDevice` trait for STM32, nRF52, RP2040

Related Topics

Embedded Rust draws on several intersecting areas:

  • C for Embedded — The predecessor Rust is measured against: same problems, different compile-time guarantees
  • Safety-Critical Systems — Rust as the primary tool for IEC 61508, ISO 26262 certified firmware
  • Ownership Model — Theoretical foundation of the borrow checker - the basis of memory safety without GC
  • OS Memory Management — Contrast: in embedded there is no OS memory manager; Rust takes that role at compile time

Вопросы для размышления

  • If 70% of CVEs are memory safety bugs, why does the industry still write embedded firmware predominantly in C?
  • The typestate pattern encodes hardware state in types. What other peripheral states could be expressed this way - and where would it become excessive?
  • unsafe blocks in Rust vs volatile in C: both require developer discipline. What is the fundamental difference from an auditability perspective?

Связанные уроки

  • emb-04 — C for embedded - foundation before moving to Rust
  • emb-14 — Safety-critical systems use Rust as their primary tool
  • plt-13-ownership — Ownership in embedded - same rules, higher stakes
  • plt-10-linear-types — Linear types - theoretical foundation of the Rust ownership model
  • os-07-memory — Memory management without OS - exactly what Rust solves at compile time
  • os-01-intro
Rust for Embedded

0

1

Sign In