Skip to content

Embedded Rust — Programming Microcontrollers

DodaTech Updated 2026-06-21 6 min read

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"
â„šī¸ Info

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

What is the difference between std and no_std Rust?

std requires an operating system with file systems, networking, and heap allocation. no_std runs on bare metal using only core, suitable for microcontrollers, kernels, and bootloaders.

Can I use Vec in no_std?

Yes, with the alloc crate and a global allocator. However, most embedded systems avoid heap allocation entirely. Use fixed-size arrays and stack allocation instead.

How do I debug embedded Rust programs?

Use probe-rs with a debug probe (J-Link, ST-Link) for GDB debugging. The defmt crate provides logging over SWD. Semihosting lets you print to the host console.

Rust FFI
Unsafe Rust
Performance Optimization

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