HomeAbout UsContact Us

ARM Cortex-M NVIC Interrupt Latency Optimization

By Jithin Tom
Published in Embedded Concepts
July 13, 2026
3 min read
ARM Cortex-M NVIC Interrupt Latency Optimization

Table Of Contents

01
NVIC Architecture and Priority Model
02
Hardware Interrupt Optimizations
03
Interrupt Masking Registers: PRIMASK, FAULTMASK, BASEPRI
04
RTOS Critical Sections with BASEPRI
05
Priority Boosting in RTOS Kernels
06
Practical NVIC Configuration for Embedded Firmware
07
Measuring Interrupt Latency
08
Common Pitfalls
09
ASCII Art: NVIC Priority Masking Visualization
10
ASCII Art: Tail Chaining & Late Arrival Timeline
11
Summary
12
Related Reading
13
References
14
Frequently Asked Questions

ARM Cortex-M processors achieve deterministic, low-latency interrupt handling through the Nested Vectored Interrupt Controller (NVIC) and a set of hardware optimizations: tail chaining, late arrival, and pop pre-emption. Understanding these mechanisms — and how to configure NVIC priority grouping, BASEPRI, PRIMASK, and FAULTMASK — is essential for writing deterministic RTOS kernels and time-critical firmware.

NVIC Architecture and Priority Model

The NVIC implements a flexible priority scheme supporting up to 256 priority levels (8 bits), though silicon vendors typically implement 3–8 bits. Lower numerical values indicate higher priority. Each interrupt has an 8-bit priority field in the Interrupt Priority Registers (IPR).

Priority grouping splits the 8-bit priority field into group priority (preemption level) and subpriority (tie-breaker within the same group). The PRIGROUP field in the Application Interrupt and Reset Control Register (AIRCR) controls this split:

// Cortex-M3/M4/M7: SCB->AIRCR.PRIGROUP
// 0b000: 0 bits subpriority, 8 bits group priority (no subpriority)
// 0b011: 3 bits subpriority, 5 bits group priority (32 preemption levels)
// 0b100: 4 bits subpriority, 4 bits group priority (16 preemption levels)
// 0b111: 7 bits subpriority, 1 bit group priority (2 preemption levels)

For RTOS kernels, a common configuration is 4 bits group priority / 4 bits subpriority (PRIGROUP = 0b100), providing 16 preemption levels. The kernel typically reserves the lowest priority levels for tasks, while device interrupts use higher priorities.

// Configure 4 bits group priority, 4 bits subpriority (PRIGROUP = 4)
SCB->AIRCR = (SCB->AIRCR & ~(0x7 << 8)) | (4 << 8);

Hardware Interrupt Optimizations

Tail Chaining

When an ISR completes and another interrupt is already pending, the NVIC skips the full unstack/stack sequence. Instead of popping 8 registers (R0–R3, R12, LR, PC, xPSR) and pushing them again for the next ISR, it only restores PC and jumps to the next vector.

// Tail chaining sequence (6 cycles on Cortex-M3/M4 zero wait-state)
// vs 12 cycles exit + 12 cycles entry = 24 cycles without optimization
// ISR exit with pending interrupt:
BX LR // LR contains EXC_RETURN value (0xFFFFFFF1/9 for thread/handler mode)
// NVIC detects pending interrupt, skips pop/push, jumps to next vector

This optimization is automatic — no software configuration required. It activates whenever a lower/equal priority interrupt is pending at ISR exit.

Late Arrival

If a higher priority interrupt becomes pending during the stacking phase (first 12 cycles of entry), the NVIC abandons the current entry sequence, preserves the partially stacked state, and begins fetching the higher priority ISR. The original interrupt is then tail-chained after the higher priority ISR completes.

Timeline without late arrival:
[Stacking 12 cycles] → [ISR Low] → [Unstacking 12 cycles] → [Stacking 12 cycles] → [ISR High]
Timeline with late arrival (high priority arrives at cycle 6):
[Stacking 6 cycles] → [Abandon] → [Stacking 12 cycles for High] → [ISR High] → [Tail chain 6 cycles] → [ISR Low]

Pop Pre-emption

If an interrupt arrives during the unstacking phase (ISR exit), the NVIC halts unstacking and immediately begins the new ISR entry. This is the exit-phase counterpart to late arrival.

Interrupt Masking Registers: PRIMASK, FAULTMASK, BASEPRI

Cortex-M provides three special-purpose registers for interrupt masking, accessible via MSR/MRS instructions or CMSIS intrinsics.

PRIMASK (Priority Mask Register)

Single-bit register. When set to 1, masks all configurable-priority exceptions (all IRQs). NMI, HardFault, and Reset remain active.

// CMSIS intrinsics
__disable_irq(); // PRIMASK = 1
__enable_irq(); // PRIMASK = 0
// Assembly equivalent
// CPSID i // Disable interrupts (PRIMASK = 1)
// CPSIE i // Enable interrupts (PRIMASK = 0)

Use case: Short critical sections where absolutely no preemption is acceptable (e.g., manipulating linked lists shared with ISRs). Blocks all interrupts — use sparingly in RTOS contexts.

FAULTMASK (Fault Mask Register)

Similar to PRIMASK but also masks HardFault. Only NMI remains active. Not available on ARMv6-M (Cortex-M0/M0+).

__disable_fault_irq(); // FAULTMASK = 1
__enable_fault_irq(); // FAULTMASK = 0

Use case: Exception handling code that must not be interrupted by any fault except NMI.

BASEPRI (Base Priority Mask Register)

8-bit register (implements only the implemented priority bits). Masks all interrupts with priority numerically greater than or equal to BASEPRI value (i.e., lower or equal priority). Interrupts with higher priority (lower numerical value) remain unmasked.

// CMSIS: __set_BASEPRI(priority << (8 - __NVIC_PRIO_BITS))
// Example: mask priorities 0x80 and lower (higher numerical = lower priority)
__set_BASEPRI(0x80); // Allow priorities < 0x80 (higher urgency)
__set_BASEPRI(0); // Clear mask (allow all)

Critical insight: BASEPRI enables selective interrupt masking — the foundation of RTOS critical sections that preserve high-priority interrupt responsiveness.

RTOS Critical Sections with BASEPRI

FreeRTOS and Zephyr use BASEPRI for task-level critical sections (taskENTER_CRITICAL/taskEXIT_CRITICAL). The kernel defines configMAX_SYSCALL_INTERRUPT_PRIORITY (FreeRTOS) or CONFIG_MAX_SYSCALL_INTERRUPT_PRIORITY (Zephyr) — the highest priority level that ISRs can use FreeRTOS/Zephyr API functions.

// FreeRTOS Cortex-M port (simplified)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (0x80) // Example: priority 128 (8-bit)
// portENTER_CRITICAL()
void vPortEnterCritical(void) {
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm volatile (
"mrs %0, basepri\n" // Save current BASEPRI
"msr basepri, %1\n" // Set new mask
"isb\n" // Instruction synchronization barrier
"dsb\n" // Data synchronization barrier
: "=r" (ulOriginalBASEPRI)
: "i" (ulNewBASEPRI)
: "memory"
);
}
// portEXIT_CRITICAL()
void vPortExitCritical(uint32_t ulOriginalBASEPRI) {
__asm volatile (
"msr basepri, %0\n"
"isb\n"
"dsb\n"
: : "r" (ulOriginalBASEPRI) : "memory"
);
}

Key design: Interrupts above configMAX_SYSCALL_INTERRUPT_PRIORITY (e.g., priority 0x00–0x7F) are never masked by kernel critical sections. These “system-critical” interrupts (e.g., motor control PWM fault, safety watchdog) retain true real-time response.

Priority levels (lower number = higher priority):
0x00 ──────────────────┐ Never masked by BASEPRI (system-critical)
0x10 ──────────────────┤ e.g., Hard fault handlers, safety monitors
0x20 ──────────────────┤
0x30 ──────────────────┤
0x40 ──────────────────┤ configMAX_SYSCALL_INTERRUPT_PRIORITY = 0x80
0x50 ──────────────────┤ ISRs at this level CAN call FreeRTOS API
0x60 ──────────────────┤
0x70 ──────────────────┘
0x80 ──────────────────┐ MASKED by BASEPRI in critical sections
0x90 ──────────────────┤ Typical peripheral ISRs (UART, SPI, timers)
0xA0 ──────────────────┤
0xB0 ──────────────────┤
0xC0 ──────────────────┤
0xD0 ──────────────────┤
0xE0 ──────────────────┤
0xF0 ──────────────────┘ Lowest priority (task-level)

Priority Boosting in RTOS Kernels

During context switching, the kernel must manipulate task stacks and scheduler data structures atomically. FreeRTOS uses priority boosting via BASEPRI in vTaskSwitchContext() (called from PendSV):

// Simplified FreeRTOS context switch (PendSV handler)
void PendSV_Handler(void) {
// Boost priority to mask all API-callable interrupts
__set_BASEPRI(configMAX_SYSCALL_INTERRUPT_PRIORITY);
// Save current task context (R4-R11, PSP)
// Select next task (scheduler)
// Restore next task context
// Restore BASEPRI
__set_BASEPRI(0);
}

This ensures the context switch cannot be preempted by an ISR that might also call the scheduler (e.g., xTaskNotifyFromISR()), which would corrupt kernel state.

Practical NVIC Configuration for Embedded Firmware

Interrupt Priority Assignment Strategy

// Priority assignment for a typical Cortex-M4 application
// Using 4-bit group priority (16 levels), 4-bit subpriority
#define PRIO_HIGHEST 0x00 // NMI, HardFault (fixed)
#define PRIO_SAFETY_WATCHDOG 0x10 // Never masked (above MAX_SYSCALL)
#define PRIO_MOTOR_FAULT 0x20 // Never masked
#define PRIO_COMM_CRITICAL 0x30 // CAN bus-off, Ethernet PTP
#define PRIO_MAX_SYSCALL 0x80 // configMAX_SYSCALL_INTERRUPT_PRIORITY
#define PRIO_RTOS_TICK 0x90 // SysTick (calls xTaskIncrementTick)
#define PRIO_UART_DMA 0xA0 // DMA complete callbacks
#define PRIO_SPI_DMA 0xA0
#define PRIO_ADC_COMPLETE 0xB0
#define PRIO_GPIO_BUTTON 0xC0 // Low priority, debounced in task
#define PRIO_LOWEST 0xF0 // Background tasks
// Configure priorities
NVIC_SetPriority(SysTick_IRQn, PRIO_RTOS_TICK >> 4); // Group priority
NVIC_SetPriority(USART1_IRQn, PRIO_UART_DMA >> 4);
NVIC_SetPriority(DMA1_Stream1_IRQn, PRIO_UART_DMA >> 4);
NVIC_SetPriority(EXTI0_IRQn, PRIO_GPIO_BUTTON >> 4);

Subpriority Usage

Subpriority resolves tie-breaking when multiple interrupts share the same group priority. Use subpriority for:

  • DMA streams sharing a peripheral priority (e.g., SPI TX/RX DMA)
  • Multiple EXTI lines grouped by function
  • Timer channels on the same timer
// Subpriority assignment (lower = higher within group)
NVIC_SetPriority(DMA1_Stream0_IRQn, (PRIO_SPI_DMA >> 4) | 0x0); // Subpri 0
NVIC_SetPriority(DMA1_Stream1_IRQn, (PRIO_SPI_DMA >> 4) | 0x1); // Subpri 1
NVIC_SetPriority(DMA1_Stream2_IRQn, (PRIO_SPI_DMA >> 4) | 0x2); // Subpri 2

Measuring Interrupt Latency

Use a GPIO toggle + logic analyzer or cycle counter (DWT_CYCCNT) to measure actual latency:

// Enable DWT cycle counter
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
// In ISR entry
uint32_t cycles = DWT->CYCCNT - g_interrupt_assert_cycle;
// GPIO toggle method (external trigger)
void EXTI0_IRQHandler(void) {
GPIOA->ODR ^= (1 << 5); // Toggle PA5 for logic analyzer
EXTI->PR = EXTI_PR_PR0; // Clear pending
}

Typical Cortex-M4 latencies (zero wait-state flash):

  • Interrupt entry: 12 cycles (stacking + vector fetch)
  • Tail chaining: 6 cycles (exit + entry overlap)
  • Late arrival: Adds ~6 cycles to entry of preempted ISR

Common Pitfalls

PitfallSymptomFix
PRIMASK in ISRDeadlock, missed deadlinesUse BASEPRI in ISRs; never PRIMASK/FAULTMASK
Wrong PRIGROUPPriority inversion, unexpected preemptionConfigure once at startup; verify with __NVIC_GetPriorityGrouping()
BASEPRI not restoredInterrupts permanently maskedUse __get_BASEPRI()/__set_BASEPRI() pairs; check all exit paths
Subpriority ignoredNon-deterministic ISR orderSet explicit subpriorities for same-group interrupts
ISR calls blocking APIHardFault, kernel crashOnly use FromISR APIs; check configMAX_SYSCALL_INTERRUPT_PRIORITY

ASCII Art: NVIC Priority Masking Visualization

+==============================================================+
| CORTEX-M PRIORITY HIERARCHY |
| (Lower numerical value = Higher urgency) |
+==============================================================+
| |
| PRIORITY LEVEL | MASKING BEHAVIOR |
| ----------------------------|------------------------------|
| |
| 0x00 - 0x10 ############### NMI, HardFault, Safety |
| # NEVER MASKED ### Watchdog, Motor Fault |
| ############### (Hardware fixed) |
| |
| 0x20 - 0x70 ############### System-Critical Interrupts |
| # UNMASKED by # CAN bus-off, PTP sync, |
| # BASEPRI in # High-speed control loops |
| # critical sect. # (Priority < MAX_SYSCALL) |
| ############### |
| |
| 0x80 ------------------------ configMAX_SYSCALL_INTERRUPT_|
| ^ PRIORITY threshold |
| | (BASEPRI set to this value) |
| | |
| 0x90 - 0xF0 ============= Maskable by BASEPRI |
| = RTOS Tick = SysTick, UART, SPI, ADC, |
| = Peripheral ISRs = GPIO, Timers, DMA |
| = (can call RTOS = (Priority >= MAX_SYSCALL)|
| = FromISR APIs) = |
| ============= |
| |
| 0xFF ~~~~~~~~~~~~~~~~~~~~~~ Thread Mode (Tasks) |
| ~ LOWEST PRIORITY ~~~ Preempted by ALL interrupts |
| ~~~~~~~~~~~~~~~~~~~~~~ |
+==============================================================+

ASCII Art: Tail Chaining & Late Arrival Timeline

WITHOUT OPTIMIZATIONS (Naive ISR Entry/Exit):
====================================================================
ISR Low Priority ISR High Priority
+---------------------+ +------------------+
| Stacking (12 cyc) | | Stacking (12 cyc)|
+---------------------+ +------------------+
| ISR Body | | ISR Body |
+---------------------+ +------------------+
| Unstacking (12 cyc) | | Unstacking (12 cyc)|
+---------------------+ +------------------+
Total: ~36 cycles per ISR transition
WITH TAIL CHAINING (Pending interrupt at ISR exit):
====================================================================
ISR Low Priority ISR High Priority
+---------------------+
| Stacking (12 cyc) |
+---------------------+
| ISR Body |
+---------------------+
| Exit + Tail Chain |---> 6 cycles (vs 24)
| (6 cyc) |
+---------------------+
ISR High Priority
+------------------+
| (Stacking skipped)|
+------------------+
| ISR Body |
+------------------+
| Unstacking (12 cyc)|
+------------------+
WITH LATE ARRIVAL (High priority arrives during Low stacking):
====================================================================
Time ------------------------------------------------------------>
[Stack Low 6cyc][Abandon][Stack High 12cyc][ISR High][Tail 6cyc][ISR Low][Unstack 12cyc]
^ ^ ^ ^
| | | |
Cycle 0 Cycle 6 Cycle 18 Cycle 30+
Low IRQ High IRQ High ISR Tail chain
asserted arrives entry to Low

Summary

The Cortex-M NVIC’s hardware optimizations — tail chaining, late arrival, and pop pre-emption — dramatically reduce interrupt overhead without software intervention. The key to leveraging these in production firmware is proper priority configuration:

  1. Set PRIGROUP once at startup (typically 4/4 split for 16 preemption levels)
  2. Reserve priority space above configMAX_SYSCALL_INTERRUPT_PRIORITY for safety-critical interrupts that must never be masked
  3. Use BASEPRI exclusively for RTOS critical sections — never PRIMASK in ISRs
  4. Assign explicit subpriorities for same-group interrupts to ensure deterministic ordering
  5. Measure actual latency with DWT cycle counter or GPIO toggle + logic analyzer

These practices ensure your interrupt subsystem remains deterministic, responsive, and debuggable — even under heavy interrupt load.

References

  1. ARM, Cortex-M3/M4/M7 Devices Generic User Guide, Section “Nested Vectored Interrupt Controller (NVIC)”, ARM DUI 0553A
  2. ARM, ARMv7-M Architecture Reference Manual, Section “Exception Model”, ARM DDI 0403E
  3. FreeRTOS Kernel Source, portable/GCC/ARM_CM3/portmacro.h and portable/GCC/ARM_CM4F/portmacro.h
  4. Joseph Yiu, The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors, 3rd Ed., Chapter 7 (NVIC) & Chapter 11 (Interrupt Latency)
  5. ARM Community Blog, “Beginner Guide on Interrupt Latency and Interrupt Latency of the ARM Cortex-M Processors”, 2018
  6. Interrupt.memfault.com, “A Practical Guide to ARM Cortex-M Exception Handling”, 2020

Frequently Asked Questions

What is tail chaining in ARM Cortex-M NVIC?

Tail chaining is an NVIC optimization where the processor skips the unstacking and stacking operations when exiting one ISR and immediately entering another pending ISR. This reduces interrupt transition overhead from 12 cycles to just 6 cycles on Cortex-M3/M4.

How does late arrival optimization reduce interrupt latency?

When a higher priority interrupt arrives during the stacking phase of a lower priority interrupt, the NVIC skips the remaining stacking operations and immediately begins fetching the higher priority ISR. The lower priority interrupt is then tail-chained after the higher priority one completes.

What is the difference between PRIMASK, FAULTMASK, and BASEPRI?

PRIMASK disables all configurable priority interrupts (only NMI/HardFault/Reset remain). FAULTMASK disables all except NMI. BASEPRI masks interrupts at or below a configured priority level, allowing higher priority interrupts to still preempt — critical for RTOS critical sections.

How does BASEPRI enable RTOS critical sections without blocking all interrupts?

FreeRTOS sets BASEPRI to configMAX_SYSCALL_INTERRUPT_PRIORITY, masking only interrupts that can call FreeRTOS API functions. Higher priority interrupts (above configMAX_SYSCALL_INTERRUPT_PRIORITY) remain unmasked and can preempt, ensuring true real-time responsiveness for critical hardware interrupts.

What is priority boosting and when should it be used in RTOS kernels?

Priority boosting temporarily raises the execution priority (via BASEPRI, PRIMASK, or FAULTMASK) to prevent preemption during critical kernel operations like context switching. It ensures atomic updates to kernel data structures without disabling all interrupts, preserving responsiveness for highest-priority interrupts.

Tags

arm-cortex-mnvicinterrupt-latencytail-chaininglate-arrivalbasepriprimaskrtos

Share


Previous Article
Stack Usage Analysis and Optimization in Embedded C
Jithin Tom

Jithin Tom

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

Related Posts

JTAG and SWD Debugging Strategies for Embedded Systems
JTAG and SWD Debugging Strategies for Embedded Systems
June 11, 2026
4 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media