Why Choose Rust for Embedded Systems?
Rust for embedded systems means using the Rust programming language to build software that runs directly on small, resource-limited hardware devices, like microcontrollers, sensors, IoT devices, robots, drones, and industrial controllers, instead of regular computers.
- Memory Safety: Rust prevents memory leaks and buffer overflows, which are common in embedded C/C++ programs.
- No Garbage Collection: Rust avoids the overhead of garbage collection, making it suitable for resource-constrained devices.
- Concurrency: Rust’s async and threading features allow efficient multitasking on embedded devices.
- Control Over Hardware: Rust provides low-level control while ensuring safety.
- Ecosystem: Libraries like embedded-hal make working with hardware peripherals simple.
Where Rust Is Used in Embedded Development
Rust is already being used in real-world embedded projects:
- SpaceX uses Rust for parts of its flight control software.
- Automotive companies use Rust for in-car embedded systems.
- IoT startups use Rust for secure, low-power devices.
- Hobbyists use Rust for Arduino, Raspberry Pi Pico, and ESP32 projects.
Setting Up a Rust Embedded Project
Before you can start writing code for microcontrollers or embedded devices using Rust, you need to set up your development environment correctly.
This setup is different from regular Rust programming because embedded systems don’t run an operating system and use custom hardware architectures.
Step 1: Install Rust Toolchain for Embedded Development
First, you’ll need the Rust compiler and tools to support embedded targets, which are special compilation targets that generate machine code suitable for microcontrollers.
If you haven’t installed Rust yet, install it with:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Once Rust is installed, you can add support for an embedded target (for example, an ARM Cortex-M microcontroller):
rustup target add thumbv7em-none-eabihf
Step 2: Create a New Embedded Project
The Rust Embedded Working Group provides an official template to make this process super easy.
First, install the cargo-generate tool (used to create projects from templates):
cargo install cargo-generate
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
cd your_project_name
- These command creates a ready-to-use embedded project structure.
- Includes important files like memory.x (memory layout), Cargo.toml, and src/main.rs.
Step 3: Add Required Dependencies
Open the Cargo.toml file and add these under [dependencies]:
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embedded-hal = "0.2"
panic-halt = "0.2"
Key Concepts in Rust Embedded Systems
These concepts are the building blocks that allow you to control hardware, handle events, and run multiple tasks.
1. embedded-hal – The Hardware Abstraction Layer
The embedded-hal crate (HAL = Hardware Abstraction Layer) is a fundamental part of the Rust embedded ecosystem. It defines a set of standard traits (interfaces) for working with hardware peripherals, like:
- Communication protocols (SPI, I2C, UART)
- GPIO (input/output pins)
- Timers and delays
Example: Turning an LED On and Off
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use embedded_hal::digital::v2::OutputPin;
struct LedPin; // Imagine this represents a real LED pin on your board
impl LedPin {
fn new() -> Self {
LedPin
}
}
impl OutputPin for LedPin {
type Error = core::convert::Infallible;
fn set_high(&mut self) -> Result<(), Self::Error> {
// Code to set the pin high (turn LED ON)
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
// Code to set the pin low (turn LED OFF)
Ok(())
}
}
#[entry]
fn main() -> ! {
let mut led = LedPin::new();
loop {
led.set_high().unwrap(); // Turn ON the LED
delay();
led.set_low().unwrap(); // Turn OFF the LED
delay();
}
}
fn delay() {
// A very simple delay loop
for _ in 0..2_000_000 {
cortex_m::asm::nop();
}
}
2. Microcontroller-Specific Libraries
While embedded-hal gives you a generic interface, each microcontroller family (like STM32, ESP32, RP2040, etc.) has its own HAL implementation that talks directly to the hardware.
- STM32: Use the stm32f4xx-hal crate.
- ESP32: Use the esp32-hal crate.
- Raspberry Pi Pico: rp2040-hal
These libraries make it easy to control GPIO pins, timers, peripherals, and communication interfaces without writing low-level register code.
Example: Blinking an LED on STM32
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use stm32f4xx_hal::{pac, prelude::*, timer::Timer};
use panic_halt as _;
#[entry]
fn main() -> ! {
// Get access to the hardware peripherals
let peripherals = pac::Peripherals::take().unwrap();
// Split the GPIO port A into individual pins
let gpioa = peripherals.GPIOA.split();
// Configure PA5 as a push-pull output pin (connected to an onboard LED)
let mut led = gpioa.pa5.into_push_pull_output();
// Create a 1 Hz timer (1-second delay)
let mut timer = Timer::syst(peripherals.SYST, 1.hz(), peripherals.CLOCK);
loop {
led.set_high().unwrap(); // Turn LED ON
timer.wait().unwrap(); // Wait 1 second
led.set_low().unwrap(); // Turn LED OFF
timer.wait().unwrap(); // Wait 1 second
}
}
3. Handling Hardware Interrupts
Embedded systems often rely on interrupts, signals from hardware that tell the CPU to immediately respond to an event (like a button press, timer overflow, or data received over UART).
Example: Handling an Interrupt
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use cortex_m::interrupt;
use panic_halt as _;
#[entry]
fn main() -> ! {
// Execute this block without allowing interrupts to interfere
interrupt::free(|_cs| {
// Critical setup or handling code goes here
// e.g., configure external interrupt pins or reset flags
});
loop {
// Main loop continues running
}
}
- Interrupts allow the microcontroller to react instantly to external or internal events.
4. Concurrency with RTIC
Rust offers a powerful concurrency framework for embedded systems called RTIC.
It helps you structure your code into tasks that automatically run in response to interrupts, making multitasking safe, deterministic, and efficient.
Example: Using RTIC
#![no_std]
#![no_main]
#[rtic::app(device = stm32f4xx_hal::pac)]
mod app {
use stm32f4xx_hal::gpio::{gpioa, Output, PushPull};
#[resources]
struct Resources {
led: gpioa::PA5<Output<PushPull>>,
}
#[init]
fn init(cx: init::Context) -> init::LateResources {
let gpioa = cx.device.GPIOA.split();
let led = gpioa.pa5.into_push_pull_output();
init::LateResources { led }
}
#[task(resources = [led])]
fn blink(cx: blink::Context) {
// Toggle the LED safely from a task
cx.resources.led.set_high().unwrap();
cx.resources.led.set_low().unwrap();
}
}
- It allows you to write real-time, concurrent embedded code without worrying about deadlocks, unsafe shared access, or timing issues.
Learn More About Rust Programming
- What is RefCell in Rust Programming?
- What is Arc Pointer in Rust?
- What is an RC Pointer in Rust?
- What are smart pointers in Rust?
- What is Benchmarking in Rust?
- What are Macros in Rust?

M.Sc. (Information Technology). I explain AI, AGI, Programming and future technologies in simple language. Founder of BoxOfLearn.com.