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}
48
49impl<'a> TierSpec<'a> {
50    /// A degenerate single tier: wildcard groups, normal priority, the
51    /// board's default stack. Equivalent to today's single-task entry.
52    pub const fn single() -> TierSpec<'static> {
53        TierSpec {
54            name: "default",
55            groups: &[],
56            priority: 0,
57            stack_bytes: 0,
58            spin_period_us: 1_000,
59        }
60    }
61}
62
63/// FreeRTOS native priority (0..=`configMAX_PRIORITIES-1`, here 0–7)
64/// for a normalized 0–31 priority. RFC-0016 §Design: linear
65/// interpolation `(n*7 + 15) / 31` (round-to-nearest), so 0→0 (idle)
66/// and 31→7 (highest). Higher number = higher priority on FreeRTOS.
67pub const fn freertos_priority_for(normalized: u8) -> u8 {
68    let n = clamp31(normalized) as u32;
69    ((n * 7 + 15) / 31) as u8
70}
71
72/// ThreadX native priority (0..=31, **lower = higher priority**) for a
73/// normalized 0–31 priority. RFC-0016: inverted scale `31 - n`, so the
74/// normalized idle (0) maps to ThreadX 31 (lowest) and normalized
75/// critical (31) maps to ThreadX 0 (highest).
76pub const fn threadx_priority_for(normalized: u8) -> u8 {
77    31 - clamp31(normalized)
78}
79
80/// POSIX `nice` value (`-20`..=`19`, **lower = more CPU**) for a
81/// normalized 0–31 priority. Best-effort: native preemption normally
82/// uses the default scheduler (strict ordering needs `SCHED_FIFO` +
83/// privileges), so this is an advisory niceness, linear over the scale
84/// and clamped, with idle (0) pinned to the maximum `19`. Anchors track
85/// the RFC-0016 table (12→0 normal, 31→-20 critical).
86pub const fn posix_nice_for(normalized: u8) -> i32 {
87    let n = clamp31(normalized) as i32;
88    if n == 0 {
89        return 19;
90    }
91    // Slope ≈ -1.25 nice/step around the normal anchor (n=12 → 0).
92    let nice = (-5 * (n - 12)) / 4;
93    if nice > 19 {
94        19
95    } else if nice < -20 {
96        -20
97    } else {
98        nice
99    }
100}
101
102const fn clamp31(n: u8) -> u8 {
103    if n > 31 { 31 } else { n }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn freertos_anchors_match_rfc0016() {
112        // RFC-0016 table column FreeRTOS(0–7).
113        assert_eq!(freertos_priority_for(0), 0); // idle
114        assert_eq!(freertos_priority_for(12), 3); // normal
115        assert_eq!(freertos_priority_for(20), 5); // high
116        assert_eq!(freertos_priority_for(31), 7); // critical
117        // Saturates above the scale.
118        assert_eq!(freertos_priority_for(200), 7);
119    }
120
121    #[test]
122    fn threadx_inverts_scale() {
123        assert_eq!(threadx_priority_for(0), 31); // idle → lowest
124        assert_eq!(threadx_priority_for(31), 0); // critical → highest
125        assert_eq!(threadx_priority_for(12), 19);
126    }
127
128    #[test]
129    fn posix_nice_anchors() {
130        assert_eq!(posix_nice_for(0), 19); // idle pinned to max nice
131        assert_eq!(posix_nice_for(12), 0); // normal
132        assert_eq!(posix_nice_for(31), -20); // critical (clamped)
133        assert!(posix_nice_for(20) < 0); // high → negative nice
134    }
135
136    #[test]
137    fn single_tier_is_wildcard() {
138        let t = TierSpec::single();
139        assert!(t.groups.is_empty());
140        assert_eq!(t.priority, 0);
141    }
142}