Nested Vector Interrupts Control

NVIC stands for Nested Vector Interrupt Controller. It's a dedicated peripheral baked into every ARM Cortex-M core that decides which interrupt runs, when, and at what priority. Understanding it is non-negotiable if you want responsive firmware.

NVIC is a method of prioritizing interrupts, improving the MCU's performance and reducing interrupt latency. It also provides implementation schemes for handling interrupts that occur when others are being executed — or when the CPU is in the process of restoring its previous state and resuming its suspended process. With context switching, the previous interrupt or program continues executing from its suspend point.

Why prioritization matters

On a Cortex-M, every external IRQ and most system exceptions go through the NVIC. Each one carries a configurable priority. When two interrupts arrive close together, the NVIC compares their priorities and lets the more urgent one preempt the less urgent — that's the "nested" part. This is what keeps a fast UART or timer ISR responsive while a slower handler is still running.

How priority is encoded

Each interrupt has an 8-bit priority register. The catch: ARM only mandates that the upper bits be implemented. Most chips give you 3 or 4 bits (8 or 16 distinct levels); the unimplemented low bits read as zero. You write to them, but they have no effect.

FamilyPriority bitsLevels
Cortex-M0+24
STM32F1 / F4 / F7416
nRF5238
RP2040 (M0+)24

Lower number = higher priority. Priority 0 is the most urgent. This trips up everyone at least once.

Preemption priority and sub-priority

ARM splits the priority byte into two fields:

The split point between the two fields is configured once with NVIC_SetPriorityGrouping() from CMSIS. Most projects pick "all preemption, no sub-priority" — it's simpler and rarely the wrong call.

// Configure: 4 bits preemption, 0 bits sub-priority (STM32 default)
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

// Configure USART2 IRQ: priority 5, then enable
NVIC_SetPriority(USART2_IRQn, 5);
NVIC_EnableIRQ(USART2_IRQn);

Tail-chaining and late arrival

Two micro-architectural tricks the NVIC does for free:

You don't have to do anything to enable these — they're always on.

Key NVIC registers (CMSIS names)

RegisterPurpose
NVIC->ISER[]Set Enable — write 1 to enable an IRQ
NVIC->ICER[]Clear Enable — write 1 to disable
NVIC->ISPR[]Set Pending — manually mark an IRQ pending
NVIC->ICPR[]Clear Pending
NVIC->IABR[]Active flag (read-only) — is this IRQ currently running?
NVIC->IPR[]Priority — one byte per IRQ

You almost never poke these directly — CMSIS gives you NVIC_EnableIRQ, NVIC_DisableIRQ, NVIC_SetPriority, NVIC_GetPriority, NVIC_SetPendingIRQ, NVIC_ClearPendingIRQ, NVIC_GetActive.

Globally enable/disable interrupts

__disable_irq();   // PRIMASK = 1, masks all configurable IRQs
// critical section
__enable_irq();    // PRIMASK = 0

For finer-grained masking — block only IRQs at or below a given priority — use __set_BASEPRI(). RTOS kernels rely heavily on this.

Common pitfalls

The FreeRTOS angle

FreeRTOS port for Cortex-M lets you split the priority space:

This is why FreeRTOS port docs spend so much ink on priority configuration — get it wrong and you'll see seemingly random crashes.

Originally published on the WordPress mirror. — The first version of this post lives at thedarknightcom.wordpress.com.
← Back to all posts