nros_platform/board/tier.rs
1//! Per-tier scheduling descriptors — Phase 228.E (RFC-0015 execution
2//! model, RFC-0016 priority mapping).
3//!
4//! A [`TierSpec`] names one RTOS task that an `Executor` will run on a
5//! shared RMW session. The orchestration `main()` (codegen-emitted)
6//! passes a `&[TierSpec]` to the board's `run_tiers(...)`; the board
7//! opens the session once, then spawns one task per spec — each task
8//! opens an `Executor` over the *same* session (the `Borrowed` store),
9//! sets its `active_groups` filter, registers nodes (only its tier's
10//! callbacks take), and spins. The highest-priority tier runs on the
11//! boot task itself; the rest are spawned.
12//!
13//! Priorities are declared on a normalized **0–31** scale (RFC-0016):
14//! 0 = idle, 12 = normal (default app), 31 = critical. The per-RTOS
15//! mappers below lower that to each kernel's native range. Keeping the
16//! scale RTOS-agnostic lets the same `system.toml [tiers.*]` deploy
17//! across families without rewriting priorities.
18
19/// One scheduling tier: an RTOS task running an `Executor` over the
20/// shared session, admitting only the listed callback groups.
21///
22/// All fields are literal-constructible so the codegen emitter can bake
23/// a `const`/`static` array of these straight from the resolved tier
24/// table in `nros-plan.json`.
25#[derive(Clone, Copy, Debug)]
26pub struct TierSpec<'a> {
27 /// Tier name (matches the `system.toml [tiers.<name>]` key); used
28 /// for the spawned task's debug name.
29 pub name: &'a str,
30 /// Callback groups admitted on this tier. Passed verbatim to
31 /// `Executor::set_active_groups`; an empty slice = wildcard
32 /// (admit every group — the single-tier degenerate case).
33 pub groups: &'a [&'a str],
34 /// **Raw per-RTOS** task priority — the value passed straight to the
35 /// native spawn call. The system author writes it in
36 /// `[tiers.<name>.<rtos>].priority`, so it is already in the target
37 /// kernel's scale (FreeRTOS 0–7, ThreadX 0–31 lower=higher, …);
38 /// `i64` admits Zephyr's negative coop priorities. (The
39 /// `*_priority_for` mappers in this module are a separate utility for
40 /// authors who prefer a normalized 0–31 scale; the codegen path uses
41 /// the raw value verbatim.)
42 pub priority: i64,
43 /// Task stack size in bytes. `0` = let the board pick its default.
44 pub stack_bytes: usize,
45 /// Spin period for this tier's `spin_once` loop, in microseconds.
46 pub spin_period_us: u64,
47 // -- RFC-0052 / phase-296 W2 — the previously-dropped tier fields ride
48 // -- the spec end-to-end. Boards consume what their kernel offers; the
49 // -- bake already rejected platform-inapplicable knobs (fail-loud), so
50 // -- an unconsumed Some(..) here is a board TODO, not a silent config
51 // -- loss.
52 /// CPU core to pin the tier task to (SMP boards); `None` = unpinned.
53 pub core: Option<u32>,
54 /// ThreadX preemption threshold (ThreadX targets only; bake-validated).
55 pub preempt_threshold: Option<i64>,
56 /// Scheduling class: `"best_effort"` | `"real_time"` |
57 /// `"time_triggered"` (bake rejects `"interrupt"`); `None` = plain
58 /// priority tier.
59 pub class: Option<&'a str>,
60 /// Callback period (µs) — `time_triggered` window period / sporadic
61 /// replenishment period.
62 pub period_us: Option<u64>,
63 /// Execution-time budget (µs) — sporadic-server budget (W3 wires it
64 /// into the executor's `SchedContext`).
65 pub budget_us: Option<u64>,
66 /// Relative deadline (µs) for the deadline monitor (W3).
67 pub deadline_us: Option<u64>,
68 /// On deadline miss: `"ignore"` | `"warn"` | `"skip"` | `"fault"`.
69 pub deadline_policy: Option<&'a str>,
70}
71
72impl<'a> TierSpec<'a> {
73 /// A degenerate single tier: wildcard groups, normal priority, the
74 /// board's default stack. Equivalent to today's single-task entry.
75 pub const fn single() -> TierSpec<'static> {
76 TierSpec {
77 name: "default",
78 groups: &[],
79 priority: 0,
80 stack_bytes: 0,
81 spin_period_us: 1_000,
82 core: None,
83 preempt_threshold: None,
84 class: None,
85 period_us: None,
86 budget_us: None,
87 deadline_us: None,
88 deadline_policy: None,
89 }
90 }
91}
92
93/// FreeRTOS native priority (0..=`configMAX_PRIORITIES-1`, here 0–7)
94/// for a normalized 0–31 priority. RFC-0016 §Design: linear
95/// interpolation `(n*7 + 15) / 31` (round-to-nearest), so 0→0 (idle)
96/// and 31→7 (highest). Higher number = higher priority on FreeRTOS.
97pub const fn freertos_priority_for(normalized: u8) -> u8 {
98 let n = clamp31(normalized) as u32;
99 ((n * 7 + 15) / 31) as u8
100}
101
102/// ThreadX native priority (0..=31, **lower = higher priority**) for a
103/// normalized 0–31 priority. RFC-0016: inverted scale `31 - n`, so the
104/// normalized idle (0) maps to ThreadX 31 (lowest) and normalized
105/// critical (31) maps to ThreadX 0 (highest).
106pub const fn threadx_priority_for(normalized: u8) -> u8 {
107 31 - clamp31(normalized)
108}
109
110/// POSIX `nice` value (`-20`..=`19`, **lower = more CPU**) for a
111/// normalized 0–31 priority. Best-effort: native preemption normally
112/// uses the default scheduler (strict ordering needs `SCHED_FIFO` +
113/// privileges), so this is an advisory niceness, linear over the scale
114/// and clamped, with idle (0) pinned to the maximum `19`. Anchors track
115/// the RFC-0016 table (12→0 normal, 31→-20 critical).
116pub const fn posix_nice_for(normalized: u8) -> i32 {
117 let n = clamp31(normalized) as i32;
118 if n == 0 {
119 return 19;
120 }
121 // Slope ≈ -1.25 nice/step around the normal anchor (n=12 → 0).
122 let nice = (-5 * (n - 12)) / 4;
123 if nice > 19 {
124 19
125 } else if nice < -20 {
126 -20
127 } else {
128 nice
129 }
130}
131
132const fn clamp31(n: u8) -> u8 {
133 if n > 31 { 31 } else { n }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn freertos_anchors_match_rfc0016() {
142 // RFC-0016 table column FreeRTOS(0–7).
143 assert_eq!(freertos_priority_for(0), 0); // idle
144 assert_eq!(freertos_priority_for(12), 3); // normal
145 assert_eq!(freertos_priority_for(20), 5); // high
146 assert_eq!(freertos_priority_for(31), 7); // critical
147 // Saturates above the scale.
148 assert_eq!(freertos_priority_for(200), 7);
149 }
150
151 #[test]
152 fn threadx_inverts_scale() {
153 assert_eq!(threadx_priority_for(0), 31); // idle → lowest
154 assert_eq!(threadx_priority_for(31), 0); // critical → highest
155 assert_eq!(threadx_priority_for(12), 19);
156 }
157
158 #[test]
159 fn posix_nice_anchors() {
160 assert_eq!(posix_nice_for(0), 19); // idle pinned to max nice
161 assert_eq!(posix_nice_for(12), 0); // normal
162 assert_eq!(posix_nice_for(31), -20); // critical (clamped)
163 assert!(posix_nice_for(20) < 0); // high → negative nice
164 }
165
166 #[test]
167 fn single_tier_is_wildcard() {
168 let t = TierSpec::single();
169 assert!(t.groups.is_empty());
170 assert_eq!(t.priority, 0);
171 }
172}