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}