HomeAbout UsContact Us

ARM Cortex-M NVIC Interrupt Latency Optimization Techniques

By Jithin Tom
Published in Embedded OS
September 21, 2026
9 min read
ARM Cortex-M NVIC Interrupt Latency Optimization Techniques

Table Of Contents

01
Hardware Architecture & Cycle-Accurate Latency Mechanics
02
Mathematical Models for Interrupt Latency
03
Architectural Deep Dive: Tail-Chaining, Late-Arriving, and Pop-Preemption
04
Debunking the PRIGROUP Latency Fallacy
05
FPU Lazy Stacking (FPCCR.LSPEN) Optimization
06
Memory Subsystem & Vector Table Relocation (SCB->VTOR)
07
Compiler Prologues & AAPCS Register Pressure
08
Memory Barriers & Peripheral Store Buffer Hazards
09
Complete Implementation: Deterministic 100 kHz Timer Interrupt
10
Latency Optimization Checklist
11
References
12
Related Reading
13
Frequently Asked Questions

Interrupt latency is the cornerstone metric governing determinism in real-time embedded systems. In hard real-time domains—such as motor field-oriented control (FOC), high-speed industrial Ethernet transceivers, medical respirators, and switch-mode power topologies—a delayed or jittery interrupt response degrades control loop phase margins and can trigger catastrophic hardware faults.

In the ARM Cortex-M processor family (Armv7-M, Armv8-M, and Armv6-M), the Nested Vectored Interrupt Controller (NVIC) is integrated directly into the processor core rather than situated as an external peripheral on the system bus. While this tight coupling provides industry-leading responsiveness, developers often observe significant jitter and unexpected delays in production firmware. These delays stem from multi-cycle instruction retirement, flash memory wait states, floating-point register stacking penalties, store-buffer drain latencies, and misconfigured priority grouping schemes.

This guide analyzes the cycle-accurate mechanics of Cortex-M exception entry, debunks common architectural misconceptions regarding interrupt controller timing, and presents production-grade C techniques to achieve minimal, deterministic latency.

+---------------------------------------------------------------------------------------+
| CORTEX-M EXCEPTION ENTRY PIPELINE FLOW |
+---------------------------------------------------------------------------------------+
Peripheral / External Event
|
v
[ Interrupt Request Asserted ]
|
v
[ Clock Synchronization Stage ] -------------> 1 to 3 Core Clock Cycles
|
v
[ Priority Arbitration & Masking ] ----------> Evaluated combinationally against BASEPRI
|
v
[ Instruction Boundary Resolution ] ---------> Completes current instruction or abandons
| interruptible multi-cycle load/store
+-----------------------+
| |
v v
(Data Bus: AHB/AXI) (Code Bus: I-Code)
+-------------------------+ +-------------------------+
| Context Stacking | | Vector Address Fetch |
| Pushes R0-R3, R12, LR, | | Reads 32-bit vector |
| PC, xPSR to SP_main/ | | entry from VTOR + offset
| SP_process (12 cycles)| | in parallel with stack |
+-------------------------+ +-------------------------+
| |
+-----------+-----------+
|
v
[ Pipeline Fetch & Decode ]
|
v
[ First Instruction of ISR Executes ]

Figure 1: Cycle-accurate exception entry flow in ARM Cortex-M3/M4/M7 cores, highlighting concurrent dual-bus operations.


Hardware Architecture & Cycle-Accurate Latency Mechanics

In legacy microcontrollers (such as ARM7TDMI or classic 8051/PIC devices), interrupt handling required software dispatch routines that saved registers via assembly instructions, read peripheral interrupt vectors, and branched to handler addresses. The ARM Cortex-M NVIC automates this sequence entirely in hardware.

1. Synchronization and Core Recognition (1 to 3 Cycles)

External interrupt signals routed through chip-level GPIO pins or peripheral logic must pass through dual flip-flop synchronizers to prevent metastability across asynchronous clock boundaries. This adds 1 to 3 core processor clock cycles before the NVIC peripheral logic registers the interrupt as pending.

2. Instruction Boundary Resolution and Interruptible Instructions

ARM Cortex-M processors take interrupts on instruction boundaries. If a single-cycle instruction (such as ADD, MOV, or AND) is currently in the execute stage of the pipeline, it completes in that cycle.

However, if a multi-cycle instruction is executing, standard Cortex-M implementations behave as follows:

  • Load/Store Multiple (LDM, STM, PUSH, POP): On Cortex-M3, Cortex-M4, and Cortex-M7 cores, multi-cycle load and store operations are interruptible and abandonable. The core abandons the multi-cycle instruction mid-stream without updating register state, services the interrupt immediately, and restarts the instruction upon return (PC points to the abandoned instruction). Alternatively, on cores utilizing the Interrupt-Continuable Instruction (ICI) bits in the xPSR, execution resumes from the interrupted register transfer.
  • Hardware Integer Divide (UDIV, SDIV): Division takes between 2 and 12 cycles depending on operand magnitude. On Cortex-M3/M4, division operations can be abandoned immediately upon interrupt arrival.
  • Memory Bus Wait States: If a load or store instruction is stalled waiting for external bus access or slow peripheral acknowledge, the processor cannot abandon the bus transaction until the bus cycle terminates. This bus stall directly inflates interrupt latency.

3. Dual-Bus Parallel Stacking and Vector Fetching

The hallmark of Cortex-M exception entry is hardware context stacking performed concurrently with vector fetching:

  • Stacking (Data Bus): The processor automatically pushes eight 32-bit registers onto the active stack pointer (MSP or PSP): R0, R1, R2, R3, R12, LR (R14), PC (R15), and xPSR.
  • Vector Fetch (Instruction Bus): Simultaneously, the core outputs the address VTOR + (IRQn + 16) * 4 onto the code bus (I-Code or Flash interface) to retrieve the 32-bit starting address of the target ISR.

Because the Cortex-M Harvard architecture features independent Code (I-Code) and System/Data (D-Code / System AHB) buses, the vector fetch overlaps with register stacking.

On Cortex-M3, Cortex-M4, and Cortex-M33 cores operating with zero-wait-state memory, this hardware sequence completes in exactly 12 clock cycles.

+-----------------------------------------------------------------------------------------+
| CORE LATENCY & TAIL-CHAINING COMPARISON TABLE |
+-------------------+--------------------+------------------------+-----------------------+
| Processor Core | Baseline Entry | Tail-Chaining Latency | Hardware Architecture |
| | (Zero Wait States) | (Zero Wait States) | Pipeline Features |
+-------------------+--------------------+------------------------+-----------------------+
| Cortex-M0 | 16 cycles | N/A (not supported) | 2-stage von Neumann |
| Cortex-M0+ | 15 cycles | 13 cycles | 2-stage von Neumann |
| Cortex-M3 | 12 cycles | 6 cycles | 3-stage Harvard |
| Cortex-M4 / M4F | 12 cycles | 6 cycles | 3-stage Harvard + DSP |
| Cortex-M7 | 12 cycles (TCM) | 6 cycles (TCM) | 6-stage dual-issue |
| Cortex-M23 | 15 cycles | 13 cycles | 2-stage TrustZone |
| Cortex-M33 | 12 cycles (NS->NS) | 6 cycles | 3-stage TrustZone |
+-------------------+--------------------+------------------------+-----------------------+

[!NOTE] The Cortex-M0 (Armv6-M) does not implement tail-chaining. Each ISR exit performs a full unstack, and each new ISR entry performs a full stack push, even when back-to-back interrupts are pending.


Mathematical Models for Interrupt Latency

To calculate worst-case interrupt latency (WCIL) for safety-critical systems, system architects must account for hardware synchronization, bus stalls, memory wait states, and software prologues.

Comprehensive Worst-Case Latency Formulation

On Cortex-M3/M4/M7/M33 cores with independent Code and Data buses, stacking and vector fetching execute concurrently. The hardware entry cost is therefore the longer of the two operations, not their sum:

T_latency = T_sync + max(T_insn, T_crit) + max(T_stack, T_vector) + T_prologue
Where:
T_sync = Hardware clock synchronization delay (1 to 3 core cycles)
T_insn = Maximum non-abandonable instruction stall or bus wait state
T_crit = Longest atomic critical section duration (PRIMASK or BASEPRI)
T_stack = Context stacking duration across data bus (12 cycles at zero WS)
T_vector = Vector fetch duration across code bus (overlapped with T_stack)
T_prologue = Software execution time of compiler-generated stack frame (R4-R11)

On von Neumann bus cores (Cortex-M0, M0+, M23), stacking and vector fetching share a single bus and execute sequentially. On these cores, use T_stack + T_vector instead of max(T_stack, T_vector).

Context Stacking Latency with Memory Wait States

When the stack pointer references memory that requires bus wait states (e.g., external SRAM, PSRAM, or contested internal multi-master SRAM), stacking stalls the processor pipeline:

Cycles_stack = 12 + (8 * W_RAM)
Where:
12 = Baseline entry cost at zero wait states (includes 8 register pushes
plus pipeline sequencing and bus arbitration overhead)
8 = Number of additional wait-state penalties (one per 32-bit register:
R0-R3, R12, LR, PC, xPSR)
W_RAM = Data memory write wait states per 32-bit transfer

Vector Fetch Latency with Flash Wait States

If the Vector Table Offset Register (SCB->VTOR) points to Flash memory operating with wait states, the vector read stalls completion of the exception sequence:

Cycles_vector = 2 + W_Flash
Where:
2 = Baseline internal bus transfer cycles for I-Code transaction
W_Flash = Flash read wait states (e.g., 5 wait states on STM32F4 at 168 MHz)

If W_Flash > 0 and the instruction bus is slower than the data bus stack push, vector retrieval becomes the critical path, extending entry latency beyond 12 cycles.


Architectural Deep Dive: Tail-Chaining, Late-Arriving, and Pop-Preemption

The Cortex-M NVIC implements three dedicated hardware mechanisms designed to eliminate redundant stacking operations when handling bursts of interrupts.

CASE A: Standard Sequential Interrupt Handling (No Tail-Chaining)
+---------------+---------------+---------------+---------------+---------------+
| ISR A Executes| Full Unstack | Idle/Thread | Full Stacking | ISR B Executes|
| | (10-12 cycles)| (1+ cycles) | (12 cycles) | |
+---------------+---------------+---------------+---------------+---------------+
Total Inter-ISR Transition: ~22 to 25 Clock Cycles
CASE B: NVIC Tail-Chaining Active
+---------------+---------------+---------------+
| ISR A Executes| Tail-Chain | ISR B Executes|
| | (6 cycles) | |
+---------------+---------------+---------------+
Total Inter-ISR Transition: EXACTLY 6 Clock Cycles (68% to 76% Latency Reduction)

Figure 2: Execution timeline comparing conventional unstack/restack transitions against NVIC hardware tail-chaining.

1. Tail-Chaining (6 Cycles on Cortex-M3/M4/M7/M33)

When an ISR finishes execution and another interrupt is pending with eligible priority, returning to the interrupted background thread only to immediately re-enter an exception would waste over 20 clock cycles (unstacking 8 registers, executing BX LR, and immediately restacking 8 registers).

Instead, the NVIC intercepts the BX LR (EXC_RETURN) sequence:

  1. It skips the popping of R0-R3, R12, LR, PC, and xPSR.
  2. It keeps the existing stack frame intact in SRAM.
  3. It fetches the vector of the newly pending ISR.
  4. The transition takes only 6 clock cycles.

2. Late-Arriving Optimization

If Interrupt A (low priority) asserts first, the NVIC begins the 12-cycle context stacking sequence. If Interrupt B (high priority) asserts on cycle 7 of this stacking phase:

  • The core does not abort or restart stacking.
  • It allows the current 12-cycle stacking sequence to complete normally using the same stack frame.
  • It dynamically switches the parallel vector fetch to read Interrupt B’s vector instead of Interrupt A’s vector.
  • Interrupt B begins executing at cycle 12.

When Interrupt B finishes, Interrupt A is serviced via tail-chaining in 6 cycles. This prevents high-priority interrupts from being blocked by lower-priority context saves.

3. Pop-Preemption

If a higher-priority interrupt arrives while the processor is in the middle of unstacking registers (popping context) at the conclusion of an ISR, the NVIC halts the pop sequence, preserves the remaining stack frame, and switches directly to servicing the new interrupt.


Debunking the PRIGROUP Latency Fallacy

A persistent myth in embedded software engineering claims that certain PRIGROUP configurations in SCB->AIRCR take “extra clock cycles to evaluate” due to asymmetric bitfield decoding in hardware.

[!IMPORTANT] Hardware Fact: The NVIC priority arbitration circuit is a fully parallel, fixed-width combinational comparator implemented in silicon gates. Regardless of whether PRIGROUP is set to 0, 3, 5, or 7, priority comparison completes in the exact same fraction of a clock cycle. It introduces zero additional comparison cycles.

The Real Impact of Priority Grouping on Latency: Head-of-Line Blocking

The real latency danger of PRIGROUP is logical rather than electrical. The 8-bit priority register (of which typical silicon vendors implement the top 3, 4, or 5 bits) is split into two fields:

  1. Preemption Priority (Group Priority): Determines whether a pending interrupt can preempt a currently executing ISR.
  2. Subpriority: Determines execution order when multiple interrupts with identical preemption priority are pending simultaneously. Subpriority NEVER allows preemption of an active ISR.
+---------------------------------------------------------------------------------------+
| 4-BIT NVIC PRIORITY REGISTER SPLIT (STM32 EXAMPLE) |
+---------------------------------------------------------------------------------------+
| Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 (0) | Bit 2 (0) | Bit 1 (0) | Bit 0 (0) |
+-------+-------+-------+-------+-----------+-----------+-----------+-------------------+
|<-------- Preemption --------->| (Unimplemented / Read as Zero in 4-bit MCU) |
| (PRIGROUP = 3: 16 Preemption Levels, 0 Subpriority Levels - RECOMMENDED) |
+-------------------------------+-------------------------------------------------------+
|<-- Preempt -->|<-- Subprio -->| |
| (PRIGROUP = 5: 4 Preemption Levels, 4 Subpriority Levels) |
+---------------+---------------+-------------------------------------------------------+

The Blocking Hazard

Consider two interrupts:

  • IRQ_MotorCurrent: Time-critical ADC sampling (budget: < 2 us).
  • IRQ_UART_Rx: Low-priority byte reception (takes 15 us to read FIFO).

If both IRQs are assigned Preemption Priority 2 with differing Subpriorities:

  • When IRQ_UART_Rx is executing, IRQ_MotorCurrent asserts.
  • Because their preemption priorities are identical, IRQ_MotorCurrent cannot preempt IRQ_UART_Rx.
  • IRQ_MotorCurrent experiences head-of-line blocking jitter equal to the entire 15 us runtime of the UART handler, causing an overrun.

For deterministic real-time systems and RTOS kernels (such as FreeRTOS, Zephyr, and ThreadX), assign all implemented priority bits to preemption priority (PRIGROUP = 3 on 4-bit priority architectures like STM32):

/* Configure all 4 priority bits for preemption (0 subpriority bits) */
NVIC_SetPriorityGrouping(3); /* CMSIS: NVIC_PriorityGroup_4 */

FPU Lazy Stacking (FPCCR.LSPEN) Optimization

On Cortex-M4F, Cortex-M7, and Cortex-M33 devices equipped with a single-precision or double-precision Floating Point Unit, interrupt stacking behavior changes dramatically.

The Standard vs. Extended Stack Frame

  1. Integer Stack Frame: Pushes 8 registers (R0-R3, R12, LR, PC, xPSR) = 32 bytes (12 cycles).
  2. Extended Floating-Point Frame: Pushes 8 integer registers + 16 floating-point registers (S0-S15) + FPSCR + 1 alignment word = 26 registers (104 bytes).

Writing 104 bytes over a 32-bit bus requires at least 26 bus cycles. If an ISR must push the full FPU context, entry latency spikes from 12 cycles to over 27 cycles, introducing massive timing jitter into non-floating-point interrupts.

+---------------------------------------------------------------------------------------+
| FPU LAZY STACKING HARDWARE MECHANICS |
+---------------------------------------------------------------------------------------+
Interrupt Asserts (Core in Thread Mode with FPU active)
|
v
[ Check FPCCR.LSPEN Bit ]
|
+--------+--------+
| |
[LSPEN = 0] [LSPEN = 1] (Default CMSIS)
| |
| +--> 1. Decrements SP by 104 bytes (reserving space)
| 2. Writes ONLY integer registers R0-R3, R12, LR, PC, xPSR
| 3. Sets FPCCR.LSPACT = 1 (Hardware Flag)
| 4. Enters ISR in EXACTLY 12 CYCLES!
| |
v v
Eager Stacking: Does ISR execute any FPU instruction?
Pushes all 26 registers |
S0-S15 + FPSCR immediately +-------+-------+
Takes 27+ clock cycles! | |
(Severe Latency Jitter) [ NO ] [ YES ]
| |
| +--> Hardware pauses pipeline,
| dumps S0-S15 to reserved slot,
| clears LSPACT, executes float.
v
Returns via EXC_RETURN:
No FP registers ever written!
Saved 15+ cycles on entry and exit!

Configuring Lazy Stacking

In production firmware, verify that both Automatic Stacking (ASPEN) and Lazy Stacking (LSPEN) are enabled in the Floating Point Context Control Register (FPCCR):

void System_EnableFpuLazyStacking(void) {
/* Enable lazy stacking: reserve space without writing S0-S15 */
FPU->FPCCR |= (FPU_FPCCR_ASPEN_Msk | FPU_FPCCR_LSPEN_Msk);
__DSB();
__ISB();
}

Memory Subsystem & Vector Table Relocation (SCB->VTOR)

Flash memory access time is among the largest external sources of interrupt jitter. High-performance microcontrollers operate their CPU cores at frequencies far higher than internal embedded Flash can support:

+---------------------------------------------------------------------------------------+
| FLASH WAIT STATES AT MAXIMUM FREQUENCIES |
+--------------------------+--------------------+-------------------+-------------------+
| Microcontroller Family | Maximum Core Clock | Flash Access Time | Required Wait |
| | | (Zero Wait State) | States (LATENCY) |
+--------------------------+--------------------+-------------------+-------------------+
| STM32F407 / F429 | 168 / 180 MHz | <= 30 MHz | 5 / 6 wait states |
| STM32F746 / F767 | 216 MHz | <= 30 MHz | 7 wait states |
| STM32H743 / H753 | 480 MHz | <= 70 MHz | 4 AXI wait states |
| NXP LPC55S69 | 150 MHz | <= 25 MHz | 5 wait states |
+--------------------------+--------------------+-------------------+-------------------+

When an interrupt fires while the vector table resides in Flash:

  1. The vector fetch issues a 32-bit read request across the I-Code bus.
  2. The internal flash memory controller inserts 5 to 7 wait states before returning the 32-bit ISR address.
  3. This adds 5 to 7 clock cycles to every single interrupt entry.

Relocating the Vector Table to Zero-Wait-State SRAM

To eliminate Flash wait states from the vector fetch path, copy the vector table to internal SRAM (or Tightly-Coupled Memory, ITCM/DTCM) and update SCB->VTOR.

[!CAUTION] VTOR Alignment Rule: The ARM architecture specifies that SCB->VTOR must be aligned to a power-of-2 boundary equal to or greater than the vector table size, with an absolute architectural minimum alignment of 128 bytes (32 words). For a device with 16 system exceptions and 84 external interrupts (100 vectors total = 400 bytes), the next highest power of 2 is 512 bytes (0x200).

#define VECTOR_TABLE_SIZE (16 + 96) /* 16 system + 96 peripheral IRQs */
#define VTOR_ALIGNMENT_BYTES (512)
/* Allocate vector table buffer in fast SRAM aligned to 512-byte boundary */
__attribute__((aligned(VTOR_ALIGNMENT_BYTES)))
static uint32_t ram_vector_table[VECTOR_TABLE_SIZE];
void System_RelocateVectorTableToRAM(void) {
uint32_t *flash_vectors = (uint32_t *)SCB->VTOR;
/* Copy exception and peripheral vectors from Flash to SRAM */
for (size_t i = 0; i < VECTOR_TABLE_SIZE; i++) {
ram_vector_table[i] = flash_vectors[i];
}
/* Data Synchronization Barrier to ensure all writes commit to RAM */
__DSB();
/* Point VTOR to the RAM buffer */
SCB->VTOR = ((uint32_t)ram_vector_table & SCB_VTOR_TBLOFF_Msk);
/* Instruction Synchronization Barrier to flush pipeline */
__ISB();
}

Compiler Prologues & AAPCS Register Pressure

The ARM Architecture Procedure Call Standard (AAPCS) defines register roles for function calls:

  • Caller-Saved (Scratch) Registers: R0, R1, R2, R3, R12, LR.
  • Callee-Saved (Preserved) Registers: R4, R5, R6, R7, R8, R9, R10, R11.
  • Special-Purpose Registers (not part of AAPCS allocation): PC (R15), xPSR.

The Cortex-M hardware exception entry sequence automatically pushes eight values onto the stack: R0-R3, R12, LR, the return PC, and xPSR. Because this set covers all AAPCS caller-saved general-purpose registers, an ISR written in C that uses only R0-R3 and R12 requires zero software stacking overhead. The compiler emits no PUSH in the function prologue.

The Hidden Penalty of Subroutine Calls inside ISRs

If an ISR calls another C function:

/* ANTI-PATTERN: Calling helper functions inside an ISR */
void TIM2_IRQHandler(void) {
TIM2->SR = ~TIM_SR_UIF;
Process_Telemetry_Buffer(); /* FUNCTION CALL! */
}

The compiler must conform to AAPCS:

  1. The BL Process_Telemetry_Buffer instruction overwrites LR with the return address within the ISR. Since LR holds the special EXC_RETURN value needed for exception return, the compiler must save it. Additionally, if the ISR body or the callee uses any callee-saved registers (R4-R11), those must be preserved too. The compiler generates a prologue:
    PUSH {R4-R7, LR} ; Consumes 5 to 6 additional memory write cycles!
  2. Before returning, it must execute an epilogue that restores the saved registers and branches via EXC_RETURN:
    POP {R4-R7, PC} ; Consumes 5 to 6 additional memory read cycles!

Optimization Strategy

  1. Mark helper functions invoked from ISRs with __attribute__((always_inline)) static inline.
  2. Limit local variable scope in critical ISRs so the compiler can allocate all variables strictly within R0-R3 and R12.
  3. Avoid 64-bit integer arithmetic or complex structure passing inside the ISR, which forces register spills to R4-R11.

Memory Barriers & Peripheral Store Buffer Hazards

Cortex-M3, Cortex-M4, and Cortex-M7 processors feature an internal asynchronous store buffer situated between the core pipeline and the AHB/AXI bus matrix. This buffer decouples CPU execution from slow peripheral bus write cycles.

The Spurious Interrupt Tail-Chain Bug

Consider an ISR clearing a peripheral timer flag:

void TIM3_IRQHandler(void) {
TIM3->SR = ~TIM_SR_UIF; /* Write to clear interrupt flag in peripheral */
/* Function exit: compiler emits BX LR (EXC_RETURN) */
}

Here is what occurs at the silicon level:

  1. The CPU core issues the store instruction for TIM3->SR.
  2. The store buffer accepts the write address and data, immediately signaling completion to the pipeline.
  3. The core executes BX LR and begins unstacking registers (or initiates tail-chaining).
  4. The write transaction has not yet arrived at the peripheral over the APB bridge due to bus clock dividers and bridge latency.
  5. The peripheral interrupt line leading into the NVIC remains asserted high.
  6. The NVIC inspects the interrupt line, assumes a new interrupt has arrived, and immediately tail-chains back into TIM3_IRQHandler!

This ghost execution doubles interrupt overhead and corrupts timing budgets.

The Fix: Data Synchronization Barrier (__DSB) or Register Read-Back

To guarantee that the peripheral interrupt flag is cleared before the exception exits, use one of two verified patterns:

/* PATTERN 1: Data Synchronization Barrier */
void TIM3_IRQHandler_Pattern1(void) {
TIM3->SR = ~TIM_SR_UIF;
/* Ensure the peripheral store buffer drains before exception return */
__DSB();
}
/* PATTERN 2: Peripheral Register Read-Back (Recommended across all architectures) */
void TIM3_IRQHandler_Pattern2(void) {
TIM3->SR = ~TIM_SR_UIF;
/* Read-back forces the APB bridge to stall until the write completes */
(void)TIM3->SR;
}

Similarly, when modifying core mask registers (PRIMASK or BASEPRI) to exit critical sections, follow the write with an Instruction Synchronization Barrier (__ISB()) to ensure pipeline flush:

__set_BASEPRI(new_mask);
__ISB(); /* Flushes pipeline so subsequent instructions obey the new mask */

Complete Implementation: Deterministic 100 kHz Timer Interrupt

The following production example demonstrates a fully optimized, cycle-accurate timer ISR on an ARM Cortex-M4 microcontroller running at 168 MHz:

  • Configures priority grouping to 16 preemption levels (PRIGROUP = 3).
  • Relocates the vector table to zero-wait-state SRAM.
  • Verifies FPU lazy stacking.
  • Implements a zero-prologue ISR with peripheral store buffer synchronization.
  • Measures cycle latency using the Data Watchpoint and Trace (DWT) cycle counter.
#include <stdint.h>
#include <stdbool.h>
#include "stm32f4xx.h"
#define VECT_TAB_SIZE_WORDS (16 + 96)
#define VECT_TAB_ALIGN (512)
/* Statically allocate RAM vector table buffer */
__attribute__((aligned(VECT_TAB_ALIGN)))
static uint32_t ram_vtor[VECT_TAB_SIZE_WORDS];
/* Global metrics for validation */
volatile uint32_t g_max_isr_latency_cycles = 0;
void TIM1_UP_TIM10_IRQHandler(void);
void System_OptimizeInterruptEnvironment(void) {
/* Step 1: Configure all 4 priority bits as preemption bits */
NVIC_SetPriorityGrouping(3); /* CMSIS: 16 preemption groups, 0 subpriority */
/* Step 2: Ensure FPU Lazy Stacking is enabled */
FPU->FPCCR |= (FPU_FPCCR_ASPEN_Msk | FPU_FPCCR_LSPEN_Msk);
/* Step 3: Relocate Vector Table to Zero-Wait-State internal SRAM */
uint32_t *flash_vtor = (uint32_t *)SCB->VTOR;
for (size_t i = 0; i < VECT_TAB_SIZE_WORDS; i++) {
ram_vtor[i] = flash_vtor[i];
}
/* Install optimized handler into RAM vector table */
ram_vtor[16 + TIM1_UP_TIM10_IRQn] = (uint32_t)TIM1_UP_TIM10_IRQHandler;
__DSB();
SCB->VTOR = ((uint32_t)ram_vtor & SCB_VTOR_TBLOFF_Msk);
__ISB();
/* Step 4: Configure DWT Cycle Counter for precise latency profiling */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
DWT->CYCCNT = 0;
/* Step 5: Configure TIM10 for 100 kHz (10 us) periodic interrupts */
RCC->APB2ENR |= RCC_APB2ENR_TIM10EN;
__DSB();
TIM10->PSC = 0; /* Prescaler = 1 (168 MHz tick) */
TIM10->ARR = 1680 - 1; /* 168 MHz / 1680 = 100 kHz period */
TIM10->DIER |= TIM_DIER_UIE; /* Enable update interrupt */
/* Set TIM10 interrupt to highest preemption priority (0) */
NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 0);
NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);
/* Start Timer */
TIM10->CR1 |= TIM_CR1_CEN;
}
/*
* Optimized Interrupt Service Routine
* - No function calls (inlined execution)
* - Restricts variable usage to R0-R3 (zero compiler prologue)
* - Enforces store buffer drainage via peripheral read-back
*/
void TIM1_UP_TIM10_IRQHandler(void) {
uint32_t start_cycles = DWT->CYCCNT;
/* Clear hardware interrupt flag */
TIM10->SR = (uint16_t)~TIM_SR_UIF;
/* Read-back forces APB bus synchronization before exit */
(void)TIM10->SR;
/* User Payload: e.g., Read ADC or update control loop state */
/* ... minimal deterministic logic ... */
/* Profiling calculation */
uint32_t elapsed = DWT->CYCCNT - start_cycles;
if (elapsed > g_max_isr_latency_cycles) {
g_max_isr_latency_cycles = elapsed;
}
}

Latency Optimization Checklist

When tuning a Cortex-M system for sub-microsecond determinism, audit the system against this engineering checklist:

  1. Vector Table Location:
    • Is SCB->VTOR pointing to internal zero-wait-state SRAM or TCM?
    • Is the vector table buffer aligned to the nearest power-of-2 address?
  2. Priority Grouping:
    • Is NVIC_SetPriorityGrouping() configured with all bits assigned to preemption?
    • Are time-critical ISRs assigned priority 0?
  3. FPU Configuration:
    • Is FPCCR.LSPEN set to 1 to avoid pushing 16 float registers on entry?
    • Do integer-only time-critical ISRs avoid calling floating-point math routines?
  4. Compiler Stack Frame:
    • Are ISR helper functions marked static inline __attribute__((always_inline))?
    • Did you inspect the compiler disassembly (objdump / disassembly view) to confirm absence of PUSH {R4-R11, LR} in the prologue?
  5. Bus Synchronization:
    • Does the ISR clear the peripheral interrupt request flag and issue a read-back or __DSB() prior to BX LR?
    • Are critical sections closed with __set_BASEPRI() followed immediately by __ISB()?
  6. Instruction Execution:
    • Are long critical sections (cpsid i) banned in thread mode, replaced by scoped BASEPRI masking?

References

  1. ARM Limited, ARMv7-M Architecture Reference Manual, ARM DDI 0403E.e, 2021.
  2. ARM Limited, Cortex-M4 Devices Generic User Guide, ARM DUI 0553B, 2013.
  3. ARM Limited, Cortex-M7 Technical Reference Manual, ARM DDI 0489F, 2020.
  4. Joseph Yiu, The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors, 3rd Edition, Newnes (Elsevier), Oxford, UK, 2013. ISBN: 978-0124080829.
  5. STMicroelectronics, Application Note AN4031: Using the STM32F2, STM32F4 and STM32F7 Cortex-M4/M7 Processor Interrupt Controller, DocID022899 Rev 4, 2017.
  6. FreeRTOS Documentation, Running FreeRTOS on a Cortex-M Core: Priority Grouping and BASEPRI Register Configuration, Real Time Engineers Ltd., 2024.

Frequently Asked Questions

What is interrupt latency and why does it matter in real-time systems?

Interrupt latency is the duration between the hardware assertion of an interrupt signal and the execution of the first instruction of the corresponding Interrupt Service Routine (ISR). In hard real-time systems, non-deterministic latency risks missed execution deadlines and control loop instability.

How does the NVIC contribute to interrupt latency in Cortex-M processors?

The NVIC incurs latency through signal synchronization (1-3 cycles), preemption arbitration, context stacking of core registers, and vector table fetches. On architectures like Cortex-M3 and Cortex-M4, concurrent dual-bus transactions achieve a baseline entry latency of exactly 12 zero-wait-state clock cycles.

What are key techniques to minimize interrupt latency in Cortex-M based embedded systems?

Techniques include relocating the vector table to zero-wait-state SRAM or TCM, enabling FPU lazy stacking (FPCCR.LSPEN), configuring all NVIC priority bits as preemption bits, optimizing compiler register allocation to avoid prologue spilling, utilizing tail-chaining (6 cycles), and eliminating peripheral bus buffer delays with DSB instructions.

Tags

armcortex-mnvicinterrupt-latencyoptimization

Share


Previous Article
Fixing Zephyr BMI160 I2C Timeout on STM32
Jithin Tom

Jithin Tom

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

Related Posts

Embedded Linux: Fixing Slow Boot Time
Embedded Linux: Fixing Slow Boot Time
August 30, 2026
8 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media