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 listprints 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
defmttransport).
Procedure
Pick the target triple from the core and install it. Done when:
rustup target add <triple>succeeds and the triple appears inrustc --print target-list. The full table is inreferences/embedded-rust-targets.md.Core Target triple Cortex-M0, M0+ thumbv6m-none-eabiCortex-M3 thumbv7m-none-eabiCortex-M4, M7 without FPU thumbv7em-none-eabiCortex-M4F, M7F thumbv7em-none-eabihfCortex-M33 with FPU thumbv8m.main-none-eabihfRISC-V RV32IMAC riscv32imac-unknown-none-elfWrite
Cargo.tomland.cargo/config.toml. Use edition 2024. The versions below are the current crates.io releases on 2026-09-05; runcargo add <crate>to take the current one rather than copying a number.debug = truein the release profile keeps DWARF fordefmtandprobe-rs; it does not change the flashed code size because debug info is not loaded to flash. Done when:cargo build --releaselinks.# 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# .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.xis the linker scriptcortex-m-rtgenerates; it includes yourmemory.x:MEMORY { FLASH : ORIGIN = 0x08000000, LENGTH = 512K RAM : ORIGIN = 0x20000000, LENGTH = 128K }Write the minimal program.
#![no_std]drops the standard library,#![no_main]hands the entry point tocortex-m-rt, and the twoas _imports link the RTT transport and the panic handler without naming them. Done when: the program builds andcortex_m::Peripherals::take()is called at most once.#![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); } }Flash and stream logs with probe-rs.
probe-rs runflashes, resets, and prints RTT anddefmtoutput;probe-rs attachconnects without reset or flash and keeps the running state. Done when:cargo run --releaseprints theinfo!lines.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-firmwareIf
probe-rs runfails to find a probe or chip, readprobe-rs run --helpandprobe-rs listbefore changing the config.Log with defmt.
defmtsends 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 throughinfo!("{:?}", value).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-rttneeds a probe attached and is the default.defmt-semihostingworks through a GDB or OpenOCD semihosting channel and is slower; use it when RTT is unavailable.For interrupt-driven concurrency, use RTIC 2. Tasks with
bindsare 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.#[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:
rticwith thethumbv7-backendfeature andrtic-monotonicswith thecortex-m-systickfeature. Embassy (embassy-executor) is the async alternative; pick one executor per binary.Pick exactly one panic handler. Two handlers produce a duplicate
#[panic_handler]link error. Done when:Cargo.tomllists one of the crates below and the build links.Crate Behavior Use when panic-haltInfinite loop Production without a probe panic-probePrints the message through defmt, then a breakpointDevelopment with probe-rs panic-semihostingPrints through semihosting Development under GDB or OpenOCD panic-resetResets 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.