Skip to main content

godspeed/driver/
delay.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Holding still for a set time, waiting for nothing.
3//!
4//! # Why this exists
5//!
6//! Some hardware needs a gap that no register reports: let a reset bit land before the next write,
7//! give a port the recovery time a specification demands before it is addressed. Nothing can be read to
8//! say the gap is over, so this is not a [`wait`](super::wait) - there is no condition, only a
9//! duration. Drivers wrote it by hand, and the copies disagreed about the one case that matters, a
10//! machine whose clock the kernel could not calibrate:
11//!
12//! - `genet`'s `delay_us` yielded once, which returns at once when nothing else is runnable;
13//! - `xhci`'s two holds built their length from `duration_cycles`, which floors to one counter tick
14//!   there, so they held for nothing at all.
15//!
16//! Both are the same mistake in the direction that matters. A hold is a MINIMUM: the device needs at
17//! least this long, and the safe error is too long, never too short.
18//!
19//! # What it does
20//!
21//! ```ignore
22//! use godspeed::driver::delay;
23//! use godspeed::driver::wait::Budget;
24//!
25//! m.write32(CMD, SW_RESET);
26//! delay::hold(ctx, Budget::us(10)); // the reset bit needs time to land; nothing says when it has
27//! m.write32(CMD, 0);
28//! ```
29//!
30//! **On a calibrated machine [`hold`] spins** for the duration, measured by the counter. It does not
31//! sleep: on every port but ARMv7 (whose sub-tick sleep is not relied on here) the kernel's sleep
32//! floors at a scheduler quantum, nominally 10 ms (CLAUDE.md 9.1), which would
33//! turn a 10 us hold into a thousand times that. **[`hold_parked`] sleeps first and spins out the
34//! rest**, for holds of tens of milliseconds where the core matters more than the microsecond: it can
35//! run up to a quantum long, which a minimum allows, and is never short. It has three callers (`ehci`,
36//! `wifi-usb`'s rtl8188 backend, and `sdk/audio`'s settings read, pausing between retries), and it is
37//! in on probation: `docs/driver-library.md` 1l says why.
38//!
39//! **On an uncalibrated one both sleep whole quanta**, as many as the duration needs at the nominal
40//! 10 ms: the quantum is measured by the kernel's tick rather than the counter nobody could calibrate,
41//! so it is the one duration left, and it errs long. [`super::wait::calibrated`] says which case this
42//! machine is in.
43//!
44//! **It does not log**, for the reason [`wait`](super::wait) does not: it cannot fail.
45
46#[cfg(not(test))]
47use godspeed_sdk::service_context::ServiceContext;
48
49#[cfg(not(test))]
50use super::wait::Budget;
51
52/// The scheduler quantum the constitution fixes (CLAUDE.md 9.1), in microseconds: the length an
53/// uncalibrated hold counts its sleeps in.
54const NOMINAL_QUANTUM_US: u64 = 10_000;
55
56/// Sleeps an uncalibrated hold of `us` microseconds takes: enough nominal quanta to cover it, at least
57/// one, so a hold is never skipped.
58pub(crate) fn quanta_for(us: u64) -> u64 {
59    (us / NOMINAL_QUANTUM_US + (us % NOMINAL_QUANTUM_US != 0) as u64).max(1)
60}
61
62/// Hold for at least `d`. Spins on a calibrated machine; sleeps whole scheduler quanta on one that is
63/// not. See the module documentation.
64#[cfg(not(test))]
65pub fn hold(ctx: &ServiceContext, d: Budget) {
66    let per_10ms = ctx.tsc_ticks_per_10ms();
67    if per_10ms == 0 {
68        for _ in 0..quanta_for(d.as_us()) {
69            ctx.sleep_ms(1); // floors to one quantum on this machine, which is the point
70        }
71        return;
72    }
73    let ticks = super::wait::ticks_for(per_10ms, d.as_us());
74    let start = ctx.read_tsc();
75    spin_until(ctx, start, ticks);
76}
77
78/// Hold for at least `d`, PARKED: sleep through it, then spin out whatever the sleep left. For holds of
79/// tens of milliseconds - a USB reset, a debounce - where the core matters more than waking on the
80/// microsecond.
81///
82/// A [`hold`] that long spins the whole time, and a driver that holds often pays for it: `ehci`, which
83/// holds 50 and 100 ms on every turn of its rescan loop, read 100% of a core in `observe` with nothing
84/// plugged in until it learned to park. The sleep can wake late by up to a quantum, which a MINIMUM
85/// allows; the spin covers a sleep that woke early, so the hold is never short on a calibrated machine.
86/// On an uncalibrated one it is [`hold`]'s whole quanta.
87#[cfg(not(test))]
88pub fn hold_parked(ctx: &ServiceContext, d: Budget) {
89    let per_10ms = ctx.tsc_ticks_per_10ms();
90    if per_10ms == 0 {
91        return hold(ctx, d);
92    }
93    let ticks = super::wait::ticks_for(per_10ms, d.as_us());
94    let start = ctx.read_tsc();
95    ctx.sleep(ticks);
96    spin_until(ctx, start, ticks);
97}
98
99/// Spin until `ticks` have passed since `start`. `wrapping_sub` so a counter that wraps mid-hold still
100/// measures the interval correctly.
101#[cfg(not(test))]
102fn spin_until(ctx: &ServiceContext, start: u64, ticks: u64) {
103    while ctx.read_tsc().wrapping_sub(start) < ticks {
104        core::hint::spin_loop();
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn an_uncalibrated_hold_is_never_skipped() {
114        assert_eq!(quanta_for(0), 1);
115        assert_eq!(quanta_for(10), 1); // genet's 10 us
116        assert_eq!(quanta_for(2_000), 1); // xhci's settle after HCRST
117    }
118
119    #[test]
120    fn an_uncalibrated_hold_errs_long() {
121        assert_eq!(quanta_for(10_000), 1);
122        assert_eq!(quanta_for(10_001), 2);
123        assert_eq!(quanta_for(55_000), 6); // xhci's reset-recovery hold
124        assert_eq!(quanta_for(u64::MAX), u64::MAX / NOMINAL_QUANTUM_US + 1);
125    }
126}