UNIT 09 · LESSON 2 OF 6

Interrupt Requests and Handler Execution

Does the new one wait, or does it cut in? When the first handler returns, does the processor go back to the main program, or straight into the next handler? And when one interrupt line serves eight GPIO pins, whose job is it to find out which pin it was?

INTERACTIVEPending, active, nested, tail-chained
A timeline of two interrupt handlers showing nesting or tail-chaininghandler Ahandler Bmain programB arrivesB preempts A: A is suspended from 20 to 50 and finishes at 90. B waited 0.
A timeline of two interrupt handlers showing nesting or tail-chaininghandler Ahandler Bmain programB arrivesB preempts A: A is suspended from 20 to 50 andfinishes at 90. B waited 0.

Try this

B’s priority (A is 1)
B preempts A: A is suspended from 20 to 50 and finishes at 90. B waited 0.

Handler A (priority 1) is running when interrupt B arrives. If B is more urgent (a lower number), it preempts A: A’s state is stacked, B runs, then A resumes. If B is equal or less urgent, it stays pending until A finishes; the core then goes straight from A into B without restoring and re-saving the interrupted program’s registers, which Arm calls tail-chaining. Times are in arbitrary units.

What you will be able to do
  • Describe the inactive, pending, active and active-and-pending states of an interrupt.
  • Trace the sequence from request to handler to exception return, and relate it to the stacked frame.
  • Predict whether a second interrupt preempts a running handler or is tail-chained after it.
  • Install a handler by vector name or at run time, and explain the trade-offs.
  • Write a handler for a shared interrupt line that services every pending source.
Before you start
  • The five conditions for taking an interrupt (lesson 1).
  • The exception frame and EXC_RETURN (unit 6, lesson 4); weak vector names (unit 5, lesson 3).
Steps in this lesson
  1. Four states
  2. From request to return
  3. When requests overlap
  4. Installing handlers
  5. Shared lines: service every source
  6. Worked example: a UART and a timer at equal priority
  7. Common misconceptions

The puzzle

A handler is running when another interrupt arrives. Does the new one wait, or does it cut in? When the first handler returns, does the processor go back to the main program, or straight into the next handler? And when one interrupt line serves eight GPIO pins, whose job is it to find out which pin it was?

STEP 1

Four states

Each interrupt in the NVIC is in one of four states:

  • inactive: nothing requested;
  • pending: requested, waiting to be taken;
  • active: its handler is running (or has been preempted and will resume);
  • active and pending: its handler is running and a new request for the same interrupt has already arrived.

A request makes an interrupt pending; taking it makes it active and clears the pending bit; the exception return makes it inactive. Software can also set or clear the pending bit directly (NVIC_SetPendingIRQ, NVIC_ClearPendingIRQ), which is useful for tests and for triggering deferred work (lesson 6).

STEP 2

From request to return

When a pending interrupt can be taken, the core:

  1. stacks at least the eight-word basic frame (r0–r3, r12, LR, return address, xPSR) on the current stack, plus 18 floating-point words on a core with an FPU when floating-point context is active, and possibly one alignment word (unit 6, lesson 4);
  2. fetches the handler address from the vector table (unit 5, lesson 3) and loads LR with an EXC_RETURN value;
  3. runs the handler, an ordinary C function, since the registers C may clobber have been saved;
  4. on return (a branch to the EXC_RETURN value in LR), unstacks the frame and resumes where it left off.

STEP 3

When requests overlap

If a new request arrives while a handler runs, priority decides (lesson 3). A more urgent interrupt preempts: the running handler is itself interrupted, its state stacked, and it resumes later. An equal or less urgent one stays pending. When the running handler finishes, the core does not unstack the frame and immediately stack it again: it goes straight into the pending handler. Arm calls this tail-chaining; it saves the time of a full return and re-entry.

↑ This step uses the figure at the top of the page.

STEP 4

Installing handlers

A handler is found through the vector table. Defining a function with the vector’s name (TIM3_IRQHandler, or isr_dma_0 on the RP2040) overrides the weak default at link time (unit 5, lesson 3). SDKs also install handlers at run time: the pico-sdk’s irq_set_exclusive_handler() sets the single handler for an interrupt number, and irq_add_shared_handler() adds one of several handlers for a multiplexed line, called in order of an “order priority”. The pico-sdk notes that the link-time method offers no speed benefit and can cause link conflicts.

STEP 5

Shared lines: service every source

Many interrupt lines serve several sources: on the RP2040, all GPIOs of a bank share one IO interrupt per core. A handler for such a line must read the status, handle and clear every pending source, and only then return. Serving only the first one leaves the line asserted, so the handler is entered again at once.

INTERACTIVEOne interrupt line, several sources
Sources pending on a shared interrupt line and which the handler servespending sources on IO_IRQ_BANK0GPIO2 servedGPIO7 still pendingGPIO9this pass serves GPIO2GPIO7 still pending: the handler is entered again immediately, costing another entryand exit, and in a busy system the last source can starve.
Sources pending on a shared interrupt line and which the handler servespending sources on IO_IRQ_BANK0GPIO2 servedGPIO7 still pendingGPIO9this pass serves GPIO2GPIO7 still pending: the handler is entered againimmediately, costing another entry and exit, and ina busy system the last source can starve.
Served GPIO2; GPIO7 re-trigger the line.

Many interrupt lines are shared: on the RP2040 all GPIOs of a bank share one IO interrupt per core, and the pico-sdk lets several handlers attach to one IRQ, each of which “should check and clear the relevant hardware interrupt source”. A handler that services only the first source it finds returns with others still pending, and the line fires again at once.

STEP 6

Worked example: a UART and a timer at equal priority

A UART receive handler takes 6 µs; a timer interrupt of equal priority arrives 2 µs into it and takes 10 µs.

  • The timer request is pending until the UART handler ends, then tail-chains:
twait=tend,A−tarrive,B=6−2=4 μst_{\text{wait}} = t_{\text{end},A} - t_{\text{arrive},B} = 6 - 2 = 4\ \mu\text{s}

so it starts at t = 6 µs and ends at 16 µs.

  • The main program resumes only at 16 µs, having been interrupted once with one stack and one unstack, instead of two of each.
  • Had the timer been more urgent, it would have started at t = 2 µs and the UART handler would have finished at 6 + 10 = 16 µs, 10 µs later than before.

MYTHS AND FACTS

Common misconceptions

A second interrupt is lost while a handler runs

It stays pending (even a second request for the same interrupt makes it active-and-pending) and runs afterwards. But there is only one pending bit: further requests while it is already pending merge into one, so count events in the peripheral if every one matters.

Every interrupt returns to the main program before the next one starts

Pending interrupts are tail-chained directly.

One interrupt line means one source

Shared lines need handlers that find and clear every source.

Defining the handler by name is faster than installing it at run time

The SDK says it offers no run-time benefit over an exclusive handler; the vector table is used either way. Shared handlers add a short dispatch chain.

Check yourself

Answer in your head, then open the card.

Handler A (priority 1) runs from t = 0 to 50. B (priority 0) arrives at t = 10 and takes 20. When does A finish?

B preempts at 10 and runs until 30; A resumes and finishes at 50 + 20 = 70.

Same, but B has priority 2. When does B start?

At t = 50, tail-chained after A; it waits 40.

A GPIO bank interrupt fires for pins 3 and 12 at once. The handler clears pin 3's event and returns. What happens next?

The line is still asserted by pin 12's event, so the handler is entered again immediately; if it keeps serving only the first pin it finds, busy low-numbered pins can starve high-numbered ones.

A timer interrupt fires again while its own handler is still running. What state is it in, and what happens?

Active and pending. When the handler returns, it is tail-chained into itself and runs again. If the first run cleared the peripheral flag after the second event had already set it, the second run finds nothing to do; handlers must tolerate such empty runs.

Sources (3)
  1. Arm, CMSIS 6, CMSIS/Core/Include/core_cm4.h and core_cm0plus.h — NVIC registers ISER (enable), ICER, ISPR (set pending), ICPR (clear pending), IABR “Interrupt Active bit Register” (Cortex-M4; absent on the Cortex-M0+), IPR (priority); NVIC_GetActive reads IABR
  2. Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_irq/irq.h and irq.c — three ways to set handlers: irq_add_shared_handler (“Each handler, should check and clear the relevant hardware interrupt source”; called “in sequence from highest order_priority to lowest”), irq_set_exclusive_handler, or defining e.g. isr_dma_0 at link time (“can cause link conflicts … offers no runtime performance benefit”); one IO interrupt per bank per core
  3. Arm, Armv7-M Architecture Reference Manual (DDI0403), §B1.5 “ARMv7-M exception model” (exception entry, return, tail-chaining) — pending/active states, preemption by priority, and tail-chaining of a pending exception at exception return; section number from memory, not opened in this session