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}