Embedded Rust â Programming Microcontrollers
Embedded Rust brings memory safety and zero-cost abstractions to microcontroller programming, replacing C with a language that prevents memory bugs at compile time while running on bare metal without an OS.
What You'll Learn
In this tutorial, you'll learn how Rust runs on embedded devices: no_std programming, peripheral access using the embedded HAL, GPIO control, interrupt handling, and building firmware for ARM Cortex-M microcontrollers.
Why It Matters
Embedded systems control everything from medical devices to industrial controllers to consumer electronics. A single memory corruption in firmware can cause catastrophic failures. Rust prevents these bugs at compile time, making it ideal for safety-critical embedded applications.
Real-World Use
Tesla uses Rust in their vehicle firmware. Google's Pixel firmware uses Rust. The Tock OS is written entirely in Rust for IoT devices. Smart home devices, drone flight controllers, and Durga Antivirus Pro's hardware security module use Rust for reliable firmware.
flowchart TD
APP[Rust Application] --> HAL[Embedded HAL Traits]
HAL --> PAC[Peripheral Access Crate]
PAC --> MCU[Microcontroller]
MCU --> GPIO[GPIO Pins]
MCU --> TIM[Timers]
MCU --> UART[Serial]
MCU --> I2C[Sensors]
note: "no_std: no OS, no allocator, no heap"
Prerequisites: Rust Ownership, Traits & Generics, and Unsafe Rust. Basic electronics knowledge helps.
no_std Programming
Embedded Rust often uses #![no_std] to exclude the standard library.
// This is a conceptual example -- requires a target like thumbv7em-none-eabi
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn main() -> ! {
// No std library available
// No heap allocation without alloc crate
// No println! without custom implementation
loop {}
}
GPIO Control with Embedded HAL
The embedded HAL provides platform-independent abstractions for microcontroller peripherals.
// Conceptual example using embedded_hal traits
use embedded_hal::digital::v2::{OutputPin, InputPin};
use cortex_m::delay::Delay;
use cortex_m::Peripherals;
struct Led<P: OutputPin> {
pin: P,
}
impl<P: OutputPin> Led<P> {
fn new(pin: P) -> Self {
Led { pin }
}
fn on(&mut self) {
self.pin.set_high().ok();
}
fn off(&mut self) {
self.pin.set_low().ok();
}
fn toggle(&mut self) {
self.pin.toggle().ok();
}
}
fn blink_led<L: OutputPin + InputPin>(led: &mut L, delay: &mut Delay) {
loop {
led.set_high().unwrap();
delay.delay_ms(500);
led.set_low().unwrap();
delay.delay_ms(500);
}
}
Timer and Delay
Accessing hardware timers through the HAL.
// Conceptual example using stm32f4xx_hal
/*
use stm32f4xx_hal::{
prelude::*,
stm32,
timer::Timer,
};
use cortex_m::peripheral::syst::SystClkSource;
fn setup_timer(dp: stm32::Peripherals, cp: cortex_m::Peripherals) {
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let mut timer = Timer::syst(cp.SYST, 1.hz(), clocks);
timer.listen(|t| {
// Timer interrupt fired
t.clear();
// Handle timer event
});
}
*/
fn main() {
println!("Timer setup (conceptual)");
println!("Timers require hardware-specific peripheral access crates");
}
Interrupt Handling
Rust safely handles hardware interrupts.
// Conceptual interrupt handling with cortex-m-rt
// #![no_std]
// #![no_main]
// use cortex_m_rt::entry;
// use stm32f4xx_hal::stm32;
// #[interrupt]
// fn TIM2() {
// static mut COUNTER: u32 = 0;
// *COUNTER += 1;
// // Interrupt service routine
// // No data races -- single core, interrupt disables other interrupts
// }
fn main() {
println!("Interrupt handling (conceptual)");
println!("Interrupts are safe in Rust: no data races in ISRs");
}
Memory-Mapped Registers
Safe access to hardware registers using typed MMIO.
use core::ptr;
// Simulated memory-mapped register
#[repr(C)]
struct GpioRegisters {
moder: u32, // Mode register
otyper: u32, // Output type
ospeedr: u32,// Speed
pupdr: u32, // Pull-up/pull-down
idr: u32, // Input data
odr: u32, // Output data
bsrr: u32, // Bit set/reset
}
const GPIO_BASE: usize = 0x4002_0000;
fn set_gpio_high(pin: u8) {
let gpio = unsafe { &*(GPIO_BASE as *const GpioRegisters) };
// Safe wrapper around memory-mapped register
unsafe {
gpio.bsrr.write_volatile(1 << pin);
}
}
fn main() {
println!("MMIO register access (conceptual)");
println!("unsafe is required for hardware register access");
}
Common Mistakes
1. Forgetting the Target Specification
Embedded Rust requires --target thumbv7em-none-eabi (or similar). Forgetting this causes linker errors. Install targets via rustup target add.
2. Using std Instead of core
In no_std environments, use core:: instead of std::. Functions like Vec are unavailable unless you add the alloc crate.
3. Ignoring the Linker Script
Microcontrollers need a custom linker script for memory layout. The cortex-m-rt crate provides one for Cortex-M targets.
4. Stack Overflow from Recursion
Embedded devices have limited stack (often 1-4KB). Recursion overflows quickly. Use iterative approaches and check stack usage with cargo-call-stack.
5. Not Using entry Attribute
The #[entry] attribute from cortex-m-rt sets up the correct startup sequence. Without it, the firmware may not initialize properly.
Practice Questions
1. What does no_std mean in Rust?
It means the program does not use the standard library. Only the core library is available, providing basic types without OS-dependent features like file I/O, networking, or heap allocation.
2. What is the embedded HAL? The Embedded Hardware Abstraction Layer is a set of traits that provide platform-independent interfaces to microcontroller peripherals like GPIO, I2C, SPI, UART, and timers.
3. How does Rust prevent data races in interrupt handlers?
On single-core microcontrollers, interrupt handlers disable other interrupts automatically. Rust's type system ensures that shared data accessed by both main code and interrupts is properly protected (e.g., using CriticalSection).
4. What toolchain is needed for embedded Rust?
A target-specific Rust toolchain (e.g., thumbv7em-none-eabi), a linker script, a flashing tool (OpenOCD, probe-rs), and hardware-specific peripheral access and HAL crates.
5. Challenge: Design an embedded application structure using the embedded HAL that reads a temperature sensor via I2C and blinks an LED at a rate proportional to the temperature.
Mini Project: Button-Controlled LED
// Conceptual embedded application
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
struct ButtonControlledLed {
led_pin: u8,
button_pin: u8,
state: bool,
}
impl ButtonControlledLed {
fn new(led_pin: u8, button_pin: u8) -> Self {
ButtonControlledLed { led_pin, button_pin, state: false }
}
fn update(&mut self, button_pressed: bool) {
if button_pressed && !self.state {
self.state = true;
self.set_led(true);
} else if !button_pressed && self.state {
self.state = false;
self.set_led(false);
}
}
fn set_led(&self, on: bool) {
// Hardware-specific GPIO write
let value: u8 = if on { 1 } else { 0 };
unsafe {
core::ptr::write_volatile(
(0x4002_0014 + self.led_pin as usize) as *mut u8,
value,
);
}
}
}
fn main() -> ! {
let mut controller = ButtonControlledLed::new(5, 3);
loop {
let button = unsafe {
core::ptr::read_volatile((0x4002_0010 + 3) as *const u8)
};
controller.update(button != 0);
}
}
FAQ
Related Concepts
What's Next
Learn Performance Optimization for profiling and optimizing embedded firmware, and explore Cargo Workspaces for managing multi-crate embedded projects.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro