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:
- 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).
- 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
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
xTimerPendFunctionCallFromISRinstead 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
printfcan 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
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)
- 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()
- 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”
- 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
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — SCB ICSR with PENDSVSET (bit 28) to make the PendSV exception pending from software