Skip to main content

godspeed/driver/
irq.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Waiting for a device's INTERRUPT - and for requests, on the same endpoint, without losing one.
3//!
4//! # Why this exists
5//!
6//! A driver that takes interrupts idles in one place: a timed receive on its endpoint. The kernel
7//! delivers the device's interrupt there as a message (CLAUDE.md 12.2), and the driver's clients send
8//! their requests there too, because a task owns exactly one endpoint. So every wake is one of three
9//! things - the interrupt, a request, or the deadline - and the driver must tell them apart.
10//!
11//! `xhci`, `ehci` and `dwc2` each wrote that out by hand, and the copies record what it cost:
12//!
13//! - **A request taken and dropped.** The receive CONSUMES; it does not peek. `dwc2` took a message,
14//!   found no disk, and let it go - and `block-driver`, blocked in request/reply, hung before printing a
15//!   line, with `fs` behind it. The same `let _ =` once cost the keyboard its interrupts.
16//! - **Any message counted as an interrupt.** `xhci` set its "waking on interrupts" flag on every wake,
17//!   and with a disk attached disk requests set it, so a log line that read as proof of MSI proved only
18//!   that something had arrived.
19//! - **A zero deadline.** `recv_timeout(0)` blocks FOREVER, so a computed budget that rounds to zero
20//!   turns a watchdog into a hang.
21//!
22//! This is the one wait, written once, from those three - and since 2026-10-22 it is the wait of all three
23//! of them, and of both audio drivers: every driver in `services/` that takes an interrupt waits for it
24//! here. A driver that drains its endpoint before it blocks takes what is queued with [`Irq::take`], the
25//! same sorting without the wait.
26//!
27//! # The shape
28//!
29//! ```ignore
30//! use godspeed::driver::irq::{Irq, Woke};
31//! use godspeed::driver::wait::Budget;
32//!
33//! let irq = Irq::granted(ctx);              // None-vector is honest: no interrupt was routed
34//! loop {
35//!     match irq.wait(ctx, Budget::ms(150)) {
36//!         Woke::Interrupt => { /* read and clear the device's status; then */ irq.rearm(ctx); }
37//!         Woke::Request(msg) => serve(ctx, &msg), // SERVED, never dropped
38//!         Woke::Timeout => { /* the watchdog: look anyway, an interrupt can be lost */ }
39//!     }
40//!     do_the_work();
41//! }
42//! ```
43//!
44//! **With no interrupt the same loop is a paced poll that still serves requests**, so a driver keeps
45//! one loop rather than an interrupt loop and a polling loop that drift apart. [`Irq::routed`] says
46//! which case it is in, for a driver that should say so (invariant 12: a driver that wanted an
47//! interrupt and polls must not do it silently).
48//!
49//! # What it will not do for you
50//!
51//! **It does not read the device.** What raised the interrupt, and how to clear it, are the device's;
52//! a driver that does not clear its source before [`Irq::rearm`] gets the interrupt again at once.
53//!
54//! **It does not log**, for the reason `wait` does not: only the driver knows what a timeout means.
55//!
56//! **It cannot tell a one-byte request equal to the vector from an interrupt.** The kernel's
57//! notification is a one-byte payload naming the vector, and that is the only mark it carries - the
58//! convention all three drivers already relied on. A protocol whose requests are longer than one byte,
59//! as every protocol in the tree is, never meets it.
60
61#[cfg(not(test))]
62use godspeed_sdk::service_context::ServiceContext;
63
64#[cfg(not(test))]
65use super::wait::Budget;
66#[cfg(not(test))]
67use crate::ipc::Message;
68
69/// Is a message with this payload the kernel's notification of interrupt `vector`?
70pub(crate) fn is_interrupt(payload: &[u8], vector: Option<u8>) -> bool {
71    match vector {
72        Some(v) => payload == [v],
73        None => false,
74    }
75}
76
77/// Counter ticks for a receive deadline of `us` microseconds, NEVER zero: zero blocks forever. On an
78/// uncalibrated machine (`per_10ms == 0`) that is 1, which the kernel floors to one scheduler quantum -
79/// the same answer `ServiceContext::duration_cycles` gives.
80pub(crate) fn deadline_ticks(per_10ms: u64, us: u64) -> u64 {
81    if per_10ms == 0 { 1 } else { super::wait::ticks_for(per_10ms, us) }
82}
83
84/// What ended a wait.
85#[cfg(not(test))]
86pub enum Woke {
87    /// The device's interrupt. Clear its source, then [`Irq::rearm`].
88    Interrupt,
89    /// A request from a client. It was taken off the endpoint and is now the caller's to SERVE - its
90    /// reply capability, if it carries one, is waiting to be taken (`gs::ipc::take_sent_cap`).
91    Request(Message),
92    /// The deadline passed with nothing arriving.
93    Timeout,
94}
95
96/// The interrupt this driver was granted, if any, and how many it has seen.
97#[cfg(not(test))]
98pub struct Irq {
99    vector: Option<u8>,
100    seen: core::cell::Cell<u64>,
101}
102
103#[cfg(not(test))]
104impl Irq {
105    /// The interrupt the kernel routed to this driver at spawn. A driver asks for one in its spawn row
106    /// (`hwclass::pci_irq`) and never names a vector: routing a vector is authority the kernel keeps.
107    pub fn granted(ctx: &ServiceContext) -> Self {
108        Irq { vector: ctx.irq_vector(), seen: core::cell::Cell::new(0) }
109    }
110
111    /// Was an interrupt routed at all? `false` means every [`Irq::wait`] ends on a request or its deadline.
112    pub fn routed(&self) -> bool {
113        self.vector.is_some()
114    }
115
116    /// The vector the kernel routed, for a driver that reports it or tells its notice apart itself.
117    ///
118    /// Reading it is not routing it: the vector is the kernel's to choose (see [`Irq::granted`]), and a
119    /// driver that knows the number still cannot ask for another one.
120    pub fn vector(&self) -> Option<u8> {
121        self.vector
122    }
123
124    /// Interrupts seen by [`Irq::wait`] and [`Irq::take`] so far - evidence, for a driver to report.
125    pub fn seen(&self) -> u64 {
126        self.seen.get()
127    }
128
129    /// Wait for the interrupt, a request or `within`, whichever is first.
130    ///
131    /// On a machine whose clock the kernel could not calibrate, `within` is not a duration: the wait
132    /// ends after one scheduler quantum whatever was asked (see `deadline_ticks`).
133    ///
134    /// `#[inline(always)]` because it returns a 4 KiB message by value; as its own frame that would be
135    /// 4 KiB of stack on every caller (the SDK's `recv_timeout` says the same).
136    #[inline(always)]
137    pub fn wait(&self, ctx: &ServiceContext, within: Budget) -> Woke {
138        let ticks = deadline_ticks(ctx.tsc_ticks_per_10ms(), within.as_us());
139        match ctx.recv_timeout(ticks) {
140            None => Woke::Timeout,
141            Some(m) => self.sort(m),
142        }
143    }
144
145    /// Take what is already waiting, without blocking: the interrupt, a request, or `None` when nothing
146    /// is queued. Never [`Woke::Timeout`].
147    ///
148    /// For a driver that DRAINS its endpoint in a bounded loop and blocks only at the end of a pass -
149    /// `dwc2` and `xhci` both do, so a burst of requests is served in one pass. Each drain used to tell
150    /// the interrupt apart by hand, and `dwc2` once dropped one as a capless request, leaving its line
151    /// masked for good; this is the same test [`Irq::wait`] makes, so the two cannot disagree.
152    #[inline(always)]
153    pub fn take(&self, ctx: &ServiceContext) -> Option<Woke> {
154        ctx.try_recv().map(|m| self.sort(m))
155    }
156
157    /// The interrupt, counted, or a request - the one test both waits make.
158    #[inline(always)]
159    fn sort(&self, m: Message) -> Woke {
160        if is_interrupt(m.payload_bytes(), self.vector) {
161            self.seen.set(self.seen.get().saturating_add(1));
162            Woke::Interrupt
163        } else {
164            Woke::Request(m)
165        }
166    }
167
168    /// Let the interrupt fire again, after the driver has cleared its source. A level-triggered line is
169    /// held masked by the kernel while it is being handled, so it cannot storm; this re-opens it. For an
170    /// MSI, which is an edge and is never masked, it is a no-op - so a driver calls it either way and does
171    /// not have to know which it was given.
172    pub fn rearm(&self, ctx: &ServiceContext) {
173        if let Some(v) = self.vector {
174            ctx.irq_unmask(v);
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn only_the_vector_alone_is_an_interrupt() {
185        assert!(is_interrupt(&[0x41], Some(0x41)));
186        assert!(!is_interrupt(&[0x42], Some(0x41))); // another vector's notice is not ours
187        assert!(!is_interrupt(&[0x41, 0x00], Some(0x41))); // a longer message is a request
188        assert!(!is_interrupt(&[], Some(0x41)));
189        assert!(!is_interrupt(&[0x41], None)); // no interrupt routed: nothing is one
190    }
191
192    #[test]
193    fn a_deadline_is_never_zero() {
194        assert_eq!(deadline_ticks(0, 150_000), 1); // uncalibrated: one quantum, not "forever"
195        assert_eq!(deadline_ticks(540_000, 0), 1); // a zero budget still ends
196        assert_eq!(deadline_ticks(540_000, 10_000), 540_000);
197    }
198}