Skip to main content

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}