kernel::interrupt
Interrupt routing framework (plan W6.2).
A peripheral IRQ vectors into the trap handler, which calls [dispatch].
Dispatch runs the registered top-half (a tiny IRAM handler) which does the
minimal work of enqueuing an event to the owning driver task’s queue via
Queue::send_isr. The driver task’s bottom-half then runs at its own
priority. The top-half closure owns the forwarding — there is no shared
mutable event buffer (the previous &'static mut hand-out was UB).
Handler-table access is guarded by a critical section.
Structs
Section titled “Structs”InterruptGuard
Section titled “InterruptGuard”struct InterruptGuard { /* private fields */ }RAII marker for “we are now running trap-context code that must not
block”. Held around the top-half call in [dispatch] and around the
callback invocation in timer::process_timers.
Implements: Drop
Functions
Section titled “Functions”cache_is_off
Section titled “cache_is_off”fn cache_is_off() -> boolWhether a flash operation currently has the instruction cache disabled.
Relaxed is right: the only writer is the core that is about to do the
flash operation, and the only reader that matters is that same core’s trap
handler. The other core is stalled in hardware for the duration and cannot
observe anything at all.
clear_pending
Section titled “clear_pending”fn clear_pending(irq: u8)Acknowledge a CPU-level edge/software interrupt via the INTCLEAR special register. Level-triggered peripheral sources must additionally be cleared at the peripheral by the driver’s top-half.
irq must be < 32 (the INTCLEAR register is 32 bits, one bit per IRQ);
1u32 << irq for irq >= 32 is a shift-overflow that silently produces a
wrong (or, in debug builds, panicking) mask instead of clearing the
intended interrupt (item 10b) — validate instead of trusting the caller.
connect
Section titled “connect”unsafe fn connect(source: u8, handler: fn(..)) -> Result<hal::CpuInt, ConnectError>Route a peripheral source to the first free CPU input the kernel may
hand out, take the handler, unmask it, and return which input it landed on.
This is the ergonomic form: a driver that does not care which CPU input
its interrupt uses — which is almost all of them — no longer has to invent
a number and hope nothing else picked the same one. The kernel walks the
external, level-1 inputs vectors.S can service (intr_map::USABLE, in
ascending order) and takes the lowest one with no handler yet. A caller
that genuinely needs a specific input (an interrupt level, say) uses
[connect_at].
Returns [ConnectError::NoneFree] when every such input already has a
handler, and [ConnectError::Route] when the crossbar refuses the source
itself (no such source) — the latter is a source problem no free input
would have fixed.
Safety
Section titled “Safety”As [connect_at]: writes the interrupt crossbar and unmasks a CPU input,
and handler runs in trap context — short, non-blocking, and it must
acknowledge its peripheral.
References: ConnectError, CpuInt
connect_at
Section titled “connect_at”unsafe fn connect_at(source: u8, cpu_int: u8, handler: fn(..)) -> Result<(), ConnectError>Route a peripheral source to a named CPU input, take the handler, and
unmask it. The explicit-slot form; [connect] picks the input for you.
Three steps that must happen in this order, and did not have one place to live: six call sites retyped them, which is how one of them eventually gets it wrong. The order is load-bearing in both directions —
- unmasking before a handler is registered leaves a window where the
interrupt fires and
dispatchfinds nothing, and for a level-triggered source that window does not close: it re-enters forever; - registering before routing is harmless but useless, and reads as though the route were optional.
Enabling the interrupt at the peripheral is still the driver’s job, and deliberately separate. A peripheral that raises its line before the kernel is ready to answer is the same storm by a different route.
Safety
Section titled “Safety”Writes the interrupt crossbar and unmasks a CPU input. handler runs in
trap context: it must be short, must not block, and must acknowledge its
peripheral.
References: ConnectError
connect_iram_safe
Section titled “connect_iram_safe”unsafe fn connect_iram_safe(source: u8, handler: fn(..)) -> Result<hal::CpuInt, ConnectError>[connect], for a handler that has made the promises in
[register_iram_safe] — IRAM, and no locks.
Safety
Section titled “Safety”As [connect], plus both of those promises.
References: ConnectError, CpuInt
connect_iram_safe_at
Section titled “connect_iram_safe_at”unsafe fn connect_iram_safe_at(source: u8, cpu_int: u8, handler: fn(..)) -> Result<(), ConnectError>[connect_at], for a handler that has made the promises in
[register_iram_safe] — IRAM, and no locks.
Safety
Section titled “Safety”As [connect_at], plus both of those promises.
References: ConnectError
dispatch
Section titled “dispatch”fn dispatch(irq: u8)Dispatch a fired IRQ to its top-half. Called from the trap handler with interrupts already masked, so no extra critical section is taken.
in_interrupt
Section titled “in_interrupt”fn in_interrupt() -> boolTrue while executing a top-half ISR or a software-timer callback (trap
context). Blocking primitives in mutex.rs/queue.rs/timer.rs check
this and refuse rather than silently blocking the interrupted task.
is_iram_safe
Section titled “is_iram_safe”fn is_iram_safe(irq: u8) -> boolWhether irq has a handler that promised to be IRAM-safe. Test support.
mask_non_iram_safe
Section titled “mask_non_iram_safe”unsafe fn mask_non_iram_safe() -> u32Mask every interrupt that is not safe to run with the cache off.
Returns the previous INTENABLE, which [restore_mask] puts back. The
handlers registered through [register_iram_safe] stay enabled; everything
else — including any CPU interrupt with no handler at all, since an
unregistered one has nothing promising anything — is masked.
This replaces raising PS.INTLEVEL, which masked the lot. Both esp-idf and
NuttX mask selectively for exactly this reason: a sector erase is tens of
milliseconds, and stopping every interrupt for that long is a real-time
defect whether or not a radio is involved.
Safety
Section titled “Safety”Must be paired with [restore_mask] on the same core. Interrupts stay
masked until it is, and a driver whose interrupt never fires again looks
like a dead peripheral rather than a missing call.
register
Section titled “register”fn register(irq: u8, isr: fn(..)) -> boolRegister a top-half for irq. Returns false if the table is full, or if
irq already has a registered handler — a second registration would
otherwise be silently unreachable dead code (dispatch stops at the
first match), which is exactly the “plausible-looking wrong result” the
project’s error-handling philosophy forbids (item 10a).
register_iram_safe
Section titled “register_iram_safe”fn register_iram_safe(irq: u8, isr: fn(..)) -> boolRegister a top-half that may run while the instruction cache is off.
A flash erase or program runs with the cache disabled, and anything fetched from flash during that window fetches nothing at all — the core stops. So FlintOS masks interrupts for the duration. Blanket masking is safe but costs the whole operation in latency, and a sector erase is tens of milliseconds: long enough to drop a Wi-Fi link.
This is the opt-out. A handler registered here stays enabled through a
flash operation, and in exchange it promises that it and everything it
calls live in IRAM. esp-idf spells the same promise ESP_INTR_FLAG_IRAM
and NuttX honours it in esp32_spiflash_opstart.
The promise is not checked
Section titled “The promise is not checked”Nothing here can verify it. #[link_section] places a function body and
says nothing about a copy the optimiser folded into a caller — a trap this
project has fallen into three times in the flash driver alone. Check the
built ELF, not the attributes.
Breaking it does not fault. The core simply stops fetching, which presents as a board that went silent during a flash write, nowhere near the handler that caused it.
And it must not take a lock
Section titled “And it must not take a lock”A second promise, and a less obvious one. The other core is stalled in
hardware for the duration of a flash operation — see esp32-flash’s
module docs — and a stalled core does not release what it holds. A handler
that waits on a Spinlock core 1 was stalled holding will wait for the
rest of time.
That constraint used to be satisfied by accident, because nothing ran
between the stall and the release. It is a real constraint now: this is the
register for handlers that run in exactly that window. Queue::send_isr
and the atomics are fine; anything that locks is not.
The way out, when a handler genuinely needs one, is NuttX’s
esp32_spiflash_opstart: park the other core with a handshake it enters
voluntarily, at a point where it holds nothing, instead of stalling it
wherever it happens to be.
restore_mask
Section titled “restore_mask”unsafe fn restore_mask(previous: u32)Put back what [mask_non_iram_safe] returned.
Safety
Section titled “Safety”previous must be the value that call returned, on this core.