Skip to main content

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). Its Embassy
54/// sibling, ~~`EmbassyBoardEntry`~~, was retired with the `nros::main!`
55/// Embassy branch (issue 2134).
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-521 W6 (RFC-0113 D4) — the network link's receive interrupt.
160    /// Invoked from the proc-macro-emitted `#[task(binds = <rx_irq>,
161    /// priority = 2)]` hardware task. It masks or acknowledges the source;
162    /// the interrupt itself is what ends the run task's park (a `wfi` in the
163    /// port's wake object), so an arrival is dispatched when it lands rather
164    /// than at the next tick.
165    ///
166    /// Default: no-op, for boards whose `RticBoardSpec` declares no `rx_irq`.
167    fn on_rx_irq() {}
168
169    /// Phase 289 (#178 layer 2) — called once at the top of the
170    /// `__nros_run` task, after `#[init]` returned and interrupts unmasked,
171    /// BEFORE [`open_executor`](Self::open_executor). The place to install
172    /// idle-yield hooks that require a live IRQ source (e.g. the mps2
173    /// board's `enable_wfi_idle()`, which makes the zenoh connect busy-wait
174    /// `wfi` between iterations so host-timed slirp packets can arrive under
175    /// QEMU `-icount`). Installing `wfi` with no armed IRQ deadlocks — the
176    /// tick task exists precisely so this hook is safe to run here.
177    ///
178    /// Default: no-op.
179    fn on_interrupts_live() {}
180}
181
182#[cfg(test)]
183mod tests {
184    //! Compile-time smoke test: a dummy `RticBoardEntry` impl wires
185    //! through every assoc type / const slot and the `Board`
186    //! super-trait chain (`BoardInit + BoardPrint + BoardExit`). The
187    //! impl is never invoked at runtime — the test is purely about
188    //! the trait surface accepting a real-shaped board type without
189    //! any extra bounds creep.
190    use super::*;
191    use crate::board::{BoardExit, BoardInit, BoardPrint, NodeDispatchRuntime};
192
193    struct DummyPac;
194    struct DummyCore;
195    struct DummyExecutor;
196    struct DummyRuntime;
197    struct DummyBoard;
198
199    impl BoardInit for DummyBoard {
200        fn init_hardware() {}
201    }
202    impl BoardPrint for DummyBoard {
203        fn println(_args: core::fmt::Arguments<'_>) {}
204    }
205    impl BoardExit for DummyBoard {
206        fn exit_success() -> ! {
207            // Test impl — never executed; the trait surface only
208            // requires the signature.
209            loop {
210                core::hint::spin_loop();
211            }
212        }
213        fn exit_failure() -> ! {
214            loop {
215                core::hint::spin_loop();
216            }
217        }
218    }
219
220    impl NodeDispatchRuntime for DummyRuntime {
221        fn spin_once(&mut self, _timeout_ms: u32) -> Result<(), ()> {
222            Err(())
223        }
224    }
225
226    struct DummyBoot;
227
228    impl RticBoardEntry for DummyBoard {
229        type Pac = DummyPac;
230        type Core = DummyCore;
231        type Executor = DummyExecutor;
232        type Runtime = DummyRuntime;
233        type Boot = DummyBoot;
234
235        const DISPATCHERS: &'static [&'static str] = &["USART1", "USART2"];
236
237        fn init_hardware(_device: Self::Pac, _core: Self::Core) -> (Self::Boot, Self::Runtime) {
238            (DummyBoot, DummyRuntime)
239        }
240
241        fn open_executor(_boot: Self::Boot) -> Self::Executor {
242            DummyExecutor
243        }
244    }
245
246    #[test]
247    fn dummy_board_satisfies_rtic_board_entry() {
248        // Trait-method call confirms the assoc types + const slot
249        // line up. We never spin the returned pair.
250        let (boot, _rt) = <DummyBoard as RticBoardEntry>::init_hardware(DummyPac, DummyCore);
251        let _exec = <DummyBoard as RticBoardEntry>::open_executor(boot);
252        assert_eq!(<DummyBoard as RticBoardEntry>::DISPATCHERS.len(), 2);
253    }
254}