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. Concrete board impls plug
70 /// in `nros::Executor`; the assoc type keeps the layering clean
71 /// (`nros-platform` does not depend on `nros`). The proc-macro
72 /// stashes this value in RTIC `#[local]` storage.
73 type Executor: 'static;
74
75 /// Dispatch sink the proc-macro wires into RTIC `#[local]`
76 /// storage. Required to implement
77 /// [`NodeDispatchRuntime`] so signaled callbacks queued from
78 /// RTIC tasks reach the registered Node pkgs.
79 ///
80 /// Per Phase 216.A.2, `NodeDispatchRuntime` already carries
81 /// `signal_callback` + `dispatch_strategy`; the RTIC runtime
82 /// impl uses `DispatchStrategy::Deferred` and routes signals
83 /// through a `heapless::spsc::Producer` into an RTIC software
84 /// task (see Phase 216.B.2).
85 type Runtime: NodeDispatchRuntime + 'static;
86
87 /// RTIC `dispatchers = [...]` list, declared at the board layer
88 /// so each chip pins its own interrupt slots (e.g. `&["USART1",
89 /// "USART2"]`). The proc-macro splices this into the generated
90 /// `#[rtic::app(dispatchers = …)]` attribute.
91 const DISPATCHERS: &'static [&'static str];
92
93 /// Run from inside the proc-macro-generated `#[init]` body.
94 /// Returns the `(Executor, Runtime)` pair the macro stashes in
95 /// RTIC `#[local]` storage. The board impl owns clock / pin /
96 /// transport bringup before constructing the executor + runtime
97 /// pair.
98 fn init_hardware(device: Self::Pac, core: Self::Core) -> (Self::Executor, Self::Runtime);
99
100 /// Like [`init_hardware`](Self::init_hardware) but applies a deploy-metadata
101 /// overlay (Phase 244.D1) to the board's compiled-in net/locator `Config`
102 /// before opening the executor. `nros::main!()` calls THIS from the
103 /// generated `#[init]` body, passing the
104 /// `[package.metadata.nros.deploy.<board>]` block.
105 ///
106 /// The default ignores `deploy` and forwards to
107 /// [`init_hardware`](Self::init_hardware), so existing RTIC boards are
108 /// unchanged. Boards with a baked net `Config` (the bare-metal firmware
109 /// boards) override it so each Entry pkg can pin its own ip / locator /
110 /// gateway — required when two RTIC firmwares share one board on the same
111 /// QEMU network (e.g. the talker-rtic / listener-rtic pub/sub pair).
112 fn init_hardware_with_deploy(
113 device: Self::Pac,
114 core: Self::Core,
115 _deploy: &DeployOverlay,
116 ) -> (Self::Executor, Self::Runtime) {
117 <Self as RticBoardEntry>::init_hardware(device, core)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 //! Compile-time smoke test: a dummy `RticBoardEntry` impl wires
124 //! through every assoc type / const slot and the `Board`
125 //! super-trait chain (`BoardInit + BoardPrint + BoardExit`). The
126 //! impl is never invoked at runtime — the test is purely about
127 //! the trait surface accepting a real-shaped board type without
128 //! any extra bounds creep.
129 use super::*;
130 use crate::board::{BoardExit, BoardInit, BoardPrint, NodeDispatchRuntime};
131
132 struct DummyPac;
133 struct DummyCore;
134 struct DummyExecutor;
135 struct DummyRuntime;
136 struct DummyBoard;
137
138 impl BoardInit for DummyBoard {
139 fn init_hardware() {}
140 }
141 impl BoardPrint for DummyBoard {
142 fn println(_args: core::fmt::Arguments<'_>) {}
143 }
144 impl BoardExit for DummyBoard {
145 fn exit_success() -> ! {
146 // Test impl — never executed; the trait surface only
147 // requires the signature.
148 loop {}
149 }
150 fn exit_failure() -> ! {
151 loop {}
152 }
153 }
154
155 impl NodeDispatchRuntime for DummyRuntime {
156 fn spin_once(&mut self, _timeout_ms: u32) -> Result<(), ()> {
157 Err(())
158 }
159 }
160
161 impl RticBoardEntry for DummyBoard {
162 type Pac = DummyPac;
163 type Core = DummyCore;
164 type Executor = DummyExecutor;
165 type Runtime = DummyRuntime;
166
167 const DISPATCHERS: &'static [&'static str] = &["USART1", "USART2"];
168
169 fn init_hardware(_device: Self::Pac, _core: Self::Core) -> (Self::Executor, Self::Runtime) {
170 (DummyExecutor, DummyRuntime)
171 }
172 }
173
174 #[test]
175 fn dummy_board_satisfies_rtic_board_entry() {
176 // Trait-method call confirms the assoc types + const slot
177 // line up. We never spin the returned pair.
178 let (_exec, _rt) = <DummyBoard as RticBoardEntry>::init_hardware(DummyPac, DummyCore);
179 assert_eq!(<DummyBoard as RticBoardEntry>::DISPATCHERS.len(), 2);
180 }
181}