Embedded

Interrupt latency in embedded

By CourseTron Team ·  7 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    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.

    Latency = fixed hardware cost + variable jitter Cortex-M3/M4: 12 cycles to the first ISR instruction (zero wait-states) syncfinish instr.arbitratestack 8 regsvector fetch IRQ asserted ISR body runs Critical sections and wait-states add jitter on top of this floor. CourseTron · coursetron.com
    The latency budget: synchronization, instruction completion, state push, and vector fetch all precede your code.

    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.
    IRQ held off while interrupts are masked interrupts masked stall = jitter IRQ asserted serviced CourseTron · coursetron.com
    The pulse reaches the mask window, stalls until interrupts re-enable, then is serviced — the stall is pure jitter.

    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.

    MechanismEffectTypical use
    Preemption priorityNested interrupt — higher can interrupt lower mid-ISRMotor fault vs. UART
    Sub-priorityOrder of service when both already pendingTwo equal timers
    Tail-chainingBack-to-back ISRs skip redundant pop/push (~6 cycles saved)Bursty peripherals
    BASEPRI registerMask only below a threshold, leaving critical IRQs liveShort 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.

    Nested preemption on the NVIC Main code ISR A, prio 4 ISR B, prio 1 mainmain resumes ISR AA resumes ISR B B (lower number) preempts A mid-ISR; A resumes when B returns. CourseTron · coursetron.com
    Preemption priority: a lower numeric value interrupts a running higher-numbered handler, then the earlier handler resumes.

    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.

    Advertisement