rlvgl_platform/board_runtime/safe_stop.rs
1//! DPR-02 SafeStop — warm-reset peripheral cleanup scaffold.
2//!
3//! Per [DPR-02 §5.1][dpr02]: a [`SafeStop`] run executes the frozen
4//! 5-step ordering — NVIC mask → DMA channel disable → peripheral
5//! disable → NVIC pending clear → telemetry record — for every
6//! peripheral implied by the active [`ServiceSet`]. The full mapping
7//! from service to peripheral set, including each service's NVIC IRQ
8//! lines and timeout bit position, is in DPR-02 §5.4.
9//!
10//! Total wall-clock budget across all peripherals in a single run is
11//! ~5 ms (DPR-02 INV-DPR-2-2). A peripheral that exceeds its per-step
12//! budget (~1 ms each at step 2 / step 3) sets its bit in
13//! [`SafeStopReport::timeouts`] and the sequence proceeds — safe-stop
14//! MUST NOT block boot on a stuck peripheral.
15//!
16//! ## Scaffold status
17//!
18//! [`SafeStop::run`] is a NOP returning `SafeStopReport { timeouts: 0,
19//! elapsed_us: 0 }`. The signature is frozen by this module; the real
20//! disable sequence lands under DPR-02a phase-1 step 2+. Consumer
21//! wiring into `BoardRuntime::init` is also deferred (call-site
22//! migration matches the DPR-01a / DPR-01b precedent).
23//!
24//! ## Alignment with DPR-01 `ServiceSet`
25//!
26//! The [`ServiceSet`] bitset defined here mirrors the DPR-01 §5.1
27//! `ServiceSet` semantically — `{ audio, codec_reset, sd, qspi,
28//! mems_mic, scope_probes }`. DPR-01's own `ServiceSet` type lands in
29//! a future DPR-01a/b code PR; at that point this scaffold's
30//! `ServiceSet` either re-exports the DPR-01 type or is replaced by it,
31//! per the eventual `BoardRuntime::init` composition.
32//!
33//! [dpr02]: https://github.com/softoboros/rlvgl/blob/main/docs/concepts/DPR-02-CONCEPTS.md
34
35// ── ServiceSet ──────────────────────────────────────────────────────────
36
37/// Service bitset consumed by [`SafeStop::run`].
38///
39/// Mirrors [DPR-01 §5.1][dpr01] `ServiceSet` semantically; the
40/// authoritative bit positions ratify when the DPR-01 type lands.
41/// Bit positions chosen here are stable for DPR-02a scaffold purposes.
42///
43/// Adding a new service is **Specification Required** per DPR-01 §5.1
44/// (and DPR-02 §5.4 — the SafeStopSequence mapping table MUST be
45/// amended in the same PR per [INV-DPR-2-4][inv]).
46///
47/// [dpr01]: https://github.com/softoboros/rlvgl/blob/main/docs/concepts/DPR-01-CONCEPTS.md
48/// [inv]: https://github.com/softoboros/rlvgl/blob/main/docs/concepts/DPR-02-CONCEPTS.md
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct ServiceSet(u32);
51
52impl ServiceSet {
53 /// Empty service set — no peripherals registered.
54 pub const EMPTY: Self = Self(0);
55 /// `audio` — SAI1 Block A/B + DMA1 streams 0/1 per DPR-02 §5.4.
56 pub const AUDIO: Self = Self(1 << 0);
57 /// `codec_reset` — I2C4 transactions to the WM8994 codec per DPR-02 §5.4.
58 pub const CODEC_RESET: Self = Self(1 << 1);
59 /// `sd` — SDMMC1 + built-in IDMA per DPR-02 §5.4.
60 pub const SD: Self = Self(1 << 2);
61 /// `qspi` — QUADSPI + MDMA channel per DPR-02 §5.4.
62 pub const QSPI: Self = Self(1 << 3);
63 /// `mems_mic` — SAI4 Block A + BDMA per DPR-02 §5.4.
64 pub const MEMS_MIC: Self = Self(1 << 4);
65 /// `scope_probes` — GPIO output pins. Excluded from PeripheralServiceSet
66 /// per DPR-02 §3 (GPIO does not require disable sequencing).
67 pub const SCOPE_PROBES: Self = Self(1 << 5);
68
69 /// `true` if every bit in `other` is also set in `self`.
70 #[inline]
71 pub const fn contains(self, other: Self) -> bool {
72 (self.0 & other.0) == other.0
73 }
74
75 /// Bitwise union of two service sets.
76 #[inline]
77 pub const fn union(self, other: Self) -> Self {
78 Self(self.0 | other.0)
79 }
80
81 /// Raw bitmask. Mirrors the value persisted to the
82 /// `service_set_active` telemetry slot (DPR-02 §5.3) for the eventual
83 /// `BoardRuntime::init` composition.
84 #[inline]
85 pub const fn raw(self) -> u32 {
86 self.0
87 }
88}
89
90// ── SafeStopReport ──────────────────────────────────────────────────────
91
92/// Structured output of a [`SafeStop::run`] execution.
93///
94/// Persisted to the `safe_stop_report` telemetry slot at
95/// `0x3800_0510..0x3800_0520` per DPR-02 §5.3. The struct stores the
96/// in-RAM detail (`timeouts` + `elapsed_us`); the on-SRAM4 layout adds
97/// entry / exit sentinels (`0xB007_5A5E` / `0xB007_D000 | timeouts`)
98/// and a reserved word per DPR-02 §5.3.
99///
100/// Layout sized for two `u32` payload words — `mem::size_of` is 8.
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102#[repr(C)]
103pub struct SafeStopReport {
104 /// Per-peripheral timeout bitmask. Bit positions follow the
105 /// DPR-02 §5.4 mapping table (audio = bits 0..3, mems_mic =
106 /// bits 4..5, sd = bit 6, qspi = bit 7, codec_reset = bit 8;
107 /// 9..15 reserved for future services, 16..31 reserved for
108 /// per-peripheral sub-step timeouts).
109 pub timeouts: u32,
110 /// Total elapsed wall-clock from the DWT cycle counter, in
111 /// microseconds, across the entire safe-stop sequence. Hard upper
112 /// bound is the DPR-02 §6 INV-DPR-2-2 budget of ~5 000 µs.
113 pub elapsed_us: u32,
114}
115
116// ── SafeStop ────────────────────────────────────────────────────────────
117
118/// SafeStop — owns the warm-reset cleanup sequence.
119///
120/// Per [DPR-02 §5.5][dpr02] and the §7 API sketch: `SafeStop::run` is
121/// the single entry point, called once during `BoardRuntime::init`
122/// before the new boot's init code touches any peripheral in the
123/// active [`ServiceSet`]. The current boot's clock tree MUST NOT have
124/// been reprogrammed yet (INV-DPR-2-1).
125///
126/// The type is intentionally a zero-sized marker today. DPR-02a
127/// phase-1 step 2+ extends it to own typed `hwcore::regs::*` handles
128/// for the peripherals it disables (DMA1, SAI1, BDMA, SAI4, SDMMC1,
129/// QUADSPI, I2C4, NVIC) plus a DWT cycle-counter reference for the
130/// `elapsed_us` accumulator. The owned-handles refactor preserves the
131/// `unsafe fn run` signature.
132///
133/// [dpr02]: https://github.com/softoboros/rlvgl/blob/main/docs/concepts/DPR-02-CONCEPTS.md
134pub struct SafeStop;
135
136impl SafeStop {
137 /// Run the safe-stop sequence for every peripheral implied by
138 /// `services`, in the [DPR-02 §5.1][dpr02] frozen order. Returns
139 /// the per-peripheral [`SafeStopReport::timeouts`] bitmask and the
140 /// elapsed wall-clock from the DWT cycle counter.
141 ///
142 /// # Safety
143 ///
144 /// The caller MUST assert all of the following:
145 ///
146 /// 1. **Called exactly once during early boot** — before any
147 /// peripheral in the active [`ServiceSet`] is enabled by the
148 /// new boot's init code. The DPR-02 §5.4 mapping enumerates
149 /// which peripherals correspond to each service.
150 /// 2. **Pre-clock-tree-reprogramming** — the current boot's
151 /// `RCC` PLL / divider / kernel-clock-mux configuration MUST
152 /// NOT have been modified yet (DPR-02 §6 INV-DPR-2-1).
153 /// Reprogramming kernel clocks while a peripheral is still
154 /// running an inherited transfer can latch the peripheral into
155 /// a partially-disabled state.
156 /// 3. **No concurrent access to the named peripherals** — no
157 /// other code path is currently writing to any peripheral in
158 /// the DPR-02 §5.4 mapping for `services`. The caller's
159 /// typed-register-handle ownership discipline is what makes
160 /// this assertion safe (DPR-02a phase-1 wires this through
161 /// `hwcore::regs::*` accessors).
162 ///
163 /// ## Scaffold body
164 ///
165 /// This is **scaffold**: the body is a NOP and returns
166 /// `SafeStopReport { timeouts: 0, elapsed_us: 0 }`. The signature
167 /// is frozen here so consumer wiring in `BoardRuntime::init`
168 /// ratifies separately from the peripheral-disable sequence.
169 /// DPR-02a phase-1 step 2+ replaces the body with the real
170 /// 5-step ordering per DPR-02 §5.1.
171 ///
172 /// [dpr02]: https://github.com/softoboros/rlvgl/blob/main/docs/concepts/DPR-02-CONCEPTS.md
173 #[inline]
174 pub unsafe fn run(services: ServiceSet) -> SafeStopReport {
175 // Suppress unused-arg warning in the scaffold. The DPR-02a
176 // phase-1 step 2+ body iterates each service bit and executes
177 // the §5.1 ordering for its peripherals.
178 let _ = services;
179 SafeStopReport {
180 timeouts: 0,
181 elapsed_us: 0,
182 }
183 }
184}
185
186// ── Tests ───────────────────────────────────────────────────────────────
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn service_set_empty_is_zero() {
194 assert_eq!(ServiceSet::EMPTY.raw(), 0);
195 }
196
197 #[test]
198 fn service_set_named_bits_distinct() {
199 // Each named service occupies a unique single bit per DPR-02 §5.4
200 // mapping intent. Bit positions are scaffold-local but stable for
201 // DPR-02a; the eventual DPR-01 ServiceSet type ratifies the
202 // canonical positions.
203 let all = [
204 ServiceSet::AUDIO,
205 ServiceSet::CODEC_RESET,
206 ServiceSet::SD,
207 ServiceSet::QSPI,
208 ServiceSet::MEMS_MIC,
209 ServiceSet::SCOPE_PROBES,
210 ];
211 for (i, a) in all.iter().enumerate() {
212 assert!(
213 a.raw().is_power_of_two(),
214 "service {i} is not a single bit: 0x{:08x}",
215 a.raw()
216 );
217 for (j, b) in all.iter().enumerate() {
218 if i == j {
219 continue;
220 }
221 assert_eq!(
222 a.raw() & b.raw(),
223 0,
224 "services {i} and {j} share bits: 0x{:08x} & 0x{:08x}",
225 a.raw(),
226 b.raw()
227 );
228 }
229 }
230 }
231
232 #[test]
233 fn service_set_union_and_contains() {
234 let audio_sd = ServiceSet::AUDIO.union(ServiceSet::SD);
235 assert!(audio_sd.contains(ServiceSet::AUDIO));
236 assert!(audio_sd.contains(ServiceSet::SD));
237 assert!(!audio_sd.contains(ServiceSet::QSPI));
238 assert!(audio_sd.contains(ServiceSet::EMPTY));
239 // Self-union is identity.
240 assert_eq!(
241 audio_sd.union(audio_sd).raw(),
242 audio_sd.raw(),
243 "union is idempotent"
244 );
245 }
246
247 #[test]
248 fn service_set_round_trip_raw() {
249 // Compose a few services, take .raw(), and confirm individual
250 // bit checks recover the original membership.
251 let s = ServiceSet::AUDIO
252 .union(ServiceSet::CODEC_RESET)
253 .union(ServiceSet::MEMS_MIC);
254 let raw = s.raw();
255 assert_ne!(raw & ServiceSet::AUDIO.raw(), 0);
256 assert_ne!(raw & ServiceSet::CODEC_RESET.raw(), 0);
257 assert_ne!(raw & ServiceSet::MEMS_MIC.raw(), 0);
258 assert_eq!(raw & ServiceSet::SD.raw(), 0);
259 assert_eq!(raw & ServiceSet::QSPI.raw(), 0);
260 assert_eq!(raw & ServiceSet::SCOPE_PROBES.raw(), 0);
261 }
262
263 #[test]
264 fn safe_stop_report_size_is_two_u32() {
265 // DPR-02 §5.3 reserves a 16-byte on-SRAM4 slot; the in-RAM
266 // payload is the two `u32` fields. Future fields require a
267 // §5.3 amendment.
268 assert_eq!(core::mem::size_of::<SafeStopReport>(), 8);
269 }
270
271 #[test]
272 fn safe_stop_run_scaffold_returns_zero_report() {
273 // Scaffold body invariant: zero timeouts + zero elapsed_us
274 // regardless of the service set. The DPR-02a phase-1 step 2+
275 // body replaces this; this test will be replaced (not deleted)
276 // when the real body lands.
277 let report = unsafe { SafeStop::run(ServiceSet::EMPTY) };
278 assert_eq!(report.timeouts, 0);
279 assert_eq!(report.elapsed_us, 0);
280
281 let report = unsafe {
282 SafeStop::run(
283 ServiceSet::AUDIO
284 .union(ServiceSet::CODEC_RESET)
285 .union(ServiceSet::SD),
286 )
287 };
288 assert_eq!(report.timeouts, 0);
289 assert_eq!(report.elapsed_us, 0);
290 }
291}