nros_platform/board/rtic_entry.rs
1//! [`RticBoardEntry`] — Phase 216.B.1.
2//!
3//! Sibling to [`super::BoardEntry`] for **framework-owned-spin**
4//! boards. Where `BoardEntry::run` owns the boot lifecycle and
5//! drives the executor itself, `RticBoardEntry` hands the runtime
6//! over to RTIC: the `nros::main!()` proc-macro (216.B.3) generates a
7//! `#[rtic::app]` module that calls [`RticBoardEntry::init_hardware`]
8//! from inside the framework-generated `#[init]` body, stashes the
9//! returned `(Executor, Runtime)` pair in `#[local]` storage, and
10//! lets RTIC's interrupt-driven scheduler drive dispatch.
11//!
12//! ```ignore
13//! impl RticBoardEntry for RticStm32F4 {
14//! type Pac = stm32f4xx_hal::pac::Peripherals;
15//! type Core = cortex_m::Peripherals;
16//! type Executor = nros::Executor;
17//! type Runtime = RticRuntime;
18//!
19//! const DISPATCHERS: &'static [&'static str] = &["USART1", "USART2"];
20//!
21//! fn init_hardware(
22//! device: Self::Pac,
23//! core: Self::Core,
24//! ) -> (Self::Executor, Self::Runtime) {
25//! // … clock / pin / transport bringup, build Executor + Runtime …
26//! }
27//! }
28//! ```
29//!
30//! ## Layering note
31//!
32//! `nros-platform` sits **below** `nros` in the dep graph (`nros`
33//! depends on `nros-platform`, not the other way around). That
34//! forces two abstractions here:
35//!
36//! 1. [`RticBoardEntry::Executor`] is an opaque assoc type — concrete
37//! board impls plug in `nros::Executor`, but the trait surface
38//! cannot name it without inverting the dep graph.
39//! 2. [`RticBoardEntry::Core`] is an opaque assoc type for the same
40//! reason against `cortex_m`. Every Cortex-M chip board will
41//! pick `cortex_m::Peripherals`, but pulling `cortex_m` into
42//! `nros-platform` would force the dep on every consumer
43//! (POSIX, Zephyr, FreeRTOS, …) that has no use for it.
44
45use super::{Board, DeployOverlay, runtime::NodeDispatchRuntime};
46
47/// Board-side hook for RTIC integration. The `nros::main!()`
48/// proc-macro (216.B.3) generates a `#[rtic::app]` module that calls
49/// [`Self::init_hardware`] from inside the framework-generated
50/// `#[init]` body and wires the returned pair into RTIC `#[local]`
51/// storage.
52///
53/// Distinct from [`super::BoardEntry`] (board-owns-spin) and
54/// [planned] `EmbassyBoardEntry` (216.C.1, executor-owns-spin via
55/// `embassy_executor::Spawner`).
56pub trait RticBoardEntry: Board {
57 /// Chip Peripheral Access Crate handle (e.g.
58 /// `stm32f4xx_hal::pac::Peripherals`). Whatever the RTIC
59 /// `#[rtic::app(device = …)]` attribute expects as the `device`
60 /// peripheral struct.
61 type Pac: 'static;
62
63 /// Core peripheral handle. Typically `cortex_m::Peripherals` on
64 /// Cortex-M chips but kept abstract so `nros-platform` doesn't
65 /// take a transitive `cortex_m` dep that every POSIX / Zephyr /
66 /// RTOS consumer would inherit.
67 type Core: 'static;
68
69 /// Executor type the board hands back from
70 /// [`open_executor`](Self::open_executor). Concrete board impls plug in
71 /// `nros::Executor`; the assoc type keeps the layering clean
72 /// (`nros-platform` does not depend on `nros`). The proc-macro drives the
73 /// opened executor's spin loop in the `__nros_run` task.
74 type Executor: 'static;
75
76 /// #178 — hardware-ready deferred-open carrier returned by
77 /// [`init_hardware`](Self::init_hardware). Holds whatever the board needs
78 /// to open the executor later (locator / domain / node-name — all
79 /// `'static`), but performs **no** blocking network I/O itself.
80 ///
81 /// The split exists because `Executor::open` does a **blocking** zenoh
82 /// session open (a TCP connect driven by the platform poll loop, which
83 /// needs the timer tick + RX interrupt), and RTIC runs `#[init]` with
84 /// interrupts masked. The proc-macro stashes this carrier in RTIC
85 /// `#[local]` storage from `#[init]`, then the `__nros_run` task calls
86 /// [`open_executor`](Self::open_executor) on its first poll — after `init`
87 /// returns and interrupts unmask.
88 type Boot: 'static;
89
90 /// Dispatch sink the proc-macro wires into RTIC `#[local]`
91 /// storage. Required to implement
92 /// [`NodeDispatchRuntime`] so signaled callbacks queued from
93 /// RTIC tasks reach the registered Node pkgs.
94 ///
95 /// Per Phase 216.A.2, `NodeDispatchRuntime` already carries
96 /// `signal_callback` + `dispatch_strategy`; the RTIC runtime
97 /// impl uses `DispatchStrategy::Deferred` and routes signals
98 /// through a `heapless::spsc::Producer` into an RTIC software
99 /// task (see Phase 216.B.2).
100 type Runtime: NodeDispatchRuntime + 'static;
101
102 /// RTIC `dispatchers = [...]` list, declared at the board layer
103 /// so each chip pins its own interrupt slots (e.g. `&["USART1",
104 /// "USART2"]`). The proc-macro splices this into the generated
105 /// `#[rtic::app(dispatchers = …)]` attribute.
106 const DISPATCHERS: &'static [&'static str];
107
108 /// Run from inside the proc-macro-generated `#[init]` body.
109 /// Brings up clock / pin / transport hardware and splits the dispatch
110 /// SPSC, then returns the `(Boot, Runtime)` pair the macro stashes in
111 /// RTIC `#[local]` storage. #178 — this must NOT open the executor (that
112 /// blocking connect is deferred to [`open_executor`](Self::open_executor),
113 /// called from the `__nros_run` task where interrupts are live).
114 fn init_hardware(device: Self::Pac, core: Self::Core) -> (Self::Boot, Self::Runtime);
115
116 /// Like [`init_hardware`](Self::init_hardware) but applies a deploy-metadata
117 /// overlay (Phase 244.D1) to the board's compiled-in net/locator `Config`
118 /// before opening the executor. `nros::main!()` calls THIS from the
119 /// generated `#[init]` body, passing the
120 /// `[package.metadata.nros.deploy.<board>]` block.
121 ///
122 /// The default ignores `deploy` and forwards to
123 /// [`init_hardware`](Self::init_hardware), so existing RTIC boards are
124 /// unchanged. Boards with a baked net `Config` (the bare-metal firmware
125 /// boards) override it so each Entry pkg can pin its own ip / locator /
126 /// gateway — required when two RTIC firmwares share one board on the same
127 /// QEMU network (e.g. the talker-rtic / listener-rtic pub/sub pair).
128 fn init_hardware_with_deploy(
129 device: Self::Pac,
130 core: Self::Core,
131 _deploy: &DeployOverlay,
132 ) -> (Self::Boot, Self::Runtime) {
133 <Self as RticBoardEntry>::init_hardware(device, core)
134 }
135
136 /// #178 — open the executor from the [`Boot`](Self::Boot) carrier.
137 ///
138 /// This performs the **blocking** zenoh session open (`Executor::open`),
139 /// so it MUST be called from the `__nros_run` task, NOT `#[init]`:
140 /// RTIC masks interrupts during `#[init]`, which starves the platform
141 /// poll loop (no timer tick / RX IRQ) and deadlocks the TCP handshake.
142 /// The proc-macro calls this on the task's first poll, once `init` has
143 /// returned and interrupts are unmasked.
144 fn open_executor(boot: Self::Boot) -> Self::Executor;
145
146 /// Phase 289 (#178 layer 3) — clear + re-arm the board's periodic tick
147 /// IRQ. Invoked from the proc-macro-emitted
148 /// `#[task(binds = <tick_irq>, priority = 2)]` hardware task, whose only
149 /// job is waking the `wfi` inside `__nros_run`'s connect/poll busy-waits.
150 /// The board arms the timer itself in
151 /// [`init_hardware`](Self::init_hardware) (it owns the PAC); this hook
152 /// only handles the per-interrupt acknowledge. An unacknowledged flag is
153 /// an IRQ storm that starves the priority-1 run task — always clear it.
154 ///
155 /// Default: no-op, for boards whose `RticBoardSpec` declares no
156 /// `tick_irq` (the macro then emits no tick task at all).
157 fn on_tick() {}
158
159 /// Phase 289 (#178 layer 2) — called once at the top of the
160 /// `__nros_run` task, after `#[init]` returned and interrupts unmasked,
161 /// BEFORE [`open_executor`](Self::open_executor). The place to install
162 /// idle-yield hooks that require a live IRQ source (e.g. the mps2
163 /// board's `enable_wfi_idle()`, which makes the zenoh connect busy-wait
164 /// `wfi` between iterations so host-timed slirp packets can arrive under
165 /// QEMU `-icount`). Installing `wfi` with no armed IRQ deadlocks — the
166 /// tick task exists precisely so this hook is safe to run here.
167 ///
168 /// Default: no-op.
169 fn on_interrupts_live() {}
170}
171
172#[cfg(test)]
173mod tests {
174 //! Compile-time smoke test: a dummy `RticBoardEntry` impl wires
175 //! through every assoc type / const slot and the `Board`
176 //! super-trait chain (`BoardInit + BoardPrint + BoardExit`). The
177 //! impl is never invoked at runtime — the test is purely about
178 //! the trait surface accepting a real-shaped board type without
179 //! any extra bounds creep.
180 use super::*;
181 use crate::board::{BoardExit, BoardInit, BoardPrint, NodeDispatchRuntime};
182
183 struct DummyPac;
184 struct DummyCore;
185 struct DummyExecutor;
186 struct DummyRuntime;
187 struct DummyBoard;
188
189 impl BoardInit for DummyBoard {
190 fn init_hardware() {}
191 }
192 impl BoardPrint for DummyBoard {
193 fn println(_args: core::fmt::Arguments<'_>) {}
194 }
195 impl BoardExit for DummyBoard {
196 fn exit_success() -> ! {
197 // Test impl — never executed; the trait surface only
198 // requires the signature.
199 loop {
200 core::hint::spin_loop();
201 }
202 }
203 fn exit_failure() -> ! {
204 loop {
205 core::hint::spin_loop();
206 }
207 }
208 }
209
210 impl NodeDispatchRuntime for DummyRuntime {
211 fn spin_once(&mut self, _timeout_ms: u32) -> Result<(), ()> {
212 Err(())
213 }
214 }
215
216 struct DummyBoot;
217
218 impl RticBoardEntry for DummyBoard {
219 type Pac = DummyPac;
220 type Core = DummyCore;
221 type Executor = DummyExecutor;
222 type Runtime = DummyRuntime;
223 type Boot = DummyBoot;
224
225 const DISPATCHERS: &'static [&'static str] = &["USART1", "USART2"];
226
227 fn init_hardware(_device: Self::Pac, _core: Self::Core) -> (Self::Boot, Self::Runtime) {
228 (DummyBoot, DummyRuntime)
229 }
230
231 fn open_executor(_boot: Self::Boot) -> Self::Executor {
232 DummyExecutor
233 }
234 }
235
236 #[test]
237 fn dummy_board_satisfies_rtic_board_entry() {
238 // Trait-method call confirms the assoc types + const slot
239 // line up. We never spin the returned pair.
240 let (boot, _rt) = <DummyBoard as RticBoardEntry>::init_hardware(DummyPac, DummyCore);
241 let _exec = <DummyBoard as RticBoardEntry>::open_executor(boot);
242 assert_eq!(<DummyBoard as RticBoardEntry>::DISPATCHERS.len(), 2);
243 }
244}