embedded-rust · git:20260905.fd53e99 · 2026-09-05 · sha256 f7b8c3dab67acd28
embedded-rust git:20260905.fd53e99B
Immutable. This exact content is served forever at /api/v1/blob/f7b8c3dab67acd28.
---
name: embedded-rust
description: 'Use when writing no_std Cortex-M or RISC-V firmware in Rust with cortex-m-rt, probe-rs, defmt, RTIC, or a panic handler. Not for no_std library constraints: use rust-no-std.'
---
# Embedded Rust
## Contract
| Field | Bound contract |
|---|---|
| Trigger | A Rust firmware project needs to be set up or fixed: `#![no_std]` `#![no_main]` layout, `cortex-m-rt` startup, `probe-rs` flashing and log streaming, `defmt` logging, an RTIC application, or the choice of panic handler. |
| Authority | Reversible local: writes only the project files under the directory the user names (`Cargo.toml`, `.cargo/config.toml`, `memory.x`, `src/`); rollback is deleting that directory or reverting it in version control. No remote mutation. |
| Side effect | New or edited project files in the named directory. Flashing a board writes the board's flash, which the next `cargo run` overwrites. |
| Done | `cargo build --release` produces an ELF for the target triple, `cargo run --release` flashes it and streams `defmt` output to the terminal, and exactly one panic handler is linked. |
## Inputs
- MCU part and its probe-rs chip name (`probe-rs chip list` prints the names).
- Core: which Cortex-M or RISC-V core, and whether it has an FPU. This picks the target triple.
- Flash and RAM origin and size from the datasheet (for `memory.x`).
- Concurrency model: plain `#[entry]` loop, RTIC, or Embassy.
- Debug probe on hand, or none (this picks the panic handler and the `defmt` transport).
## Procedure
1. Pick the target triple from the core and install it. Done when: `rustup target add <triple>` succeeds and the triple appears in `rustc --print target-list`. The full table is in `references/embedded-rust-targets.md`.
| Core | Target triple |
|---|---|
| Cortex-M0, M0+ | `thumbv6m-none-eabi` |
| Cortex-M3 | `thumbv7m-none-eabi` |
| Cortex-M4, M7 without FPU | `thumbv7em-none-eabi` |
| Cortex-M4F, M7F | `thumbv7em-none-eabihf` |
| Cortex-M33 with FPU | `thumbv8m.main-none-eabihf` |
| RISC-V RV32IMAC | `riscv32imac-unknown-none-elf` |
2. Write `Cargo.toml` and `.cargo/config.toml`. Use edition 2024. The versions below are the current crates.io releases on 2026-09-05; run `cargo add <crate>` to take the current one rather than copying a number. `debug = true` in the release profile keeps DWARF for `defmt` and `probe-rs`; it does not change the flashed code size because debug info is not loaded to flash. Done when: `cargo build --release` links.
```toml
# Cargo.toml
[package]
name = "my-firmware"
version = "0.1.0"
edition = "2024"
[dependencies]
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
defmt = "1"
defmt-rtt = "1"
panic-probe = { version = "1", features = ["print-defmt"] }
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
debug = true
```
```toml
# .cargo/config.toml
[build]
target = "thumbv7em-none-eabihf"
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F411CEUx"
rustflags = ["-C", "link-arg=-Tlink.x"]
```
`link.x` is the linker script `cortex-m-rt` generates; it includes your `memory.x`:
```
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 512K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}
```
3. Write the minimal program. `#![no_std]` drops the standard library, `#![no_main]` hands the entry point to `cortex-m-rt`, and the two `as _` imports link the RTT transport and the panic handler without naming them. Done when: the program builds and `cortex_m::Peripherals::take()` is called at most once.
```rust
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use defmt::info;
use defmt_rtt as _;
use panic_probe as _;
#[entry]
fn main() -> ! {
info!("boot");
let _core = cortex_m::Peripherals::take().unwrap();
loop {
info!("tick");
cortex_m::asm::delay(8_000_000);
}
}
```
4. Flash and stream logs with probe-rs. `probe-rs run` flashes, resets, and prints RTT and `defmt` output; `probe-rs attach` connects without reset or flash and keeps the running state. Done when: `cargo run --release` prints the `info!` lines.
```bash
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh
probe-rs list # connected probes
probe-rs chip list | grep -i stm32 # chip names for --chip
cargo run --release # build, flash, stream defmt
probe-rs attach --chip STM32F411CEUx target/thumbv7em-none-eabihf/release/my-firmware
```
If `probe-rs run` fails to find a probe or chip, read `probe-rs run --help` and `probe-rs list` before changing the config.
5. Log with defmt. `defmt` sends an interned string index plus raw arguments; the host decodes them from the ELF, so the ELF that is running must be the one the host reads. Done when: a `#[derive(Format)]` type prints through `info!("{:?}", value)`.
```rust
use defmt::{Format, error, info, warn};
#[derive(Format)]
struct Packet { id: u8, len: u16 }
info!("temperature {} C", temp);
warn!("stack {}/{}", used, total);
error!("i2c {:?}", err);
defmt::assert_eq!(result, expected);
```
Transport: `defmt-rtt` needs a probe attached and is the default. `defmt-semihosting` works through a GDB or OpenOCD semihosting channel and is slower; use it when RTT is unavailable.
6. For interrupt-driven concurrency, use RTIC 2. Tasks with `binds` are hardware interrupt handlers; software tasks run on the dispatcher interrupts you list. Shared resources are locked, so RTIC proves no data race at compile time. Done when: the RTIC app compiles and the bound ISR fires on the hardware event.
```rust
#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true, dispatchers = [SPI1])]
mod app {
use defmt::info;
#[shared]
struct Shared { counter: u32 }
#[local]
struct Local {}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
periodic::spawn().unwrap();
(Shared { counter: 0 }, Local {})
}
#[task(shared = [counter])]
async fn periodic(mut cx: periodic::Context) {
loop {
let n = cx.shared.counter.lock(|c| { *c += 1; *c });
info!("count {}", n);
rtic_monotonics::systick::Systick::delay(500.millis()).await;
}
}
#[task(binds = EXTI0, priority = 2)]
fn button(_cx: button::Context) {
info!("button");
}
}
```
Cargo dependencies for this: `rtic` with the `thumbv7-backend` feature and `rtic-monotonics` with the `cortex-m-systick` feature. Embassy (`embassy-executor`) is the async alternative; pick one executor per binary.
7. Pick exactly one panic handler. Two handlers produce a duplicate `#[panic_handler]` link error. Done when: `Cargo.toml` lists one of the crates below and the build links.
| Crate | Behavior | Use when |
|---|---|---|
| `panic-halt` | Infinite loop | Production without a probe |
| `panic-probe` | Prints the message through `defmt`, then a breakpoint | Development with probe-rs |
| `panic-semihosting` | Prints through semihosting | Development under GDB or OpenOCD |
| `panic-reset` | Resets the core | Recovery where a watchdog would reset anyway |
## Failure and recovery
| Symptom | Cause | Fix |
|---|---|---|
| `can't find crate for core` | Target not installed | `rustup target add <triple>`. |
| Link error naming `memory.x` or `_stack_start` | `memory.x` missing or not on the linker search path | Put `memory.x` next to `Cargo.toml`, or emit its directory from `build.rs` with `cargo:rustc-link-search`. |
| Duplicate `#[panic_handler]` | Two panic crates linked | Keep one. |
| No `defmt` output | Host reads a different ELF than the one flashed, or RTT is not linked | Rebuild and flash in one `cargo run`; keep `use defmt_rtt as _;`. |
| `probe-rs` reports no probe | USB permissions or no udev rule | Run `probe-rs list`; install the udev rules from the probe-rs docs. |
| HardFault at boot | Wrong `MEMORY` origins or FPU triple on a core without FPU | Check `memory.x` against the datasheet and the triple against the core. |
## Output
A project directory that builds for the target triple, flashes with `cargo run --release`, streams `defmt` logs, and links one panic handler, plus a note naming the target triple and the probe-rs chip name that were used.