Skip to main content

rivet/
irq.rs

1//! IRQ dispatch (plan.md Phase 13).
2//!
3//! A fixed-size table (`RIVET_MAX_IRQS` slots) of registered handlers,
4//! populated at init and walked by whichever `rivet-arch-*` controller
5//! driver the board enables (`rivet-arch-cortex-m/nvic`'s
6//! `rivet_irq_handler`, `rivet-arch-riscv/plic`'s claim/dispatch/complete
7//! loop). The split follows the same Group A/B logic as everything else
8//! in this kernel: **the controller** (NVIC, PLIC) **is arch** — every
9//! board on that ISA shares the same interrupt-controller hardware — but
10//! **the IRQ number** (which number is UART0, which is the GPIO block) is
11//! entirely board-specific, so it lives in each `rivet-bsp-*` crate's own
12//! `irq` module as plain constants, never here.
13//!
14//! [`register`] stores a plain function pointer, not a closure — IRQ
15//! handlers run on the arch's ISR stack with no allocator and (on
16//! Cortex-M) in Handler mode, so a `'static fn()` is the right shape: no
17//! captured state beyond what a `static` can already hold.
18
19use crate::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20
21pub const MAX_IRQS: usize = crate::config::MAX_IRQS;
22
23#[cfg(not(loom))]
24static HANDLERS: [AtomicUsize; MAX_IRQS] = [const { AtomicUsize::new(0) }; MAX_IRQS];
25#[cfg(loom)]
26loom::lazy_static! {
27    static ref HANDLERS: [AtomicUsize; MAX_IRQS] = core::array::from_fn(|_| AtomicUsize::new(0));
28}
29
30// Which `irq_num`s [`dispatch`] must never wrap in `trace::isr(...)`, even
31// when the `trace` feature is on — see [`register_untraced`].
32#[cfg(not(loom))]
33static UNTRACED: [AtomicBool; MAX_IRQS] = [const { AtomicBool::new(false) }; MAX_IRQS];
34#[cfg(loom)]
35loom::lazy_static! {
36    static ref UNTRACED: [AtomicBool; MAX_IRQS] = core::array::from_fn(|_| AtomicBool::new(false));
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum IrqError {
41    /// `irq_num >= RIVET_MAX_IRQS`.
42    OutOfRange,
43}
44
45/// Register `handler` for `irq_num`. Overwrites any previous registration
46/// for the same number (the caller is expected to do this once at init,
47/// before calling [`enable`]). Does **not** enable the interrupt at the
48/// controller — call [`enable`] once the handler is registered, in that
49/// order, so the controller never fires into an empty slot.
50pub fn register(irq_num: u32, handler: fn()) -> Result<(), IrqError> {
51    let slot = HANDLERS.get(irq_num as usize).ok_or(IrqError::OutOfRange)?;
52    slot.store(handler as usize, Ordering::Release);
53    Ok(())
54}
55
56/// Same as [`register`], but [`dispatch`] never wraps this handler in
57/// `trace::isr(...)` — for the one class of IRQ where tracing it would be
58/// actively wrong, not just noisy: whatever handler drains the UART the
59/// trace stream itself is transmitted over. Emitting an `IrqEnter`/
60/// `IrqExit` frame from inside that handler queues *more* bytes on the
61/// same wire, which (if interrupt-driven TX is in use, per
62/// [`crate::console::enable_irq_tx`]) re-arms the same interrupt before it
63/// returns — a self-sustaining feedback loop, confirmed on real hardware:
64/// the console UART IRQ re-triggered itself continuously, at a priority
65/// equal to `PendSV`/`SysTick` (so neither could preempt it), starving the
66/// scheduler entirely before the very first task ever spawned. Use this
67/// for a board's console/trace UART ISR; every other handler should keep
68/// using plain [`register`].
69pub fn register_untraced(irq_num: u32, handler: fn()) -> Result<(), IrqError> {
70    register(irq_num, handler)?;
71    if let Some(slot) = UNTRACED.get(irq_num as usize) {
72        slot.store(true, Ordering::Release);
73    }
74    Ok(())
75}
76
77/// Deregister `irq_num`'s handler (a spurious/unhandled interrupt after
78/// this becomes a silent no-op in [`dispatch`], not a fault — matching
79/// how a not-yet-registered slot behaves before the first [`register`]).
80pub fn unregister(irq_num: u32) {
81    if let Some(slot) = HANDLERS.get(irq_num as usize) {
82        slot.store(0, Ordering::Release);
83    }
84    if let Some(slot) = UNTRACED.get(irq_num as usize) {
85        slot.store(false, Ordering::Release);
86    }
87}
88
89/// Enable `irq_num` at the arch's interrupt controller
90/// (`port::arch::irq_enable`).
91pub fn enable(irq_num: u32) {
92    crate::port::arch::irq_enable(irq_num);
93}
94
95/// Disable `irq_num` at the arch's interrupt controller.
96pub fn disable(irq_num: u32) {
97    crate::port::arch::irq_disable(irq_num);
98}
99
100/// Set `irq_num`'s controller priority (0 = highest; the controller's own
101/// range/granularity — e.g. NVIC's 8-bit `ipr` byte — is arch-defined).
102pub fn set_priority(irq_num: u32, priority: u8) {
103    crate::port::arch::irq_set_priority(irq_num, priority);
104}
105
106/// Called by the arch controller driver with the IRQ number that just
107/// fired. Looks up and calls the registered handler; a no-op if nothing
108/// is registered for `irq_num` (a controller can only report interrupts
109/// it was told to enable, so this path is only hit by a genuine
110/// registration bug or race, not routine operation — silently ignoring it
111/// is safer than panicking on the ISR stack).
112pub fn dispatch(irq_num: u32) {
113    let Some(slot) = HANDLERS.get(irq_num as usize) else {
114        return;
115    };
116    let ptr = slot.load(Ordering::Acquire);
117    if ptr == 0 {
118        return;
119    }
120    // `ptr as *const ()` (not a direct `usize`-to-`fn()` transmute, which
121    // Miri correctly flags as producing a provenance-less/dangling
122    // pointer even though the bit pattern is identical) is the `as`
123    // int-to-pointer cast that looks back up the provenance the `as
124    // usize` cast in `register` exposed.
125    let raw_ptr = ptr as *const ();
126    // SAFETY: `ptr` was stored by `register` from a `fn()` value — the
127    // only thing ever stored here — so `raw_ptr` points at exactly that
128    // function; transmuting a pointer-to-pointer-width `fn()` changes
129    // only the type, not the bits.
130    let handler: fn() = unsafe { core::mem::transmute::<*const (), fn()>(raw_ptr) };
131    #[cfg(feature = "trace")]
132    {
133        let traced = !UNTRACED
134            .get(irq_num as usize)
135            .map(|f| f.load(Ordering::Acquire))
136            .unwrap_or(false);
137        if traced {
138            crate::trace::isr(irq_num, true);
139        }
140        handler();
141        if traced {
142            crate::trace::isr(irq_num, false);
143        }
144    }
145    #[cfg(not(feature = "trace"))]
146    handler();
147}
148
149#[cfg(feature = "test-support")]
150pub(crate) fn reset_for_test() {
151    for slot in HANDLERS.iter() {
152        slot.store(0, Ordering::Relaxed);
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use core::sync::atomic::{AtomicBool, Ordering as StdOrdering};
160
161    static CALLED: AtomicBool = AtomicBool::new(false);
162    fn handler() {
163        CALLED.store(true, StdOrdering::Release);
164    }
165
166    #[test]
167    fn register_and_dispatch() {
168        crate::kernel_test! {
169            CALLED.store(false, StdOrdering::Release);
170            register(3, handler).unwrap();
171            dispatch(3);
172            assert!(CALLED.load(StdOrdering::Acquire));
173        }
174    }
175
176    #[test]
177    fn dispatch_unregistered_is_noop() {
178        crate::kernel_test! {
179            // Must not panic.
180            dispatch(1);
181        }
182    }
183
184    #[test]
185    fn register_out_of_range() {
186        crate::kernel_test! {
187            assert_eq!(register(MAX_IRQS as u32, handler), Err(IrqError::OutOfRange));
188        }
189    }
190
191    #[test]
192    fn unregister_makes_dispatch_noop() {
193        crate::kernel_test! {
194            CALLED.store(false, StdOrdering::Release);
195            register(5, handler).unwrap();
196            unregister(5);
197            dispatch(5);
198            assert!(!CALLED.load(StdOrdering::Acquire));
199        }
200    }
201}