The Board Trait Family
The Board trait family is the porting surface for a new MCU or host target. It lives in packages/platform/nros-platform/src/board/ and pins the contract every board crate (nros-board-<board> or a user-authored crate in a downstream Entry pkg) implements. Earlier prototypes used nros-board-common::board_init::*; those legacy traits stay as a transition shim while the in-tree boards finish migrating.
New board crates: implement
nros-platform::board, not the legacy shim. The porting surface for new code is theBoardfamily described on this page (Board=BoardInit+BoardPrint+BoardExit, driven byBoardEntry::run). Thenros-board-common::board_inittrait family is transition-legacy — kept only so not-yet-migrated in-tree boards keep building — and must not be implemented in new board crates. Convergence onto the singlenros-platform::boardfamily is tracked in issue 0243, sequenced with the RTIC/Embassy integration work.
A board impl tells nano-ros four things: how to initialize hardware, how to print a line of text, how to terminate, and (optionally) how to bring a transport up and gate on the network. With those four pieces the BoardEntry::run driver owns the boot lifecycle, and a user Entry pkg main.rs is a ~30 LoC shim.
Where the trait family sits
nros-platform::board
│
├── Board: BoardInit + BoardPrint + BoardExit // super-trait (blanket impl)
│
│
└── BoardEntry: Board
fn run<F, E>(setup: F) -> Result<(), E>
where F: FnOnce(&mut RuntimeCtx) -> Result<(), E>;
BoardInit::init_hardware()— clock tree, pin mux, peripheral wakes. Runs once on boot before allocation. Panicking here is the same as panicking fromfn main()— no recovery.BoardPrint::println(args: core::fmt::Arguments<'_>)— emit a line. Boards wrap whatever stdout makes sense:cortex_m_semihosting::hprintln!, a vendor printf bridge, a UART writer,libc::write(STDOUT_FILENO, …), orprintk.BoardExit::{exit_success, exit_failure}() -> !— terminate cleanly (or with failure). QEMU boards callcortex_m_semihosting::debug::exit; real hardware resets or halts; POSIX shellsstd::process::exit.BoardEntry::run(setup)— the boot driver. Implementations live in the family driver crates (nros-board-{posix,freertos,threadx,…}); user Entry pkgmain.rscalls it.
Board is itself a blanket-implemented super-trait: any type that carries BoardInit + BoardPrint + BoardExit automatically satisfies Board. Concrete board crates do not impl Board directly — they impl the three sub-traits (plus whichever mixins they need).
The BoardEntry::run lifecycle
BoardEntry::run owns the full boot → user-closure → exit flow. The exact body lives in the family driver crate (e.g. nros-board-linux, nros-board-freertos); each family folds its RTOS specifics in, but the order is fixed:
BoardInit::init_hardware()— clocks, pinmux, MMIO setup.- Device bring-up — the family crate’s job, inside
run. Bring the link layer to L2 (Ethernet frames flow / WiFi associated / UART open at baud), then gate on carrier / DHCP if the board has an IP stack. There is no mixin trait for this: see the note below. - Open the executor, build a
RuntimeCtxwith overlay knobs from the launch file / CLI, and invokesetup(&mut runtime). The codegen-emittedrun_plan(runtime)body is whatsetupultimately calls. - Spin the executor to completion (or termination signal).
BoardExit::exit_success()onOk,BoardExit::exit_failure()onError any failed init step.
run returns Result<(), E> rather than ! so unit tests can drive it in a hosted process without exit() killing the test harness — but production boards still call exit_* from inside run’s body after spin returns, so in practice the caller’s Ok(()) arm is unreachable on a real target.
The setup callback is the only place user code runs inside run. Everything else is family-crate boilerplate.
Why there is no
TransportBringup/NetworkWaitmixinEarlier revisions of this page documented two mixin traits and a family-crate blanket impl that would call them:
impl<B: Board + TransportBringup + NetworkWait> BoardEntry for B { fn run … }That could not be built, and the traits were removed in phase-206 W4 (issue 1067). Two reasons, both structural:
- The blanket impl overlaps the direct
BoardEntryimpls that twelve board crates already carry. Rust’s coherence rules do not allow both.- “Skipped if the board doesn’t impl the mixin” is not expressible — Rust has no way to call a method only when the type happens to implement a trait. The order was written as if specialization existed.
Measured before removal:
TransportBringuphad zero implementations and zero call sites;NetworkWaithad one implementation and no callers, because the one place that would have called it — thenros::main!Zephyr arm — routed around it deliberately (ZephyrBoard::wait_link_upcallsstatic inlineZephyr headers with no link symbol, so the native_sim link failed).Devices are still brought up: inside
BoardEntry::run, or the family helper it delegates to. That is the contract, and it is the one that runs.
RuntimeCtx
RuntimeCtx<'a> is the per-invocation overlay context the setup callback receives:
#![allow(unused)]
fn main() {
pub struct RuntimeCtx<'a> {
pub params: &'a [(&'a str, &'a str)], // <param name=… value=…/> + -p name:=value
pub remaps: &'a [(&'a str, &'a str)], // topic/service/action renames
pub env: &'a [(&'a str, &'a str)], // env-style key/value (rarely set on embedded)
}
}
Slice-of-tuples, no_std-safe, no allocation. Codegen owns the storage and passes a &mut RuntimeCtx<'_> whose backing slices live in statics — RuntimeCtx::EMPTY is a const placeholder for launch-less single-node examples or unit tests.
Picking your transport mixins
What you implement on the transport axis depends on what link layers your board exposes:
| Board transport class | Implement | Notes |
|---|---|---|
| Ethernet (smoltcp / lwIP / NetX BSD) | driver up, then DHCP/link gate | both, in run |
| WiFi (ESP32) | same shape — association is L2, DHCP is L3 | both, in run |
| Serial UART only | open at baud | no IP, so no link gate |
| CAN / USB CDC / IVC | link layer only | no IP |
| Bridged-net (threadx-linux veth) | host kernel owns IP | probe the bridge in run |
| Native (host) | None | Host OS owns everything; the family crate’s run skips both mixins |
Boards with multiple transports compose via an internal helper (e.g. a MultiTransport newtype) rather than blanket impls — each transport’s bringup is sequential and order-sensitive (init_link before link_up, sockets only after link).
Worked example — porting a new board
Suppose you’re adding nros-board-acme-cortex-m4-eth, a Cortex-M4 with a UART for println and an MII-attached PHY routed through smoltcp. The crate sits at packages/boards/nros-board-acme-cortex-m4-eth/ and depends on nros-platform, the family crate (nros-board-freertos if FreeRTOS is the RTOS), the matching packages/drivers/<phy>-smoltcp MAC driver, and a vendor HAL crate.
// packages/boards/nros-board-acme-cortex-m4-eth/src/lib.rs
#![no_std]
use nros_platform::board::{
BoardEntry, BoardExit, BoardInit, BoardPrint,
NetworkError, TransportError, RuntimeCtx,
};
pub struct AcmeCortexM4Eth;
impl BoardInit for AcmeCortexM4Eth {
fn init_hardware() {
acme_hal::clocks::init_hse_192mhz();
acme_hal::pinmux::route_uart2();
acme_hal::pinmux::route_eth_mii();
acme_hal::eth::release_phy_reset();
}
}
impl BoardPrint for AcmeCortexM4Eth {
fn println(args: core::fmt::Arguments<'_>) {
// 256-byte stack staging buffer is enough for our log lines;
// pick whatever your UART driver wants.
let mut buf = heapless::String::<256>::new();
let _ = core::fmt::write(&mut buf, args);
let _ = buf.push('\n');
acme_hal::uart2::write_bytes(buf.as_bytes());
}
}
impl BoardExit for AcmeCortexM4Eth {
fn exit_success() -> ! { acme_hal::system::reset() }
fn exit_failure() -> ! { acme_hal::system::halt_with_blinkenlight() }
}
// Device bring-up lives in the BoardEntry::run body — usually by delegating
// to the family helper, which is where the ORDER for that RTOS is fixed:
impl BoardEntry for AcmeCortexM4Eth {
fn run<F, E>(setup: F) -> Result<(), E>
where
F: FnOnce(&mut RuntimeCtx<'_>) -> Result<(), E>,
E: core::fmt::Debug,
{
let cfg = Config::default(); // MAC / IP / netmask / gateway
nros_board_freertos::run_entry::<Self, F, E>(cfg, setup)
}
}
That’s the whole board crate. A downstream Entry pkg consumes it as:
// pkgs/robot_acme_entry/src/main.rs
use nros_board_acme_cortex_m4_eth::AcmeCortexM4Eth;
use nros_platform::board::BoardEntry;
include!(concat!(env!("OUT_DIR"), "/run_plan.rs")); // codegen-emitted
fn main() {
let _ = <AcmeCortexM4Eth as BoardEntry>::run(|runtime| {
run_plan(runtime)
});
}
See the Role reference for the Entry-pkg surface.
Family driver crates
The family crate is where the BoardEntry::run body actually lives. The kernel families with a driver crate:
nros-board-linux— native host; the reach islinux, notposix—apply_tier_affinitycallssched_setaffinitywithcpu_set_t, which libc does not define for macOS.init_transport/wait_link_upno-ops.nros-board-freertos— FreeRTOS-Kernel + lwIP;runspawns the executor task, hands DHCP to lwIP’s hook.nros-board-threadx— ThreadX + NetX BSD; same shape over NetX.nros-board-nuttx— NuttX POSIX layer;init_transportshellsifup-style logic.nros-board-zephyr— carve-out: Kconfig + DTS own BSP; the crate exposes an inherentwait_link_upover<zephyr/net/net_if.h>. The Rust staticlib cannot take overmainon Zephyr.nros-board-esp-idf— ESP-IDF component shape; WiFi association lives ininit_transport, IP lease inwait_link_up.- Direct-exec (Cortex-M / RV32, no RTOS) has no family crate: each board
implements
BoardEntry::runitself with a single-threadzp_readloop. Anros-board-bare-metalfamily driver was written for this shape and no board ever opted into it — 135 of its 161 lines were doc comment — so a cleanup deleted it.nros-board-mps2-an385is the worked reference.
Current state: the trait surface lives in
nros-platform; family driver crates and per-board shims are migrating onto it. Some in-tree boards underpackages/boards/nros-board-*still ride the legacynros-board-common::board_init::*traits — same conceptual shape, different module path.
Cross-references
- Workspace shape + how an Entry pkg consumes a board → Role reference.
- Multi-node composition root →
docs/design/0024-multi-node-workspace-layout.md. - Why the C ABI looks the way it does → Canonical Platform C ABI.
- Platform trait set vs Board trait set — these are different traits with different roles.
Platform*(clock / alloc / sockets / threading) sits below the RMW;Board*sits above the platform and owns the boot lifecycle. A bare-metal board crate typically depends on both: anros-platform-*impl for the platform traits and anros-board-*impl for the board traits.