The puzzle
A button, a UART, a sensor’s “data ready” line: something outside the processor changes, and firmware must notice. It can keep asking, it can be told, or it can sleep until woken. Which one should it use, and why does an interrupt that is “enabled” sometimes never arrive?
STEP 1
Asking: polling
Polling reads a status flag on a schedule, in the main loop (unit 7, lesson 6) or a timer tick. It is simple, predictable and needs no special care with shared data. Its costs are that every check takes time whether or not anything happened, and that an event waits up to one polling period to be noticed. If the peripheral can hold only one event (a single received byte, say), polling must also be faster than the events arrive, or events are lost.
STEP 2
Being told: interrupts
With an interrupt, the peripheral raises a request and the core stops what it is doing, saves its state (unit 6, lesson 4) and runs a handler. Nothing is spent while nothing happens, and the response is within the interrupt’s entry latency. Each event costs the entry, the handler and the exit; the handler’s own work would be needed with polling too, so the real comparison is the entry and exit overhead against the cost of all the checks.
↑ This step uses the figure at the top of the page.
The balance tips with the event rate. Rare events favour interrupts overwhelmingly. At hundreds of thousands of events a second, interrupt overhead eats a large share of the CPU, and a polling loop fast enough not to miss any does too; such data is better buffered in a FIFO or moved by DMA (unit 12).
STEP 3
The path from event to handler
An interrupt reaches its handler only if every stage passes it on:
- the peripheral sets its event flag (raw status);
- the peripheral’s interrupt enable for that event is set, so it raises its interrupt line;
- the NVIC has that interrupt number enabled;
- the core is not masking interrupts (PRIMASK = 0);
- the interrupt’s priority is higher than that of the code now running (lesson 3).
An interrupt reaches its handler only if every gate on the way is open: the peripheral has raised its event flag, the peripheral’s own interrupt-enable bit passes it on, the NVIC has that interrupt enabled, the core is not masking interrupts, and the interrupt’s priority beats whatever the core is doing. If any one is closed, the request waits (pending) or is never raised at all.
The first two live in the peripheral, often as a trio of registers: raw status, enable, and the masked status that actually drives the line (the RP2040 timer’s INTR, INTE and INTS). A request that passes the peripheral but is blocked later is pending: the NVIC remembers it, and the handler runs as soon as the last gate opens. A flag whose peripheral enable is off never becomes a request at all, but software can still poll it.
STEP 4
Sleeping until something happens: WFI and WFE
When there is nothing to do, a Cortex-M core can sleep. WFI (wait for interrupt) stops the core until an enabled interrupt with a priority above the current execution priority becomes pending (even one PRIMASK is masking). WFE (wait for event) stops it until an event: an interrupt, a SEV instruction from another core, or, with the SEVONPEND bit set, any interrupt becoming pending even while it is disabled or masked. A main loop that ends in __WFI() spends idle time asleep instead of spinning, which is the first step to low power (unit 14).
Many chips also route events directly between peripherals: a timer overflow starting an ADC conversion, an ADC result triggering a DMA transfer. No handler runs at all, and there is nothing for software latency to spoil.
STEP 5
Worked example: a UART receiver without a FIFO
A UART at 115 200 baud with 8N1 framing delivers 11 520 bytes per second, one every
If the receiver holds a single byte, polling every 100 µs loses bytes during a continuous stream: the next byte overwrites the previous one before it is read. Polling every 50 µs would work but costs CPU time all the time. An interrupt per byte, with an illustrative 60-cycle cost at 48 MHz, uses 11 520 × 60 / 48 × 10⁶ ≈ 1.4 % of the CPU only while data flows, and responds within its entry latency. At megabaud rates the interrupt load climbs, and DMA becomes the better answer.
MYTHS AND FACTS
Common misconceptions
Enabling the interrupt in the NVIC is enough
The peripheral’s own interrupt enable must be set too, the core must not be masking interrupts, and the priority must allow it.
Interrupts are always better than polling
For very frequent events their per-event overhead can exceed a tight polling loop that does nothing else; for predictable, slow checks polling is simpler.
A disabled interrupt forgets the event
The peripheral flag stays set, and a request blocked in the NVIC stays pending until it is enabled or cleared.
An idle main loop should spin
It can sleep with WFI and let the next interrupt wake it.
Check yourself
Answer in your head, then open the card.
A timer's interrupt flag is set, its INTE bit is set and the NVIC has the IRQ enabled, but the handler does not run. What else can block it?
Interrupts are masked (PRIMASK = 1, for example inside a critical section), or the core is running a handler of equal or higher priority. The request stays pending until that changes.
Polling a flag costs 20 cycles every 100 µs at 48 MHz. What fraction of the CPU is that?
20 / (100 µs × 48 MHz) = 20 / 4800 ≈ 0.42 %.
A sensor sets a data-ready flag 10 times per second and the reading must be taken within 1 ms. Poll or interrupt?
An interrupt (or polling at 1 ms or faster, which costs CPU time continuously for an event that happens 10 times a second).
What is the difference between WFI and WFE with SEVONPEND set?
WFI wakes when an enabled interrupt of sufficient priority becomes pending, even if PRIMASK is masking it (so code can sleep inside a critical section and handle the event after re-enabling); WFE with SEVONPEND also wakes when an interrupt that is disabled in the NVIC becomes pending, and on events from SEV.
Sources (3)
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h and cmsis_gcc.h — NVIC_EnableIRQ writes ISER; NVIC_SetPendingIRQ / NVIC_ClearPendingIRQ write ISPR / ICPR; SCB SCR bits SEVONPEND (4), SLEEPDEEP (2), SLEEPONEXIT (1); cmsis_gcc.h __WFI(), __WFE(), __SEV()
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_irq/irq.h — “The RP2040 uses the standard ARM nested vectored interrupt controller (NVIC) … only the lower 26 IRQ signals are connected”; one NVIC per core; “If an IRQ is enabled and fires with no handler installed, a breakpoint will be hit and the IRQ number will be in register r0”
- Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_regs/timer.h — a typical peripheral interrupt block: INTR (raw status), INTE (enable), INTF (force), INTS (status after masking and forcing)