In short: Interrupt latency is the delay between a peripheral asserting an interrupt and your handler doing useful work; you minimize it by understanding the fixed hardware costs, eliminating the variable jitter (priority inversion, critical sections, flash wait-states), configuring the NVIC correctly, and keeping every ISR brutally short.
When a timer overflows or a UART receives a byte, how fast can your firmware actually respond? On a Cortex-M that question has a surprisingly precise answer, and the gap between the best case and the worst case is where real-time systems live or die. This article dissects where the cycles go, why they vary (jitter), and the concrete techniques that tame both.
What "interrupt latency" actually measures
Interrupt latency is the elapsed time from the moment a peripheral asserts its interrupt request to the moment the first instruction of the corresponding Interrupt Service Routine (ISR) executes. It is not one number but a sum of distinct contributions, and only some of them are under your control. If you have not yet met the mechanism itself, start with our primer on how interrupts work in embedded systems, then come back for the timing.
The fixed hardware floor
On an ARM Cortex-M3 or M4, entering an exception costs a documented, deterministic number of cycles. The core finishes or abandons the current instruction, stacks eight registers (R0–R3, R12, LR, PC, xPSR) automatically, and fetches the vector. ARM specifies this as 12 cycles from IRQ assertion to the first ISR instruction when the memory system has zero wait-states. The Cortex-M0+ is similar at around 15 cycles; higher-end M7 cores pipeline more deeply. This floor is essentially free of jitter — it is the baseline you measure everything else against.
Key fact: At a 180 MHz clock, 12 cycles is about 67 nanoseconds. If your measured latency is microseconds, the hardware floor is not your problem — jitter is.
Where the jitter comes from
Jitter is the variation in latency across occurrences. A hard real-time deadline must be met in the worst case, so a handler that is usually 100 ns but occasionally 8 µs is effectively an 8 µs handler. The dominant sources:
- Disabled interrupts (critical sections). Every
__disable_irq()or spinlock that masks interrupts adds its full duration to the worst-case latency of anything that fires during it. A 5 µs critical section guarantees up to 5 µs of jitter. - Higher- or equal-priority ISRs running first. If a more urgent interrupt is already executing, yours waits. This is legitimate prioritization, but it must be budgeted.
- Multi-cycle instructions that cannot be abandoned. An integer divide or an unaligned load-multiple may delay the core's response until it completes.
- Flash wait-states and bus contention. Running from flash at high clocks inserts wait-states on the vector fetch and ISR prologue. DMA stealing bus cycles adds non-determinism.
- Cache and prefetch misses on M7-class parts: a cold instruction cache line for the ISR adds variable fetch time.
The NVIC: priorities, grouping, and preemption
The Nested Vectored Interrupt Controller is what makes Cortex-M interrupt handling deterministic. Each interrupt gets an 8-bit priority field, though most parts implement only the top 3 or 4 bits (so 8 or 16 distinct levels). Lower numeric value means higher priority. Priority 0 preempts priority 1.
The subtle part is priority grouping, set via NVIC_SetPriorityGrouping(). The priority byte splits into a preemption field and a sub-priority field. Preemption decides whether interrupt A can interrupt a running handler B; sub-priority only breaks ties among simultaneously pending interrupts of equal preemption. A common mistake is assigning two peripherals different sub-priorities and expecting one to preempt the other — it will not.
| Mechanism | Effect | Typical use |
|---|---|---|
| Preemption priority | Nested interrupt — higher can interrupt lower mid-ISR | Motor fault vs. UART |
| Sub-priority | Order of service when both already pending | Two equal timers |
| Tail-chaining | Back-to-back ISRs skip redundant pop/push (~6 cycles saved) | Bursty peripherals |
| BASEPRI register | Mask only below a threshold, leaving critical IRQs live | Short critical sections |
Key fact: Use BASEPRI instead of __disable_irq() for RTOS critical sections. It lets your highest-priority "zero-latency" interrupts keep firing, cutting worst-case jitter for the one deadline that matters most.
Keeping ISRs short: the discipline that matters most
The single most reliable way to control system-wide latency is to make every handler do the minimum and return. A long ISR blocks everything at or below its priority. The pattern:
- Acknowledge and capture. Clear the peripheral flag, read the hardware register (a received byte, a captured timestamp) into a buffer, and leave.
- Defer the work. Signal a task — post to an RTOS queue, give a semaphore, or set a flag polled by a bottom-half. FreeRTOS calls this the "deferred interrupt" pattern with
xSemaphoreGiveFromISR(). - Never block. No
malloc, no busy-wait on another peripheral, no logging over UART inside an ISR. - Avoid floating point unless you have budgeted the extra context (lazy FPU stacking adds cycles when the ISR touches the FPU).
A UART RX handler that copies one byte to a ring buffer runs in tens of cycles; one that parses a command inline can run for thousands and will drop bytes at high baud. For a deeper look at structuring the deferred half, see our guide to RTOS task scheduling.
Measuring it for real
Do not guess — instrument. Toggle a GPIO as the first and last instruction of the ISR and watch it on a logic analyzer or scope; the delay from the triggering edge to the first GPIO toggle is your actual latency, jitter and all. On Cortex-M, the DWT cycle counter (DWT->CYCCNT) gives sub-microsecond software timing. Tools from the trace ecosystem — SEGGER SystemView, or an ETM trace captured with a Lauterbach or Siemens debug probe — let you see nested preemption and tail-chaining without perturbing timing. Hardware timing teams at firms like Avecas VLSI design services rely on the same cycle-accurate trace discipline when characterizing custom silicon.
Want to go from theory to a scope capture on real hardware? CourseTron's embedded systems track walks you through NVIC configuration, deferred interrupt patterns, and live latency measurement on STM32. Explore the embedded courses →
Frequently asked questions
What is a typical interrupt latency on Cortex-M4?
The hardware floor is 12 cycles to the first ISR instruction with zero wait-states — roughly 67 ns at 180 MHz. Real-world worst case depends on your longest critical section and higher-priority handlers, commonly 1–10 µs in a well-written system.
Does a higher priority number mean a more urgent interrupt?
No — it is the opposite. In the NVIC, a lower numeric priority value is more urgent. Priority 0 preempts priority 5. This trips up almost everyone once.
Why use BASEPRI instead of disabling all interrupts?
BASEPRI masks only interrupts at or below a chosen threshold, so your most time-critical ("zero-latency") interrupts keep firing even inside a critical section. Globally disabling interrupts adds the whole critical-section duration to the worst-case latency of every interrupt in the system.
What is tail-chaining?
When one ISR finishes and another is already pending, the Cortex-M core skips the redundant register pop-then-push and chains directly into the next handler, saving about six cycles. It happens automatically and reduces latency for bursty interrupt loads.