UNIT 09 · LESSON 6 OF 6

Deferring Work and Keeping Handlers Short

How can a handler that works correctly break everything around it?

INTERACTIVEKeep the handler short, do the work later
Time spent in a handler with and without deferring its workone eventhandlerdeadline of another interruptCPU load 10 % either way (200 µs × 500/s)other interrupts of equal or lower priority can be blocked for up to 200 µs: beyondthe 50 µs deadline
Time spent in a handler with and without deferring its workone eventhandlerdeadline of another interruptCPU load 10 % either way (200 µs × 500/s)other interrupts of equal or lower priority can beblocked for up to 200 µs: beyond the 50 µs deadline

Try this

Processing per event
Events per second
Another interrupt must be served within
Blocking 200 µs, CPU 10 %.

Every microsecond spent in a handler is a microsecond in which interrupts of equal or lower priority cannot run. Deferring moves the processing out of the handler: the handler only captures the data and sets a flag or queues a job (2 µs here), and the work runs later at thread level. The total CPU time is the same; what changes is how long everything else is blocked.

What you will be able to do
  • Explain how a long handler blocks interrupts of equal and lower priority, and compute the blocking time.
  • Split a handler into a capture part and a deferred processing part.
  • Compare deferral methods: main-loop flag, low-priority software interrupt, RTOS task, Linux threaded IRQ.
  • List operations that do not belong in a handler (blocking waits, heavy printing, slow buses, unbounded loops).
  • Measure a handler’s execution time and set a budget for it.
Before you start
  • Priorities and latency (lesson 3) and ring buffers (lesson 5).
  • Measuring timing without disturbing it (unit 6, lesson 6).
Steps in this lesson
  1. A handler blocks everything at its level and below
  2. Capture now, process later
  3. Where the deferred work runs
  4. What never belongs in a handler
  5. Measure the handler
  6. Worked example: the 200 µs sensor handler
  7. Common misconceptions

The puzzle

The sensor handler reads a packet, checks its CRC, converts the values, filters them and prints a debug line. It takes 200 µs and runs 500 times a second. Nothing is wrong with the results, but the UART now drops bytes and the motor PWM update arrives late. How can a handler that works correctly break everything around it?

STEP 1

A handler blocks everything at its level and below

While a handler runs, no interrupt of equal or lower priority can start (lesson 3). Its duration is added to their worst-case latency, every time. A 200 µs handler means any equal-priority interrupt may wait 200 µs, no matter how short its own handler is, and the main loop gets nothing at all for that time.

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

The total CPU time is the same wherever the processing runs. What deferring changes is who waits: processing in thread context can itself be interrupted, so urgent handlers keep their short latency.

STEP 2

Capture now, process later

Split every non-trivial handler in two:

  1. The handler (top half) does only what cannot wait: identify the source, read the data the hardware will overwrite (a received byte, a captured timestamp), clear the flag (lesson 4), and hand the event on through a flag, a counter or a ring buffer (lesson 5).
  2. The deferred part (bottom half) does everything else: checking, converting, filtering, logging, deciding.

Linux formalises exactly this: a threaded IRQ’s hard handler “has to check whether the interrupt originates from the device … disable the interrupt on the device and return IRQ_WAKE_THREAD”, and the thread function does the rest.

STEP 3

Where the deferred work runs

INTERACTIVEWhere deferred work can run
The path from an interrupt to deferred work for one deferral methodeventshort handler:capture, clearflag, hand onflag + main loopthe workruns: up to one loop passsimplest; latency depends on the whole loop (unit 7, lesson 6)
The path from an interrupt to deferred work for one deferral methodeventshort handler: capture, clear flag, hand onflag + main loopthe workruns: up to one loop passsimplest; latency depends on the whole loop (unit 7,lesson 6)
Deferral method
flag + main loop: up to one loop pass.

The handler’s job is to capture the event and hand it on. Where the rest runs is a design choice with different latencies and rules. Examples from CMSIS, FreeRTOS and the Linux kernel.

  • A flag or ring buffer read by the main loop: simplest; the work waits up to one loop pass (unit 7, lesson 6).
  • A low-priority software interrupt: PendSV on Cortex-M, or an otherwise unused IRQ triggered by software (the RP2040 reserves IRQs 26–31 for this, set with irq_set_pending()). It runs once no handler of equal or higher urgency is active, still at interrupt level, so it must stay bounded. RP2040 user IRQs are local to the core that pends them.
  • An RTOS task: the handler wakes a task (a notification or a queue) and requests a context switch on exit if that task is more urgent than the one interrupted. The task’s priority then sets the latency (unit 13). FreeRTOS’s xTimerPendFunctionCallFromISR instead runs a function in the RTOS timer service task, whose priority (configTIMER_TASK_PRIORITY) sets the latency.

STEP 4

What never belongs in a handler

  • Waiting: sleep_ms(), busy loops polling a flag, waiting for a UART to transmit.
  • Heavy printing: a formatted printf can take milliseconds (unit 6, lesson 6).
  • Slow or blocking buses: an I²C transaction of hundreds of microseconds.
  • Unbounded work: loops whose length depends on input data.
  • Taking locks that the interrupted code may hold: the handler waits for the code it interrupted, which can never run: a deadlock.

STEP 5

Measure the handler

Toggle a GPIO at entry and exit and look at it on a logic analyser, or read a cycle counter (unit 6, lesson 6). Record the maximum over a long run, not the average: worst-case latency depends on the longest execution. Give each handler a budget and check it in testing.

STEP 6

Worked example: the 200 µs sensor handler

At 500 events per second, the processing costs

U=r×twork=500 s−1×200 μs=0.1U = r \times t_{\text{work}} = 500\ \text{s}^{-1} \times 200\ \mu\text{s} = 0.1

10 % of the CPU either way. In the handler, it blocks equal-priority interrupts for up to 200 µs, so a UART holding one byte (86.8 µs per byte at 115 200 baud) can overflow while it runs. Deferred, the handler keeps only reading the packet into a buffer and setting a flag, 2 µs; the UART interrupt waits at most 2 µs behind it, and the 200 µs of processing runs in the main loop (or a task), where the UART interrupt can preempt it whenever a byte arrives.

MYTHS AND FACTS

Common misconceptions

Deferring saves CPU time

It moves the same work; the gain is in other interrupts’ latency.

A fast CPU makes handler length irrelevant

A blocking wait or a slow bus transaction does not get faster with the CPU.

printf in a handler is fine for debugging

It changes timing by milliseconds and can hide or create the bug being chased.

The deferred part can take its time

It still needs a latency budget; it just must not block urgent interrupts.

Check yourself

Answer in your head, then open the card.

A handler takes 150 µs and runs 1000 times per second. What fraction of the CPU does it use, and how long can an equal-priority interrupt be delayed by it?

15 % of the CPU; an equal-priority interrupt can wait up to 150 µs.

Which of these belong in a UART receive handler: reading the data register, clearing the error flags, parsing a command line, echoing the line back with printf?

Reading the data register and clearing flags; put the byte into a ring buffer. Parsing and echoing belong in the deferred part.

Why must a handler never try to take a mutex that the main loop might be holding?

The main loop cannot run while the handler waits (the handler has interrupted it), so it can never release the mutex: the handler waits for ever.

How can a handler on the RP2040 hand work to a lower-priority interrupt without any RTOS?

Claim a user IRQ (user_irq_claim_unused), set its handler (irq_set_exclusive_handler) and a lower priority, enable it (irq_set_enabled) on the same core, and call irq_set_pending() on it from the urgent handler; it runs once no handler of equal or higher urgency is active.

Sources (4)
  1. FreeRTOS Kernel, include/timers.h (xTimerPendFunctionCallFromISR) — an ISR that receives data “Query[s] the hardware to determine which interface needs processing … The actual processing is to be deferred to a task”; *pxHigherPriorityTaskWoken reports whether “a context switch should be requested before the interrupt exits”, via portYIELD_FROM_ISR()
  2. Linux kernel, kernel/irq/manage.c (request_threaded_irq) and Documentation/core-api/genericirq.rst — “@handler is still called in hard interrupt context and has to check whether the interrupt originates from the device. If yes it needs to disable the interrupt on the device and return IRQ_WAKE_THREAD which will wake up the handler thread and run @thread_fn”
  3. Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_irq/irq.h — “User IRQs are numbered 26-31 and are not connected to any hardware, but can be triggered by irq_set_pending”; user_irq_claim_unused
  4. Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — SCB ICSR with PENDSVSET (bit 28) to make the PendSV exception pending from software