UNIT 09 · LESSON 1 OF 6

Polling, Interrupts, and Events

Which one should it use, and why does an interrupt that is “enabled” sometimes never arrive?

INTERACTIVEPolling or interrupts?
CPU load and latency of polling versus interruptsCPU spent polling0.425 %CPU spent in interrupts0.0125 %polling: 0.425 % of the CPU (0.417 % of it checking, even when idle); latency up to100 µs (50 µs on average)interrupts: 0.0125 % of the CPU at 100 events/s; responds within the entry latencyInterrupts win here: the CPU is free (or asleep) until something happens.
CPU load and latency of polling versus interruptsCPU spent polling0.425 %CPU spent in interrupts0.0125 %polling: 0.425 % of the CPU (0.417 % of it checking,even when idle); latency up to 100 µs (50 µs onaverage)interrupts: 0.0125 % of the CPU at 100 events/s;responds within the entry latencyInterrupts win here: the CPU is free (or asleep)until something happens.

Try this

Events per second
Polling period
Polling 0.425 % CPU, latency ≤ 100 µs; interrupts 0.0125 % CPU.

Polling checks a status flag on a schedule and pays for every check, whether or not anything happened; its latency is up to one polling period. An interrupt costs nothing while nothing happens and a fixed amount per event, and responds within its entry latency. Cycle counts are illustrative, at 48 MHz: handling an event takes 40 cycles either way; each poll check costs 20 more, each interrupt 20 more for entry and exit. The model assumes the peripheral holds one event, so a poll slower than the events loses some.

What you will be able to do
  • Compare polling and interrupts in CPU load and latency for a given event rate, and find where the balance tips.
  • List the conditions that must all hold for an interrupt to be taken, from the peripheral’s flag to the core’s priority.
  • Explain the difference between a pending interrupt and one that is never requested.
  • Describe WFI and WFE and how a processor sleeps until an interrupt or event.
  • Choose polling, interrupts or hardware event routing for a given requirement.
Before you start
  • A nonblocking main loop (unit 7, lesson 6).
  • The vector table and exception entry (unit 5, lesson 3).
Steps in this lesson
  1. Asking: polling
  2. Being told: interrupts
  3. The path from event to handler
  4. Sleeping until something happens: WFI and WFE
  5. Worked example: a UART receiver without a FIFO
  6. Common misconceptions

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:

  1. the peripheral sets its event flag (raw status);
  2. the peripheral’s interrupt enable for that event is set, so it raises its interrupt line;
  3. the NVIC has that interrupt number enabled;
  4. the core is not masking interrupts (PRIMASK = 0);
  5. the interrupt’s priority is higher than that of the code now running (lesson 3).
INTERACTIVEFive conditions, all at once
The five conditions for an interrupt to be taken and which are met✓ the peripheral’s event flag is set✓ the peripheral’s interrupt enable bit is set✗ the interrupt is enabled in the NVIC✓ interrupts are not masked (PRIMASK = 0)✓ its priority is higher than what the core is running nowThe request is pending but not taken. It is taken as soon as the interrupt is enabledin the NVIC.
The five conditions for an interrupt to be taken and which are met✓ the peripheral’s event flag is set✓ the peripheral’s interrupt enable bit is set✗ the interrupt is enabled in the NVIC✓ interrupts are not masked (PRIMASK = 0)✓ its priority is higher than what the core is running nowThe request is pending but not taken. It is taken assoon as the interrupt is enabled in the NVIC.
Not taken; still needed: the interrupt is enabled in the NVIC.

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

tbyte=10 bits115 200 bit/s≈86.8 μst_{\text{byte}} = \frac{10\ \text{bits}}{115\,200\ \text{bit/s}} \approx 86.8\ \mu\text{s}

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)
  1. 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()
  2. 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”
  3. 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)