Skip to main content

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}