HomeAbout UsContact Us

Reducing ARM Cortex-M Interrupt Latency with Tail-Chaining

By Jithin Tom
Published in Embedded C/C++
July 18, 2026
3 min read
Reducing ARM Cortex-M Interrupt Latency with Tail-Chaining

Table Of Contents

01
Interrupt Latency Components on Cortex-M
02
Tail-Chaining Mechanism
03
Priority Configuration for Optimal Tail-Chaining
04
Minimizing Stacking Overhead
05
Measuring Actual Latency
06
Summary
07
Related Reading
08
References
09
Frequently Asked Questions

Interrupt latency on ARM Cortex-M processors is not a fixed number—it depends on processor state, priority configuration, and whether tail-chaining can occur. For hard real-time systems where microseconds determine whether a motor commutates on time or a communication frame is acknowledged, understanding and minimizing this latency is mandatory.

Interrupt Latency Components on Cortex-M

The total interrupt latency consists of concurrent hardware operations:

+-----------------------------------------------------+--------------------------+
| INTERRUPT ENTRY (12 cycles total) | ISR ENTRY |
| + NVIC detects interrupt & performs priority check | |
| + Pipeline flush & vector table fetch | First ISR instruction |
| + Concurrent Stacking: Push R0-R3, R12, LR, | executes |
| PC, xPSR to stack | |
+-----------------------------------------------------+--------------------------+

Entry latency: 12 cycles on Cortex-M3/M4/M7/M33 (assuming zero wait-state memory). The architecture is highly optimized so that the state preservation (stacking the 8 integer registers) occurs in parallel with the instruction fetch of the vector table.

Variable penalties: If the FPU is enabled and lazy stacking is active, the initial entry latency is preserved at 12 cycles, but a penalty of 18-34 cycles is incurred later if the ISR executes a floating-point instruction. Unaligned stack pointers at exception entry can also add a 1 cycle penalty.

Tail-Chaining Mechanism

Tail-chaining eliminates the stacking/unstacking overhead when returning from an exception if there are pending interrupts. Instead of performing a full context restore to return to thread mode (or a lower-priority background ISR), the processor branches directly to the next pending ISR.

INTERRUPT A INTERRUPT B (Pending)
+----------------------+ +----------------------+
| Entry (12 cyc) | | |
| ISR Body | | |
| | TAIL-CHAIN | |
| BX LR (EXC_RETURN) | ----------> | (skipped context ops)|
| Exit/Unstack (10 cyc)| (6 cyc) | ISR Body |
| | | BX LR |
| (Return to Thread) | | Exit/Unstack (10 cyc)|
| B Entry (12 cyc) | | |
+----------------------+ +----------------------+
Standard overhead between ISRs: 22 cycles (Exit 10 + Entry 12)
Tail-chain overhead: 6 cycles
Total Savings: 16 cycles per chained interrupt

The tail-chain sequence takes only 6 cycles: the processor recognizes the pending interrupt during the current ISR’s exception return, fetches the new vector, and directly enters the next ISR—skipping the full unstack/restack cycle.

Critical condition: Tail-chaining occurs for pending interrupts that have a lower or equal logical priority (a higher or equal numerical group priority value) than the current ISR, which prevented them from preempting earlier. Subpriority does not affect preemption or tail-chaining eligibility.

Priority Configuration for Optimal Tail-Chaining

void NVIC_ConfigPriorityGrouping(void) {
// Configure 4 bits group priority, 4 bits subpriority (on 8-bit priority systems)
// PRIGROUP = 3 (0b011). CMSIS handles the required VECTKEY write internally.
NVIC_SetPriorityGrouping(3);
}

With 4-bit group priority (16 levels), you can assign related peripherals to the same group priority but different subpriorities. This ensures they tail-chain rather than preempt each other:

// Get current grouping to ensure encoding matches hardware configuration
uint32_t pg = NVIC_GetPriorityGrouping();
// UART1 and UART2 at same group priority (3), different subpriorities
// They will tail-chain when both fire, never preempt each other
NVIC_SetPriority(UART1_IRQn, NVIC_EncodePriority(pg, 3, 1)); // Group=3, Sub=1
NVIC_SetPriority(UART2_IRQn, NVIC_EncodePriority(pg, 3, 2)); // Group=3, Sub=2
// DMA at higher group priority (2) - will preempt UARTs
NVIC_SetPriority(DMA1_Stream5_IRQn, NVIC_EncodePriority(pg, 2, 0)); // Group=2

Common mistake: Misunderstanding vendor HAL macros. For example, in STM32’s HAL, NVIC_PRIORITYGROUP_0 actually maps to a raw PRIGROUP value of 7, which assigns 0 bits to group priority. This disables preemption entirely—every interrupt runs to completion, meaning higher-urgency events cannot interrupt lower-urgency ones. Make sure you know whether you are configuring raw ARM registers or using a vendor abstraction layer.

Minimizing Stacking Overhead

Leverage FPU Lazy Stacking

Lazy stacking (enabled by default via FPCCR.LSPEN) is an optimization that prevents the FPU registers from being stacked during interrupt entry, preserving the 12-cycle entry latency.

The penalty (18-34 cycles) only occurs if the ISR actually executes a floating-point instruction. Thus, avoiding floating-point math in your most time-critical ISRs ensures they maintain the lowest possible latency without disabling FPU support globally.

Align Stack to 8-Byte Boundary

The Cortex-M stack must be 8-byte aligned at exception entry. Misaligned stacks force the processor to adjust SP, adding a 1 cycle penalty:

// In startup code or linker script, ensure _estack is 8-byte aligned
// .stack (ORIGIN(RAM) + LENGTH(RAM)) : ALIGN(8)
// Or in C:
__attribute__((aligned(8))) static uint8_t stack_buffer[4096];

Avoid __attribute__((interrupt))

Unlike older ARM architectures, Cortex-M processors handle exception context saving and EXC_RETURN entirely in hardware using the standard AAPCS C calling convention.

// Correct: Use standard C functions for Cortex-M ISRs
void DMA1_Stream5_IRQHandler(void) {
// Handler body
// Standard 'bx lr' return triggers hardware unstacking
}

Using __attribute__((interrupt)) on Cortex-M is not only unnecessary but can degrade performance by causing the compiler to emit redundant stack-alignment instructions.

Measuring Actual Latency

Use the cycle counter (DWT->CYCCNT) for cycle-accurate measurement. Crucially, you must capture the start time right before triggering the interrupt, and the end time as the very first operation inside the handler:

#define DWT_CTRL (*(volatile uint32_t*)0xE0001000)
#define DWT_CYCCNT (*(volatile uint32_t*)0xE0001004)
#define DWT_LAR (*(volatile uint32_t*)0xE0001FB0)
#define DEMCR (*(volatile uint32_t*)0xE000EDFC)
void DWT_Init(void) {
DEMCR |= (1UL << 24); // Enable DWT
#if (__CORTEX_M >= 7)
DWT_LAR = 0xC5ACCE55; // Unlock DWT (required for Cortex-M7/M33)
#endif
DWT_CTRL |= (1UL << 0); // Enable cycle counter
}
volatile uint32_t trigger_cycles;
volatile uint32_t isr_entry_cycles;
void Trigger_Test_Interrupt(void) {
trigger_cycles = DWT_CYCCNT;
NVIC_SetPendingIRQ(EXTI0_IRQn); // Software trigger
__DSB(); // Ensure trigger executes
}
void EXTI0_IRQHandler(void) {
isr_entry_cycles = DWT_CYCCNT; // Must be first instruction!
// ... ISR body ...
}

Measure isr_entry_cycles - trigger_cycles. On Cortex-M4 at 168 MHz, expect exactly 12 cycles (71 ns) for integer-only ISRs from zero wait-state memory. Wait states (like executing from Flash) will increase this value.

Summary

TechniqueLatency BenefitNote
Tail-chaining (same group priority)Saves ~16 cycles per chained IRQRequires careful priority grouping
FPU lazy stacking (default)Preserves 12-cycle entryPenalty deferred to first FPU instruction
8-byte stack alignmentPrevents 1 cycle penaltyGuaranteed by most modern linkers
Avoid interrupt attributeAvoids redundant compiler instructionsCortex-M natively supports C functions
Priority grouping (4-bit group)Enables tail-chaining16 preemption levels max

The single most impactful optimization is priority grouping that enables tail-chaining for related interrupt sources. A UART+DMA pair at the same group priority will tail-chain reliably, saving ~16 cycles per transfer completion interrupt. For hard real-time deadlines, measure with DWT_CYCCNT—don’t rely on datasheet minimums.

  • Lock-Free Ring Buffers for ISR-to-Task Communication
  • Cortex-M Fault Handlers and Exception Handling in Embedded C

References

  1. ARM, Cortex-M4 Devices Generic User Guide, ARM DUI 0553A, Section 2.3.2 “Interrupt Latency”
  2. ARM, Cortex-M7 Processor Technical Reference Manual, ARM DDI 0489F, Chapter 3 “Nested Vectored Interrupt Controller”
  3. Joseph Yiu, The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors, 3rd Ed., Newnes, 2014, Chapter 12 “Interrupts and Exceptions”
  4. ARM, Application Note 321: Cortex-M Series Processor Exception Handling, 2016
  5. NXP, AN12253: Optimizing Interrupt Latency on Cortex-M Cores, 2021
  6. STMicroelectronics, PM0214: STM32F4 Series Programming Manual, Section 5.2 “Nested Vectored Interrupt Controller (NVIC)”

Frequently Asked Questions

What is ARM Cortex-M interrupt latency?

ARM Cortex-M interrupt latency is the time from when an interrupt signal is asserted to when the first instruction of the ISR executes. On Cortex-M3/M4/M7/M33, this is typically a fixed 12 cycles total for integer-only contexts, as hardware stacking occurs concurrently with the vector fetch.

How does tail-chaining reduce interrupt latency?

Tail-chaining skips the full context restore and re-stack when returning from an exception if a pending interrupt is eligible to preempt the background context. The processor branches directly from the current ISR's tail to the next ISR's entry, saving ~16 cycles per chained interrupt.

When does tail-chaining not occur on Cortex-M?

Tail-chaining does not occur when: (1) there are no pending interrupts with sufficient priority to preempt the background context being returned to, or (2) pending interrupts are masked by PRIMASK or BASEPRI. Note that if a higher-priority interrupt arrives during the ISR body, it nests (preempts) rather than tail-chaining.

How does interrupt priority grouping affect latency?

Priority grouping (PRIGROUP in AIRCR) splits the 8-bit priority field into group priority and subpriority. Only group priority determines preemption. Subpriority only orders pending interrupts of the same group. Misconfiguring PRIGROUP can cause unintended preemption or prevent tail-chaining between related interrupts.

What is the worst-case interrupt latency on Cortex-M7 with FPU?

With FPU enabled and lazy stacking, entry latency remains 12 cycles before the first ISR instruction. The worst-case penalty (up to 34 cycles) only occurs later if the ISR executes a floating-point instruction, triggering the lazy stacking.

Tags

arm-cortex-minterrupt-latencytail-chaininginterrupt-prioritycortex-m4cortex-m7cortex-m33

Share


Previous Article
CRC and Checksum Algorithms in Embedded C: A Practical Guide
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Debugging Hard Faults on ARM Cortex-M: A Systematic Approach
Debugging Hard Faults on ARM Cortex-M: A Systematic Approach
July 15, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media