ES: Embedded Rust Introduction

Embedded Rust brings memory safety, zero-cost abstractions, and modern tooling to bare-metal microcontroller programming. This guide walks through the tools, project setup, cross-compilation, and running your first embedded Rust program on QEMU.

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:

md
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:

ToolMinimum VersionPurpose
Rust1.51Compiler and package manager
itmdump0.3.1Parse and dump ARM ITM debug packets
cargo-binutils0.1.4GNU binutils alternatives (objdump, nm, size)
arm-none-eabi-gdb7.12ARM GNU debugger
OpenOCD0.8Open On-Chip Debugger for hardware and QEMU
minicom2.7UART terminal for serial communication

Install Rust

bash
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.

bash
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:

bash
rustup component add llvm-tools-preview

Then install cargo-binutils:

bash
cargo install cargo-binutils

Install arm-none-eabi-gdb (Ubuntu 18.04+)

bash
sudo apt install gdb-multiarch

Install OpenOCD (Ubuntu 18.04+)

bash
sudo apt install openocd

Install minicom (Ubuntu 18.04+)

bash
sudo apt install minicom


Creating a New Embedded Rust Project

Use cargo to create a new project:

bash
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:

rust
#![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:

ItemDescription
#![no_main]Disables the standard Rust main entry point
#![no_std]Disables the standard library (no OS, no heap)
panic_haltHalts the CPU on panic instead of unwinding
cortex_m_rt::entryProvides the #[entry] macro for the bare-metal entry point
cortex_m_semihostingAllows 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:

rust
[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:

bash
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 TripleProcessor
thumbv6m-none-eabiCortex-M0, Cortex-M0+
thumbv7m-none-eabiCortex-M3
thumbv7em-none-eabiCortex-M4, Cortex-M7
thumbv7em-none-eabihfCortex-M4F, Cortex-M7F (with FPU)

Install the target library for Cortex-M3:

bash
rustup target add thumbv7m-none-eabi


Building the Project

Build for Cortex-M3 with the linker script:

bash
export RUSTFLAGS="-C link-arg=-Tlink.x"
cargo build --target thumbv7m-none-eabi

The compiled binary will be at:

bash
target/thumbv7m-none-eabi/debug/hello-embedded-rust


Running on QEMU

Test the program without real hardware using QEMU's ARM system emulator:

bash
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:

FlagDescription
-cpu cortex-m3Emulates a Cortex-M3 CPU; catches miscompilation for wrong targets
-machine lm3s6965evbEmulates the LM3S6965 evaluation board (one of few QEMU-supported boards)
-nographicSuppresses the QEMU GUI
-semihosting-config enable=on,target=nativeEnables semihosting so hprintln! prints to the host terminal
-kernel $fileSpecifies the ELF binary to load and execute

Automating with Cargo Configuration

To avoid typing long commands every time, create a .cargo/config file:

bash
mkdir .cargo

Add this content to .cargo/config:

bash
[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:

bash
cargo build    # Build the project
cargo run      # Build and run in QEMU


How Embedded Rust Compares to Embedded C

md
+---------------------------+---------------------------+
|     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.