consortium_hal/timeout.rs
1//! Bounded busy-waits for blocking driver paths.
2//!
3//! A blocking transfer that polls a status flag spins forever if the flag never
4//! comes: a target holding SCL low, a peripheral whose kernel clock was gated
5//! behind the driver's back, a DMA channel that never reaches idle. `embassy-stm32`
6//! bounds those loops with a `Timeout` that reads `embassy-time` when it can and
7//! never expires when it cannot. Consortium's drivers also spin in `Drop` guards
8//! and before any time driver is running, so this `Timeout` counts polls instead
9//! of reading a clock: it works everywhere, and it always expires.
10//!
11//! The cost is that a poll budget is not a duration. One poll is one status read
12//! plus loop overhead, so the wall-clock bound scales inversely with the core
13//! clock; [`Timeout::DEFAULT`] is generous enough to cover a clock-stretched I2C
14//! byte at 10 kHz on a 1 GHz core and still bounded on a slow one.
15//!
16//! ```
17//! use consortium_hal::timeout::Timeout;
18//!
19//! let mut ready = 3;
20//! let mut wait = Timeout::polls(10).start();
21//! let result = loop {
22//! ready -= 1;
23//! if ready == 0 {
24//! break Ok(());
25//! }
26//! if let Err(e) = wait.tick() {
27//! break Err(e);
28//! }
29//! };
30//! assert_eq!(result, Ok(()));
31//! ```
32
33/// The poll budget a blocking wait may spend before giving up.
34///
35/// Each wait creates a fresh [`Deadline`] with [`start`](Self::start), so the
36/// budget bounds one wait step (one byte, one FIFO slot, one idle flag), not
37/// the whole transaction.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct Timeout {
40 polls: u32,
41}
42
43impl Timeout {
44 /// The budget drivers use unless configured otherwise: ten million polls.
45 pub const DEFAULT: Self = Self::polls(10_000_000);
46
47 /// A budget of `polls` status reads. Zero expires on the first tick.
48 #[must_use]
49 pub const fn polls(polls: u32) -> Self {
50 Self { polls }
51 }
52
53 /// The configured poll count.
54 #[must_use]
55 pub const fn count(self) -> u32 {
56 self.polls
57 }
58
59 /// Begin one bounded wait.
60 #[must_use]
61 pub const fn start(self) -> Deadline {
62 Deadline {
63 remaining: self.polls,
64 }
65 }
66
67 /// Poll `ready` until it yields a value or the budget runs out.
68 ///
69 /// `ready` runs at least once, so a condition that already holds succeeds
70 /// even with a zero budget.
71 pub fn spin<T>(self, mut ready: impl FnMut() -> Option<T>) -> Result<T, TimedOut> {
72 let mut deadline = self.start();
73 loop {
74 if let Some(value) = ready() {
75 return Ok(value);
76 }
77 deadline.tick()?;
78 }
79 }
80}
81
82impl Default for Timeout {
83 fn default() -> Self {
84 Self::DEFAULT
85 }
86}
87
88/// One wait in progress; see [`Timeout::start`].
89#[derive(Debug)]
90pub struct Deadline {
91 remaining: u32,
92}
93
94impl Deadline {
95 /// Spend one poll, failing once the budget is exhausted.
96 pub fn tick(&mut self) -> Result<(), TimedOut> {
97 match self.remaining.checked_sub(1) {
98 Some(rest) => {
99 self.remaining = rest;
100 Ok(())
101 }
102 None => Err(TimedOut),
103 }
104 }
105}
106
107/// A bounded wait ran out of polls. Drivers map it to their own
108/// `Error::Timeout`.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct TimedOut;
111
112#[cfg(test)]
113mod tests {
114 use super::{TimedOut, Timeout};
115
116 #[test]
117 fn zero_budget_expires_on_first_tick() {
118 assert_eq!(Timeout::polls(0).start().tick(), Err(TimedOut));
119 }
120
121 #[test]
122 fn budget_allows_exactly_n_ticks() {
123 let mut d = Timeout::polls(3).start();
124 assert!(d.tick().is_ok());
125 assert!(d.tick().is_ok());
126 assert!(d.tick().is_ok());
127 assert_eq!(d.tick(), Err(TimedOut));
128 }
129
130 #[test]
131 fn spin_checks_condition_before_spending() {
132 assert_eq!(Timeout::polls(0).spin(|| Some(7)), Ok(7));
133 }
134
135 #[test]
136 fn spin_gives_up_on_a_stuck_condition() {
137 let mut polls = 0u32;
138 let r: Result<(), _> = Timeout::polls(100).spin(|| {
139 polls += 1;
140 None
141 });
142 assert_eq!(r, Err(TimedOut));
143 assert_eq!(polls, 101);
144 }
145}