Rust is making a serious case for embedded systems development. Where C gives you raw hardware control with manual memory management, Rust provides the same hardware-level access with compile-time safety guarantees — no garbage collector, no runtime overhead, and no null pointer surprises.
This guide covers everything needed to write, build, and run Embedded Rust on an ARM Cortex-M target using QEMU for emulation.
What Embedded Rust Covers
The embedded Rust learning path mirrors the embedded C path, but replaces C tooling and idioms with Rust equivalents:
Embedded Rust Learning Path
-----------------------------
1. Tools and environment setup
2. Microcontroller configuration and control
3. Peripheral functionality (GPIO, PWM, ADC)
4. Communication protocols (UART, SPI, I2C)
5. Multitasking (cooperative vs preemptive, interrupts, schedulers)
6. Control systems (open-loop, closed-loop, sensors, actuators)
Required Tools
The development environment is Linux (Ubuntu). The following tools are needed:
| Tool | Minimum Version | Purpose |
|---|---|---|
| Rust | 1.51 | Compiler and package manager |
| itmdump | 0.3.1 | Parse and dump ARM ITM debug packets |
| cargo-binutils | 0.1.4 | GNU binutils alternatives (objdump, nm, size) |
| arm-none-eabi-gdb | 7.12 | ARM GNU debugger |
| OpenOCD | 0.8 | Open On-Chip Debugger for hardware and QEMU |
| minicom | 2.7 | UART terminal for serial communication |
Install Rust
https://www.rust-lang.org/tools/install
Follow the official rustup installer. This also installs cargo, Rust's package manager.
Install itmdump
ITM stands for Instrumentation Trace Macrocell — an ARM hardware feature that supports printf-style debugging and OS event tracing without a full debugger connection.
cargo install itm
itmdump -V
# itmdump 0.3.1
Install cargo-binutils
First install the LLVM tools component, which provides llvm-nm, llvm-objcopy, llvm-objdump, and llvm-size:
rustup component add llvm-tools-preview
Then install cargo-binutils:
cargo install cargo-binutils
Install arm-none-eabi-gdb (Ubuntu 18.04+)
sudo apt install gdb-multiarch
Install OpenOCD (Ubuntu 18.04+)
sudo apt install openocd
Install minicom (Ubuntu 18.04+)
sudo apt install minicom
Creating a New Embedded Rust Project
Use cargo to create a new project:
cargo new hello-embedded-rust
cd hello-embedded-rust
This creates a directory with a src/main.rs and a Cargo.toml.
Writing the Hello World Program
Replace the contents of src/main.rs with this bare-metal Rust program:
#![no_main]
#![no_std]
#[allow(unused)]
use panic_halt as _;
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
#[entry]
fn main() -> ! {
hprintln!("Hello, world!").unwrap();
// Exit QEMU cleanly
// NOTE: do not run this on real hardware — it can corrupt OpenOCD state
debug::exit(debug::EXIT_SUCCESS);
loop {}
}
Key attributes and crates used:
| Item | Description |
|---|---|
#![no_main] | Disables the standard Rust main entry point |
#![no_std] | Disables the standard library (no OS, no heap) |
panic_halt | Halts the CPU on panic instead of unwinding |
cortex_m_rt::entry | Provides the #[entry] macro for the bare-metal entry point |
cortex_m_semihosting | Allows printing to the host via QEMU semihosting |
-> ! | The return type ! means the function never returns |
Configuring Cargo.toml
Add the required dependencies to Cargo.toml:
[dependencies]
cortex-m = "0.6.0"
cortex-m-rt = "0.6.10"
cortex-m-semihosting = "0.3.3"
panic-halt = "0.2.0"
Cargo will automatically download and compile these from crates.io.
Creating the Memory Layout File
Embedded Rust uses a memory.x linker script to define the target device's memory layout. Create this file in the project root:
MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
/* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
The cortex-m-rt crate's link.x linker script reads this memory.x file to determine where to place code and data sections.
Cross-Compiling for ARM
Rust supports multiple ARM targets. Choose the one matching your microcontroller:
| Target Triple | Processor |
|---|---|
thumbv6m-none-eabi | Cortex-M0, Cortex-M0+ |
thumbv7m-none-eabi | Cortex-M3 |
thumbv7em-none-eabi | Cortex-M4, Cortex-M7 |
thumbv7em-none-eabihf | Cortex-M4F, Cortex-M7F (with FPU) |
Install the target library for Cortex-M3:
rustup target add thumbv7m-none-eabi
Building the Project
Build for Cortex-M3 with the linker script:
export RUSTFLAGS="-C link-arg=-Tlink.x"
cargo build --target thumbv7m-none-eabi
The compiled binary will be at:
target/thumbv7m-none-eabi/debug/hello-embedded-rust
Running on QEMU
Test the program without real hardware using QEMU's ARM system emulator:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/hello-embedded-rust
QEMU flags explained:
| Flag | Description |
|---|---|
-cpu cortex-m3 | Emulates a Cortex-M3 CPU; catches miscompilation for wrong targets |
-machine lm3s6965evb | Emulates the LM3S6965 evaluation board (one of few QEMU-supported boards) |
-nographic | Suppresses the QEMU GUI |
-semihosting-config enable=on,target=native | Enables semihosting so hprintln! prints to the host terminal |
-kernel $file | Specifies the ELF binary to load and execute |
Automating with Cargo Configuration
To avoid typing long commands every time, create a .cargo/config file:
mkdir .cargo
Add this content to .cargo/config:
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
rustflags = [
# LLD (shipped with the Rust toolchain) is used as the default linker
"-C", "link-arg=-Tlink.x",
]
[build]
target = "thumbv7m-none-eabi" # Default target: Cortex-M3
Now you can simply use:
cargo build # Build the project
cargo run # Build and run in QEMU
How Embedded Rust Compares to Embedded C
+---------------------------+---------------------------+
| Embedded C | Embedded Rust |
+---------------------------+---------------------------+
| Manual memory management | Ownership system |
| Undefined behavior (UB) | No UB by default |
| Raw pointers everywhere | References with lifetimes |
| No type-safe HAL | Type-state hardware APIs |
| Linker script manual | memory.x abstraction |
| cargo-equivalent: make | cargo (built-in) |
+---------------------------+---------------------------+
Rust enforces safety at compile time — many classes of embedded bugs (use-after-free, data races, uninitialized memory) simply do not compile.
Final Thoughts
Embedded Rust is production-ready for ARM Cortex-M targets. The toolchain is mature, the ecosystem of crates on crates.io is growing rapidly, and the compile-time safety guarantees make it an excellent choice for any new embedded project.
The learning curve is steeper than C, but the payoff is firmware that is correct by construction rather than correct by careful review.
Embedded Rust does not just prevent bugs — it makes entire classes of bugs impossible.