
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.
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);
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.
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]
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.
Cortex-M provides three special-purpose registers for interrupt masking, accessible via MSR/MRS instructions or CMSIS intrinsics.
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.
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.
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.
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 monitors0x20 ──────────────────┤0x30 ──────────────────┤0x40 ──────────────────┤ configMAX_SYSCALL_INTERRUPT_PRIORITY = 0x800x50 ──────────────────┤ ISRs at this level CAN call FreeRTOS API0x60 ──────────────────┤0x70 ──────────────────┘0x80 ──────────────────┐ MASKED by BASEPRI in critical sections0x90 ──────────────────┤ Typical peripheral ISRs (UART, SPI, timers)0xA0 ──────────────────┤0xB0 ──────────────────┤0xC0 ──────────────────┤0xD0 ──────────────────┤0xE0 ──────────────────┤0xF0 ──────────────────┘ Lowest priority (task-level)
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.
// 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 prioritiesNVIC_SetPriority(SysTick_IRQn, PRIO_RTOS_TICK >> 4); // Group priorityNVIC_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 resolves tie-breaking when multiple interrupts share the same group priority. Use subpriority for:
// Subpriority assignment (lower = higher within group)NVIC_SetPriority(DMA1_Stream0_IRQn, (PRIO_SPI_DMA >> 4) | 0x0); // Subpri 0NVIC_SetPriority(DMA1_Stream1_IRQn, (PRIO_SPI_DMA >> 4) | 0x1); // Subpri 1NVIC_SetPriority(DMA1_Stream2_IRQn, (PRIO_SPI_DMA >> 4) | 0x2); // Subpri 2
Use a GPIO toggle + logic analyzer or cycle counter (DWT_CYCCNT) to measure actual latency:
// Enable DWT cycle counterCoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;DWT->CYCCNT = 0;DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;// In ISR entryuint32_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 analyzerEXTI->PR = EXTI_PR_PR0; // Clear pending}
Typical Cortex-M4 latencies (zero wait-state flash):
| Pitfall | Symptom | Fix |
|---|---|---|
| PRIMASK in ISR | Deadlock, missed deadlines | Use BASEPRI in ISRs; never PRIMASK/FAULTMASK |
| Wrong PRIGROUP | Priority inversion, unexpected preemption | Configure once at startup; verify with __NVIC_GetPriorityGrouping() |
| BASEPRI not restored | Interrupts permanently masked | Use __get_BASEPRI()/__set_BASEPRI() pairs; check all exit paths |
| Subpriority ignored | Non-deterministic ISR order | Set explicit subpriorities for same-group interrupts |
| ISR calls blocking API | HardFault, kernel crash | Only use FromISR APIs; check configMAX_SYSCALL_INTERRUPT_PRIORITY |
+==============================================================+| 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 || ~~~~~~~~~~~~~~~~~~~~~~ |+==============================================================+
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 transitionWITH 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 chainasserted arrives entry to Low
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:
configMAX_SYSCALL_INTERRUPT_PRIORITY for safety-critical interrupts that must never be maskedThese practices ensure your interrupt subsystem remains deterministic, responsive, and debuggable — even under heavy interrupt load.
portable/GCC/ARM_CM3/portmacro.h and portable/GCC/ARM_CM4F/portmacro.hQuick Links
Legal Stuff





