Bare-metal Cortex-M3 (QEMU)
Single-node starter on bare-metal Cortex-M3 (QEMU MPS2-AN385) —
no RTOS, no kernel scheduler. Pure cooperative spin via
zpico_spin_once. Rust only. nros-c / nros-cpp are not
supported on bare-metal targets (they assume a hosted RTOS for
startup / heap / libc); see the
examples coverage matrix
for the policy.
When to use this path: ultra-constrained Cortex-M0+ / M3 / M4 targets with no OS scheduler, no
pthread. If you have FreeRTOS or any RTOS, use the FreeRTOS starter instead — it’s more ergonomic and produces smaller code overall.
Prereqs. Install the
nrosCLI once per machine, then provision this board.nros setupfetches a prebuilt bare-metal toolchain (arm-none-eabi-gcc,qemu-system-arm, the zenoh router) plus the Rustthumbv7m-none-eabitarget into a shared store — no manual cross-compiler install, no ROS 2 needed.
# Build the in-tree nros CLI (Phase 218):
source ./activate.sh # OR: direnv allow / source ./activate.fish
just setup-cli # builds packages/cli/target/release/nros
# Provision the bare-metal Cortex-M3 board (zenoh RMW is the default):
nros setup qemu-arm-baremetal --rmw zenoh
Real-board variants exist too:
nros setup mps2-an385andnros setup stm32f4provision the same bare-metal toolchain for physical hardware.
Project layout
examples/qemu-arm-baremetal/rust/talker/
├── Cargo.toml
├── .cargo/config.toml # target = thumbv7m-none-eabi
│ # runner = qemu-system-arm ... -kernel
├── nros.toml # network + zenoh
├── package.xml
├── generated/ # codegen output — build.rs runs
│ # `nros generate-rust` on first
│ # `cargo build`; gitignored.
└── src/main.rs # #[entry] fn main() -> !
The board crate is nros-board-mps2-an385 (note: no -freertos
suffix — this is the bare-metal variant) which provides:
- Cortex-M3 startup + linker script
- LAN9118 driver for smoltcp
BoardIdle::wfi()for cooperative wait
Configure
Verbatim from the in-tree
examples/qemu-arm-baremetal/rust/talker/nros.toml:
# nano-ros config — QEMU ARM bare-metal talker (direct mode).
# Read by the nros-board-mps2-an385 board crate via Config::from_toml.
[node]
domain_id = 0
# Single ethernet transport running a zenoh session. `ip` is CIDR
# (address/prefix); the locator rides the transport.
[[transport]]
kind = "ethernet"
ip = "10.0.2.10/24"
mac = "02:00:00:00:00:00"
gateway = "10.0.2.2"
rmw = "zenoh"
locator = "tcp/10.0.2.2:7450"
QEMU Slirp networking — no host TAP / bridge / sudo. The
zenohd default port is 7447; this example expects 7450 so
start the router with zenohd --listen tcp/127.0.0.1:7450
(or edit nros.toml to match zenohd’s 7447 default).
Build
cd examples/qemu-arm-baremetal/rust/talker
cargo build --release
First build (~5 min) cross-compiles all of nano-ros’s Rust deps for
thumbv7m-none-eabi. Re-builds finish in seconds.
Run
# 1. Bring up zenohd on the host (Slirp forwards 10.0.2.2:7450 → host
# 127.0.0.1:7450). The bare-metal test-fixture port is 7450, NOT
# zenohd's default 7447 — edit `nros.toml` if you want 7447 instead.
# The just recipe runs the in-tree zenohd installed by
# `nros setup ... --rmw zenoh` on that port:
just qemu zenohd &
# Or directly:
# zenohd --listen tcp/127.0.0.1:7450 --no-multicast-scouting
# 2. Boot the talker in QEMU. The `just qemu talker` recipe wraps
# qemu-system-arm with the LAN9118 networking wiring the example
# expects — it's the only working invocation for this tutorial
# (the example's `.cargo/config.toml` runner is bare `-kernel`,
# no `-nic socket,model=lan9118,…`, so a plain `cargo run` boots
# QEMU without networking):
just qemu talker
# Expected serial-over-semihosting output (per src/main.rs):
# Declaring publisher on /chatter (std_msgs/Int32)
# Publisher declared
# Published: 0
# Published: 1
# ...
# 3. Verify from stock ROS 2:
source /opt/ros/humble/setup.bash
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
# Talker publishes best-effort; stock `ros2 topic echo` defaults to
# RELIABLE, so the QoS-mismatched echo silently delivers nothing.
# Force best-effort to receive:
ros2 topic echo /chatter std_msgs/msg/Int32 --qos-reliability best_effort
QEMU exits via Ctrl-A x.
Readiness signal. Within ~15 seconds of QEMU boot (no RTOS
init delay, but smoltcp + zenoh handshake still takes a few
seconds), expect Published: 0 on semihosting stdout (the Rust
talker pre-publishes 0 before the counter advances). If no
Published: line:
zenohdnot running — talker spins on smoltcp poll until killed.- Wrong LAN9118 emulation flag —
qemu-system-armneeds-nic socket,model=lan9118,…or equivalent. The example’s.cargo/config.tomlrunner is bare-kernel(so a plaincargo runboots QEMU without networking); the LAN9118 wiring lives in thejust qemu talkerrecipe (just/qemu-baremetal.just::talker), which is the working invocation for this tutorial. If you copy the runner out, mirror those flags. - Cooperative spin starvation — if you added a long-running callback, the entire executor stalls; bare-metal has no preemption.
- See Troubleshooting — First 10 Minutes.
GitHub source
- Bare-metal talker:
examples/qemu-arm-baremetal/rust/talker/ - Board crate:
packages/boards/nros-board-mps2-an385/
Constraints to be aware of
- No
allocby default. Pureno_std+heaplessfor bounded collections. If you needalloc, opt in via theallocfeature on your board crate and supply a#[global_allocator]. - No wake primitive. Cooperative single-thread spin only; the
executor’s
nros_platform_wake_*slots returnUnsupported. - No preemption. A long-running user callback blocks every other dispatchable handle until it returns.
nros-c/nros-cppNOT supported. These wrappers assume hosted-RTOS libc + heap. Pure-Rust API only on this target.
For Cortex-M3 with an RTOS, switch to the FreeRTOS starter.
Next
- Subscriber / service / action peers under the same
examples/qemu-arm-baremetal/rust/tree. - Wake-callback opt-in: the
wake-callback(latency-probe) bench underpackages/testing/nros-bench/wake-latency-cortex-m3/shows how to feed a backend’s transport-notify into the cooperative spin loop on bare-metal. - Real hardware: same code runs on STM32F4-Discovery with a
different board crate (
nros-board-stm32f4) and a different linker script.