Skip to main content

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}