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) 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}