Embedded Systems · Real-Time Operating Systems

Interrupt Latency, Tick Overhead, and the Kernel Cost Budget

How to decompose the delay from an external event to a task running into measurable pieces, and which kernel configuration choices move each piece.

An event occurs and, some number of microseconds later, a task reads a value that the event produced. The number is the system’s responsiveness, and it is not one quantity but a sum of quantities with different causes and different fixes. Splitting it into its parts is what makes it improvable: a delay that comes from the kernel’s interrupt masking cannot be fixed by raising the task’s priority, and a delay that comes from the tick cannot be fixed by shortening a handler.

The chain from pin to task

Start with the pin. A signal crosses into the part, sets a flag in the peripheral, and the peripheral raises an interrupt request to the controller. The controller decides whether to forward it, based on its mask and priority settings, and the core takes the exception at the next instruction boundary. Before the core takes it, it may finish the current instruction, which on some architectures can be a multi-cycle memory access; the architectural ceiling on that delay is documented as a small number of cycles.

The core then pushes context, which on an ARMv7-M part with the standard exception entry is eight registers, thirty-two bytes, and takes a fixed number of cycles, extended if the stack is not in a fast memory. It then fetches the vector and jumps to the handler. This whole path from request to first handler instruction is the part the architecture documentation quantifies, and on a small Cortex-M part it is on the order of a dozen cycles plus memory waits.

The handler runs. In the deferred design, the handler’s own work is short and its length is a choice; in the inline design, its length is the system’s latency. The handler calls the kernel’s wake function, which takes a critical section and marks the task ready. Then the handler returns, the exception exits, and — if the wake was from an interrupt and the woken task is more urgent than the interrupted task — the kernel’s pend-and-switch logic causes another exception entry, this time to the scheduler, which restores the woken task’s context.

The final piece is the task’s own first instruction, which may itself stall on an instruction fetch miss or a data access. That stall is the part that depends on the target’s caches and on the memory map, and it is the part that is hardest to bound on a modern core.

Which parts are bounded and which are measured

The parts that come from the processor and the controller have documented bounds. Exception entry latency, vector fetch time, and the register stacking cost are architectural or vendor-documented numbers. The parts that come from the software are choices: the handler’s length, the interrupt mask times in the kernel and in the application’s critical sections, the scheduler’s search cost, and the size of the woken task’s working set.

The parts that are neither are the memory system effects. A handler that runs from flash with a slow interface may spend more time fetching instructions than executing them, and the fetch time depends on the flash’s prefetch buffer state, which depends on what ran before. A task whose data is not in cache pays for the cache miss at its first access, and that cost is not in any vendor table.

The practical approach is to measure the components that can be measured and bound the rest conservatively, then to check the total against a hardware measurement of the full path. The instrumentation for the full path is a GPIO pin set by the handler and cleared by the task, or two captures of a free-running timer counter. The difference between the measured total and the sum of the bounded parts is the memory system’s contribution, and it is worth knowing even if it is not under the designer’s control.

The tick and its cost

Every periodic tick costs an interrupt entry, the handler’s work, and an interrupt exit, whether or not anything is due. At a 1 kHz tick with a 10-microsecond handler, the tick alone consumes one percent of the processor before any task runs. At 10 kHz, it is ten percent. The cost is linear in the tick rate and independent of the application’s load, which is why the tick rate is one of the first parameters to reconsider when the processor is short.

The handler’s work scales with what is due at that tick: expiring timers, waking delayed tasks, deciding whether a time slice expired. A countdown-based timeout implementation makes the handler’s cost proportional to the number of blocked tasks, as noted in the discussion of blocking primitives; an absolute-expiry list makes it proportional to the number of tasks that actually expire, which is usually zero or one.

The other cost the tick imposes is jitter on task releases. A task that asks to wait 10 ticks resumes at the tick boundary, not at the exact time it requested, so its period has an uncertainty of one tick. A system with a 1 kHz tick has a 1-millisecond jitter on every delayed wake, and a control loop that expects a 1-millisecond sample interval cannot be built on it. The alternatives are a higher tick rate, which costs processor time, or a hardware timer per task or per group, which costs peripherals and configuration complexity.

Kernels that support tickless operation remove the periodic interrupt entirely: the timer is programmed for the next expiration and the processor sleeps until then. The cost is in the bookkeeping — the next expiration must be recomputed on every block, and the wrap-around logic for the counter becomes part of the kernel’s critical path. The benefit is that an idle system draws the sleep current instead of paying the tick’s percent of the active current, and a busy system pays only for the expirations it actually needs.

Where the priority inversion of latency hides

A high-priority task’s wake-up latency can be dominated by something that is not on its own path at all: the interrupt mask held by a lower-priority task’s critical section. When a task masks interrupts to protect a data structure, every interrupt in the system is delayed until the mask is released, regardless of priority. The mask time is therefore a system-wide latency floor, and it belongs in the same table as the handler lengths.

The kernel’s own masks are the ones the application cannot avoid. Ready-list updates, queue operations, and scheduler decisions all take a mask, and their durations are bounded by the kernel’s implementation. Kernels that publish these numbers as a “maximum interrupt disable time” are publishing the floor of the system’s responsiveness; a kernel that does not publish them leaves the designer to measure the worst case with instrumentation.

The application’s masks are the ones that grow silently. A driver that disables interrupts around a multi-byte SPI transfer of a hundred bytes at a few megabits per second holds the mask for a hundred microseconds or more, and every task in the system pays. The fix is to use the peripheral’s own buffering or DMA so that the critical section covers a register write rather than a transaction, which is a design decision that has to be made before the driver is written.

Configuring the measurement

Three settings determine whether the measured number means anything.

The interrupt controller’s priority scheme determines whether handlers can nest and whether a high-priority interrupt can preempt a low-priority handler. A configuration with all interrupts at the same priority serialises the handlers, which means the worst-case latency includes every handler that can run before the one of interest. A configuration with distinct priorities allows nesting and shortens the worst case for the highest handler, at the cost of larger stack use and more complex analysis.

The vector table’s placement determines the fetch latency. A table in slow memory adds a fetch to every interrupt; a table in fast memory removes it. On parts with a preloaded vector table in the core, this is not under the designer’s control.

The stack’s placement determines the stacking cost. Exception stacking writes thirty-two bytes to the current stack; if that stack is in a region with wait states, the write extends the entry latency. The linker script places the stack, and moving it to the fastest RAM is a change that costs nothing but a map edit.

/* measure the full path with the core's cycle counter, no GPIO needed */
volatile uint32_t t_event, t_task, worst;
static TaskHandle_t exti_task_handle;

void EXTI_IRQHandler(void)
{
    BaseType_t woken = pdFALSE;

    t_event = DWT->CYCCNT;             /* first thing in the handler */
    /* clear the peripheral's pending flag here */
    vTaskNotifyGiveFromISR(exti_task_handle, &woken);
    portYIELD_FROM_ISR(woken);
}

void exti_task(void *arg)
{
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        t_task = DWT->CYCCNT;          /* first thing after the wake */
        uint32_t delta = t_task - t_event;   /* correct across a wrap */
        if (delta > worst)
            worst = delta;
    }
}

The subtraction form is the reason both captures are unsigned: t_task - t_event gives the correct elapsed count even across a counter wrap, as long as the interval is less than half the counter range. A comparison t_task > t_event does not. The counter itself has to be switched on before any of this means anything: on ARMv7-M the cycle counter sits in the debug watchpoint and trace unit, which is enabled by a bit in the debug exception and monitor control register, and it stops when the core is halted.

Capturing in the handler and in the task changes the timing slightly, since the capture instructions are themselves part of the path. The usual practice is to capture at the very first and very last point and to treat the added few cycles as a known offset.

What the number is for

The latency budget is not a single figure to publish; it is a table with a row per path and a column per component, and its purpose is to show which change moves which number. A design that needs a faster wake-up can shorten the handler, raise the task’s priority, reduce the interrupt mask times, move the stack, or change the tick policy, and the table says how much each is worth.

The same table feeds the response-time analysis as the kernel overhead term, and it feeds the hardware selection: a part whose worst-case interrupt entry is longer than the deadline allows cannot be fixed in software. The analysis in response-time analysis for fixed-priority sets assumes these numbers are known, and stack and memory budgeting covers the memory side of the same configuration.

References

  1. ARMv7-M Architecture Reference Manual — ARM Limited
  2. Cortex-M4 Devices Generic User Guide: Exception Model — ARM Limited
  3. Specification of Operating System (AUTOSAR Classic Platform R22-11) — AUTOSAR