Skip to content

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.

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

fn cache_is_off() -> bool

Whether 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.

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.

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.

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

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 dispatch finds 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.

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

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.

As [connect], plus both of those promises.

References: ConnectError, CpuInt

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.

As [connect_at], plus both of those promises.

References: ConnectError

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.

fn in_interrupt() -> bool

True 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.

fn is_iram_safe(irq: u8) -> bool

Whether irq has a handler that promised to be IRAM-safe. Test support.

unsafe fn mask_non_iram_safe() -> u32

Mask 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.

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.

fn register(irq: u8, isr: fn(..)) -> bool

Register 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).

fn register_iram_safe(irq: u8, isr: fn(..)) -> bool

Register 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.

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.

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.

unsafe fn restore_mask(previous: u32)

Put back what [mask_non_iram_safe] returned.

previous must be the value that call returned, on this core.