Last updated: April 20, 2025
Table of Contents
1. Introduction: Why Rust for Embedded?
Embedded systems programming, traditionally dominated by C and C++, involves writing software for resource-constrained devices like microcontrollers (MCUs). These systems often require high reliability, predictable performance, and direct hardware interaction. Rust is emerging as a strong contender in this space due to several key advantages:
- Memory Safety (Without GC): Rust's compile-time checks prevent common memory errors (null pointers, buffer overflows, data races) without needing a garbage collector, which is often unsuitable for resource-constrained or real-time embedded systems.
- Performance: Rust compiles to efficient machine code, offering performance comparable to C/C++.
- Concurrency Safety: Rust's Send/Sync traits help manage concurrency safely, crucial for systems interacting with interrupts or multiple peripherals.
- Expressive Type System: Features like enums, pattern matching, and generics allow for writing expressive and robust code, helping manage hardware complexity.
- Modern Tooling: Cargo provides excellent dependency management and build orchestration.
This article introduces the core concepts and tools needed to start developing embedded applications using Rust.
2. Going Bare-Metal: #![no_std]
Most embedded systems do not have an operating system providing standard library functionalities like dynamic memory allocation (heap), file I/O, networking, threads, etc. Rust accommodates this through the #![no_std]
attribute.
When placed at the top of your crate (lib.rs
or main.rs
), #![no_std]
tells the compiler not to link the standard library (std
). Instead, you rely on the much smaller core
library, which provides fundamental types (like primitives, Option, Result), traits, and macros, but nothing requiring OS support or heap allocation by default.
You also typically need to provide a custom panic handler and define the entry point of your program, as the standard main
function relies on the std
runtime.
// Example at the top of src/main.rs or src/lib.rs
#![no_std]
#![no_main] // Indicates we are providing our own entry point
use core::panic::PanicInfo;
// Define a custom panic handler
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
// Implement what happens on panic, e.g., loop forever
loop {}
}
// Define the entry point (specific attribute depends on the target/architecture)
// e.g., for cortex-m microcontrollers:
// #[cortex_m_rt::entry]
// fn main() -> ! {
// // Your embedded application logic here...
// loop {}
// }
3. Key Concepts in Embedded Rust
3.1 Target Triples & Cross-Compilation
Embedded development usually involves cross-compilation: building code on your development machine (e.g., x86_64 Linux/macOS/Windows) for a different target architecture (e.g., ARM Cortex-M). Rust manages this through target triples, which specify the architecture, vendor, and OS/ABI (e.g., thumbv7em-none-eabihf
for an ARM Cortex-M4F).
You need to install the target support using rustup
:
rustup target add thumbv7em-none-eabihf
And build using the --target
flag:
cargo build --target thumbv7em-none-eabihf
3.2 Peripheral Access Crates (PACs)
These crates provide low-level, auto-generated register definitions for a specific microcontroller family (e.g., stm32f4
, nrf52840
). They allow direct, unsafe access to hardware registers, often using the Single-Value Pattern (SVD) files provided by chip manufacturers. Working directly with PACs is powerful but verbose and requires deep knowledge of the hardware.
3.3 Hardware Abstraction Layers (HALs)
HALs provide higher-level, safer abstractions over the PACs. They offer APIs for common peripherals (GPIO, SPI, I2C, UART, Timers) that are easier to use and often portable across different MCUs within the same family (or even across families if they implement common embedded-hal
traits).
The embedded-hal
crate defines a set of traits (interfaces) for common embedded functionalities. HAL crates implement these traits for specific hardware, promoting code reuse and portability.
3.4 Board Support Crates (BSPs)
BSPs build upon HALs and PACs to provide abstractions specific to a particular development board (e.g., Raspberry Pi Pico, STM32 Discovery Board). They configure clocks and map the board's specific pins (like LEDs, buttons, sensors) to the underlying HAL peripherals, making it easier to get started with a specific board.
4. Essential Tooling
Beyond cargo
and rustup
, several tools streamline embedded development:
4.1 cargo-embed
A tool that integrates building, flashing (programming the MCU), and debugging (via GDB) into a single command. It uses probe-rs
underneath.
# Install
cargo install cargo-embed
# Configure in Cargo.toml (example)
# [package.metadata.embed]
# chip = "STM32F401RETx"
# target = "thumbv7em-none-eabihf"
# ... other options
# Run (builds, flashes, starts GDB server)
cargo embed --release
4.2 probe-rs
A library and command-line tool for interacting with various debug probes (like ST-Link, J-Link, DAPLink) to flash firmware onto MCUs and debug them. It supports a wide range of targets.
# Install CLI tool (optional, cargo-embed uses the library)
cargo install probe-rs --features cli
# Example usage (basic flash)
probe-rs download path/to/firmware.elf --chip STM32F401RE
4.3 flip-link
(Optional)
A linker wrapper that can significantly reduce memory usage by placing the stack at the end of RAM, allowing it to grow downwards towards the heap/static variables. Particularly useful on very memory-constrained devices.
5. Getting Started: A High-Level View
- Choose Hardware: Select a microcontroller and development board.
- Install Rust & Tools: Set up Rust,
rustup
, the target triple for your MCU, and tools likecargo-embed
orprobe-rs
. - Find Crates: Look for a Board Support Crate (BSP) or Hardware Abstraction Layer (HAL) for your chosen hardware on crates.io.
- Set up Project: Use a template (like those provided by
knurling-rs/app-template
) or configure a#![no_std]
project manually. Add dependencies (PAC, HAL/BSP, RTIC, etc.). - Write Code: Use the HAL/BSP to configure peripherals and implement your application logic.
- Build & Flash: Use
cargo build --target ...
and then flash usingcargo-embed
orprobe-rs
. - Debug: Use
cargo-embed
orprobe-rs
with GDB for debugging.
6. Conceptual Example: Blinking an LED
While the exact code depends heavily on the specific board and HAL, the general structure often looks like this:
// Example for a hypothetical board/HAL
#![no_std]
#![no_main]
use panic_halt as _; // Simple panic handler that just halts
// Import Board Support Crate or HAL
use my_bsp::{entry, hal::{prelude::*, delay::Delay, pac}};
// Or: use my_hal::{prelude::*, delay::Delay, pac};
#[entry] // Entry point macro from the runtime crate (e.g., cortex_m_rt)
fn main() -> ! {
// Acquire peripherals from the PAC
let dp = pac::Peripherals::take().unwrap();
let cp = pac::CorePeripherals::take().unwrap(); // Core peripherals like SysTick
// Get access to GPIO pins from the HAL/BSP
let gpiob = dp.GPIOB.split();
// Configure pin PB7 (example) as a push-pull output
let mut led = gpiob.pb7.into_push_pull_output();
// Set up a delay mechanism using the SysTick timer
let mut delay = Delay::new(cp.SYST, clocks); // 'clocks' would be configured based on HAL
// Main loop
loop {
led.set_high().unwrap(); // Turn LED on
delay.delay_ms(500_u32); // Wait 500ms
led.set_low().unwrap(); // Turn LED off
delay.delay_ms(500_u32); // Wait 500ms
}
}
This conceptual example shows acquiring peripherals, configuring a GPIO pin for the LED, setting up a delay, and blinking the LED in a loop.
7. Conclusion
Embedded Rust offers a compelling combination of performance, memory safety, and modern tooling for developing reliable firmware. By leveraging #![no_std]
, Hardware Abstraction Layers (HALs), and tools like cargo-embed
and probe-rs
, developers can effectively target microcontrollers and build sophisticated embedded applications.
While the ecosystem is still evolving compared to C/C++, it's growing rapidly and provides a productive and safe environment for tackling the challenges of embedded systems development.
8. Additional Resources
Related Articles
- Getting Started with Rust
- Rust Ownership and Borrowing Explained
- Unsafe Rust Explained
- Writing Concurrent Applications in Rust
External Links
- The Embedded Rust Book (Essential Reading)
- The Rust Embedded Discovery Book (Tutorial based on STM32F3 Discovery board)
- Awesome Embedded Rust (Curated list of resources, crates, tools)
- Crates implementing embedded-hal
- Probe-rs Website
- cargo-embed Repository
- Rust Embedded Matrix Chat