NVIC Interrupt Priority
Learn how ARM Cortex-M interrupt priority works — the inverse relationship between priority value and urgency, how MCU vendors implement a subset of the 8-bit priority field, USART internal interrupt mapping, NVIC pending registers, and the critical distinction between pre-empt priority and sub-priority.
Introduction
EmbeddedPathashala — ARM Cortex-M Course Home All Lectures STM32 Projects
How a Peripheral Generates One Interrupt Line
A complex peripheral like USART can trigger an interrupt for many different reasons — received data, transmitted data, errors, idle line, CTS changes. Yet the NVIC receives only one IRQ signal per peripheral. The peripheral’s internal logic combines all its individual event flags into a single interrupt output using an OR-gate network controlled by enable bits.
Understanding this mapping is essential: the ISR must read the peripheral’s status register to determine which event(s) fired, because the NVIC cannot tell you which internal flag caused the interrupt — it only knows that the peripheral’s IRQ line is asserted.
TXE & TXEIE → TX Data Register Empty
CTSIF & CTSIE → CTS Line Change
ORE & RXNEIE → Overrun Error
RXNE & RXNEIE → RX Not Empty
PE & PEIE → Parity Error
Interrupt
→ NVIC
FE / NE / ORE + EIE + DMAR → Framing Error / Noise / Overrun (in DMA receive mode)
| Status Flag | Enable Bit | Event meaning | Clear method |
|---|---|---|---|
| TC | TCIE (CR1) | Transmit complete — shift register empty, last byte sent | Write 0 to TC in SR, or read SR then write DR |
| TXE | TXEIE (CR1) | TX data register empty — ready for next byte | Write to DR |
| RXNE | RXNEIE (CR1) | Receive data register not empty — byte ready to read | Read DR |
| ORE | RXNEIE or EIE+DMAR | Overrun error — new byte arrived before previous was read | Read SR then read DR |
| IDLE | IDLEIE (CR1) | Idle line detected — useful for end-of-packet detection | Read SR then read DR |
| PE | PEIE (CR1) | Parity error | Read SR then read DR |
| CTSIF | CTSIE (CR3) | CTS input toggled (hardware flow control) | Write 0 to CTSIF in SR |
| LBD | LBDIE (CR2) | LIN break detected | Write 0 to LBD in SR |
| FE / NE | EIE (CR3) | Framing error / Noise error (only signalled in DMA mode) | Read SR then read DR |
Why your ISR must check SR explicitly
/* USART2 ISR — must check every possible flag, not just assume RXNE */
void USART2_IRQHandler(void)
{
uint32_t sr = USART2->SR;
uint32_t cr1 = USART2->CR1;
/* Check RXNE first — most common event */
if ((sr & USART_SR_RXNE) && (cr1 & USART_CR1_RXNEIE)) {
uint8_t byte = (uint8_t)USART2->DR; /* reading DR clears RXNE */
rx_buffer_put(byte);
}
/* Check overrun — also asserted via RXNEIE path */
else if ((sr & USART_SR_ORE) && (cr1 & USART_CR1_RXNEIE)) {
(void)USART2->SR; /* read SR then DR to clear ORE */
(void)USART2->DR;
overrun_error_count++;
}
/* Transmit complete */
if ((sr & USART_SR_TC) && (cr1 & USART_CR1_TCIE)) {
USART2->CR1 &= ~USART_CR1_TCIE; /* disable TC interrupt */
tx_complete_callback();
}
}
NVIC Interrupt Set-Pending Registers (ISPR)
The Interrupt Set-Pending Registers (NVIC_ISPR0 through NVIC_ISPR7) allow software to read or force-pend any external IRQ. Each register is 32 bits wide, covering 32 consecutive IRQ lines. Writing a 1 to a bit sets that IRQ to pending state; writing 0 has no effect. Reading a bit tells you whether the IRQ is currently pending.
IRQ31
IRQ0
IRQ63
IRQ35
IRQ34
IRQ33
IRQ32
Write 1 → sets that IRQ to pending (even if the peripheral hasn’t fired — software-trigger)
Write 0 → no effect (use ICPR to clear pending, not ISPR)
Read 1 → IRQ is currently pending
Read 0 → IRQ is not pending
| Register | Address | IRQs covered |
|---|---|---|
| NVIC_ISPR0 | 0xE000E200 | IRQ0 – IRQ31 (bit 0 = IRQ0, bit 31 = IRQ31) |
| NVIC_ISPR1 | 0xE000E204 | IRQ32 – IRQ63 (bit 0 = IRQ32, bit 7 = IRQ39 = USART3) |
| NVIC_ISPR2 | 0xE000E208 | IRQ64 – IRQ95 |
| NVIC_ISPR3–7 | 0xE000E20C–1C | IRQ96 – IRQ239 |
/* Software-trigger USART3 IRQ39 (test ISR without real hardware) */
/* IRQ39 → ISPR[1] bit 7 */
#define NVIC_ISPR1 (*(volatile uint32_t *)0xE000E204U)
NVIC_ISPR1 = (1U << 7); /* pend USART3 IRQ */
/* Using CMSIS: */
NVIC_SetPendingIRQ(USART3_IRQn);
/* Check if IRQ39 is pending: */
bool usart3_pending(void)
{
return (NVIC_GetPendingIRQ(USART3_IRQn) != 0);
}
/* Clear a pending IRQ (use ICPR, not ISPR): */
#define NVIC_ICPR1 (*(volatile uint32_t *)0xE000E284U)
NVIC_ICPR1 = (1U << 7); /* clear pending USART3 */
/* CMSIS: NVIC_ClearPendingIRQ(USART3_IRQn); */
Interrupt Priority: Urgency and Priority Value
Priority = Urgency
In everyday life, priority means urgency — something with higher priority gets serviced before something with lower priority. In a Cortex-M processor, when two interrupts arrive simultaneously (or a new interrupt arrives while one is executing), the NVIC must decide which handler runs first. That decision is governed entirely by the priority assigned to each interrupt.
Priority Value = a number encoding urgency
Each IRQ and each system exception (except Reset, NMI, HardFault which are fixed) has a priority value — a number stored in the NVIC_IPR registers. The priority value quantifies urgency relative to other exceptions.
ARM Cortex-M inverse rule:
A lower priority value number = higher urgency = higher priority.
A higher priority value number = lower urgency = lower priority.
This is the opposite of everyday English (“priority 1” in English means most important,
which matches: priority value 1 is more urgent than priority value 5 on ARM Cortex-M).
Worked example: TIMER vs ADC
(value 4 < value 5)
- TIMER priority value (4) is less than ADC priority value (5) → TIMER interrupt is more urgent
- TIMER priority is said to be higher than ADC priority
- If both arrive simultaneously, NVIC runs TIMER ISR first
- If TIMER arrives while the ADC ISR is executing, TIMER preempts ADC (nested interrupt)
- If ADC arrives while TIMER ISR is executing, ADC stays pending until TIMER finishes
Terminology note: In common usage, the terms “priority” and “priority value” are used interchangeably. When someone says “IRQ0’s priority is 4,” they mean the priority value stored in NVIC_IPR0 for IRQ0 is 4. Context determines whether “higher priority” means a lower number (ARM convention) or more important (intended meaning). Both refer to the same thing.
Priority Levels: The IPR Register and Vendor Implementation
The ARM Cortex-M architecture reserves an 8-bit field for each IRQ’s priority in the NVIC_IPR registers. This allows up to 256 distinct priority levels (values 0–255). However, ARM does not require all 8 bits to be implemented — chip vendors may implement only the most significant N bits, discarding the least significant bits.
| MCU Family | Bits Implemented | Priority Levels | Valid Values | Notes |
|---|---|---|---|---|
| STM32F4xx (Cortex-M4) | 4 (bits 7:4) | 16 | 0–15 (written as 0x00, 0x10, 0x20 … 0xF0) | Most STM32 HAL functions accept 0–15; CMSIS shifts automatically |
| STM32F0xx (Cortex-M0) | 2 (bits 7:6) | 4 | 0–3 (written as 0x00, 0x40, 0x80, 0xC0) | Cortex-M0 NVIC is simpler; no preemption grouping |
| TI TM4C123Gx (Cortex-M4) | 3 (bits 7:5) | 8 | 0–7 (written as 0x00, 0x20, 0x40 … 0xE0) | 3-bit implementation despite full Cortex-M4 core |
| nRF52840 (Cortex-M4F) | 3 (bits 7:5) | 8 | 0–7 | Nordic BLE SoC; software IRQs used for task-like scheduling |
| Maximum possible (ARM spec) | 8 | 256 | 0–255 | No known Cortex-M implementation uses all 8 bits |
Why MSB-aligned? Implemented bits always occupy the most significant positions (bits 7:4 for STM32F4). This means writing a priority value of 5 to an STM32F4 register requires shifting left by 4: IPR_value = (5U << 4) = 0x50. CMSIS’s NVIC_SetPriority() performs this shift automatically using __NVIC_PRIO_BITS (defined as 4 for STM32F4 in the device header).
NVIC Priority Registers (IPR) — Structure and Access
The NVIC contains 60 32-bit Interrupt Priority Registers (NVIC_IPR0 through NVIC_IPR59). Each 32-bit register holds priority fields for 4 consecutive IRQs, one byte per IRQ (8 bits each, though only the upper N bits are implemented).
[3:0] = 0
[3:0] = 0
[3:0] = 0
[3:0] = 0
Direct register access for any IRQ priority
/* Formula: IPR register index = IRQ_number / 4
Byte offset within register = IRQ_number % 4
Bit position in 32-bit word = (IRQ_number % 4) * 8 */
/* Set priority of IRQ39 (USART3) to value 5 on STM32F4 (4 bits implemented) */
/* Method 1: Direct register, 32-bit access */
#define NVIC_IPR_BASE ((volatile uint32_t *)0xE000E400U)
uint32_t irq = 39;
uint32_t reg_idx = irq / 4; /* = 9 → NVIC_IPR9 */
uint32_t byte_pos = irq % 4; /* = 3 → bits [31:24] of IPR9 */
uint32_t shift = byte_pos * 8; /* = 24 */
NVIC_IPR_BASE[reg_idx] &= ~(0xFFU << shift); /* clear current priority */
NVIC_IPR_BASE[reg_idx] |= ((5U << 4) << shift); /* write 5 (MSB-aligned) */
/* Method 2: Byte access (simpler) */
#define NVIC_IPR_BYTE ((volatile uint8_t *)0xE000E400U)
NVIC_IPR_BYTE[39] = (5U << 4); /* 0x50 */
/* Method 3: CMSIS (recommended — handles __NVIC_PRIO_BITS automatically) */
NVIC_SetPriority(USART3_IRQn, 5);
/* Read back: */
uint32_t current = NVIC_GetPriority(USART3_IRQn); /* returns 5 on STM32F4 */
Priority registers for system exceptions (SHPR)
System exceptions (MemManage, BusFault, UsageFault, SVC, PendSV, SysTick) use the System Handler Priority Registers (SHPR1–3) inside the SCB, not the NVIC_IPR registers. The format is identical — 8-bit fields, MSB-aligned.
/* System Handler Priority Register locations */
#define SCB_SHPR1 (*(volatile uint32_t *)0xE000ED18U) /* MemManage/BusFault/UsageFault */
#define SCB_SHPR2 (*(volatile uint32_t *)0xE000ED1CU) /* SVC */
#define SCB_SHPR3 (*(volatile uint32_t *)0xE000ED20U) /* PendSV / SysTick */
/* Set SVC priority = 2 (bits 31:24 of SHPR2) */
SCB->SHP[10 - 4] = (2U << 4); /* CMSIS index: SVCall maps to SHP[10-4]=SHP[6] */
/* CMSIS equivalents: */
NVIC_SetPriority(SVCall_IRQn, 2);
NVIC_SetPriority(PendSV_IRQn, 15); /* lowest priority for context switch */
NVIC_SetPriority(SysTick_IRQn, 15); /* same as PendSV */
Pre-empt Priority vs Sub-Priority
The NVIC priority field for each IRQ is further divided into two parts by the PRIGROUP setting in SCB_AIRCR. This split determines how two aspects of interrupt behaviour work:
Determines nesting
Tie-breaker
| Property | Pre-empt Priority | Sub-Priority |
|---|---|---|
| Purpose | Determines whether a new interrupt can preempt (nest inside) a running ISR | Tie-breaker between two pending interrupts with the same pre-empt priority |
| Preemption rule | A new IRQ can preempt the active ISR only if its pre-empt priority is numerically lower | Sub-priority never causes preemption — only determines which one runs first when both are pending simultaneously |
| Configured by | PRIGROUP in AIRCR[10:8] | PRIGROUP in AIRCR[10:8] |
| Typical usage | Assign 3–4 bits of preempt priority for nesting control | 0–1 bits; often all bits given to pre-empt (no sub-priority used) |
PRIGROUP settings for STM32F4 (4 implemented bits)
| PRIGROUP value | Pre-empt bits | Sub-priority bits | Pre-empt levels | Sub levels |
|---|---|---|---|---|
| 0 (AIRCR bits = 0b011) | 4 | 0 | 16 | 1 (no sub) |
| 1 (AIRCR bits = 0b100) | 3 | 1 | 8 | 2 |
| 2 (AIRCR bits = 0b101) | 2 | 2 | 4 | 4 |
| 3 (AIRCR bits = 0b110) | 1 | 3 | 2 | 8 |
| 4 (AIRCR bits = 0b111) | 0 | 4 | 1 (no nesting) | 16 |
/* STM32 HAL default: PRIGROUP = 4 → all 4 bits are pre-empt priority, no sub-priority */
/* This gives 16 nesting levels (0..15) and is the most common configuration */
/* Set PRIGROUP = 4 (all bits = pre-empt, none = sub) */
NVIC_SetPriorityGrouping(4);
/* Encode priority: 3 pre-empt bits, 1 sub-priority bit (PRIGROUP=1 example) */
uint32_t encoded = NVIC_EncodePriority(
1, /* PRIGROUP value */
5, /* pre-empt priority = 5 (range 0..7) */
0 /* sub-priority = 0 (range 0..1) */
);
NVIC_SetPriority(USART3_IRQn, encoded);
/* FreeRTOS recommendation for STM32F4:
Set configKERNEL_INTERRUPT_PRIORITY = 15 (lowest)
Set configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 (ISRs ≤ 4 cannot use FreeRTOS API)
All priority bits = pre-empt (PRIGROUP = 4) */
#define configKERNEL_INTERRUPT_PRIORITY (15 << 4) /* 0xF0 */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( 5 << 4) /* 0x50 */
FreeRTOS critical rule: On STM32F4, ISRs that call FreeRTOS API functions (e.g., xQueueSendFromISR) must have a priority value numerically ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY (i.e., equal to or lower urgency than the configured threshold). ISRs with higher urgency (numerically lower value) must never call any FreeRTOS API.
Complete Priority Configuration Example
The following example configures four interrupts on STM32F411 with a well-reasoned priority scheme. All bits are pre-empt (PRIGROUP=4 default). Lower value = runs sooner / can preempt higher values.
#include "stm32f4xx.h"
/* Priority scheme (STM32F4, 4-bit pre-empt, PRIGROUP=4):
0 → Reset (fixed –3, not configurable)
1 → NMI (fixed –2, not configurable)
2 → Hard (fixed –1, not configurable)
─────────────────────────────────────────
3 → (reserved for future critical use)
4 → TIM6 (DAC underrun alarm) — most urgent user ISR
5 → USART1 (high-speed data)
6 → USART3 (lower-speed data)
7 → ADC (periodic sample)
…
15 → PendSV / SysTick (RTOS scheduler — lowest) */
void priority_init(void)
{
/* PRIGROUP = 4: all 4 bits = pre-empt, no sub-priority */
NVIC_SetPriorityGrouping(4);
/* System exceptions */
NVIC_SetPriority(SVCall_IRQn, 3);
NVIC_SetPriority(PendSV_IRQn, 15);
NVIC_SetPriority(SysTick_IRQn, 15);
/* Peripheral IRQs */
NVIC_SetPriority(TIM6_DAC_IRQn, 4);
NVIC_SetPriority(USART1_IRQn, 5);
NVIC_SetPriority(USART3_IRQn, 6);
NVIC_SetPriority(ADC_IRQn, 7);
/* Enable all */
NVIC_EnableIRQ(TIM6_DAC_IRQn);
NVIC_EnableIRQ(USART1_IRQn);
NVIC_EnableIRQ(USART3_IRQn);
NVIC_EnableIRQ(ADC_IRQn);
}
/* Verification: read back all priorities */
void priority_verify(void)
{
/* These should print 4, 5, 6, 7 on STM32F4 */
uint32_t t = NVIC_GetPriority(TIM6_DAC_IRQn); /* = 4 */
uint32_t u1 = NVIC_GetPriority(USART1_IRQn); /* = 5 */
uint32_t u3 = NVIC_GetPriority(USART3_IRQn); /* = 6 */
uint32_t a = NVIC_GetPriority(ADC_IRQn); /* = 7 */
(void)t; (void)u1; (void)u3; (void)a;
}
Frequently Asked Questions
If I write priority value 0 to every IRQ, what happens?
All IRQs have the same pre-empt priority. None can preempt any other — once an ISR starts, it runs to completion regardless of any other IRQ arriving (no nesting). Pending IRQs are served in IRQ number order as a tie-breaker (lower IRQ number first) after the current ISR exits. This is fine for simple bare-metal systems with no nesting requirement.
Why do the lower 4 bits of the IPR byte always read as 0 on STM32F4?
The STM32F4 implements only the top 4 bits of the 8-bit priority field. The lower 4 bits are wired to 0 in hardware — writes to them are silently discarded and reads always return 0. Writing priority value 5 as the raw 8-bit value 5 (0b00000101) would actually set bits [3:0] which are not implemented, effectively writing a priority of 0 (the most urgent!). Always shift left by 4: 5 << 4 = 0x50. CMSIS does this automatically.
Can two IRQs have the same pre-empt priority but different sub-priorities?
Yes. If IRQ-A and IRQ-B have the same pre-empt priority, neither can preempt the other. When both are pending simultaneously, the NVIC uses sub-priority as a tie-breaker: lower sub-priority value runs first. If sub-priorities are also equal, the lower IRQ number wins. After the first one finishes, the second executes normally.
My USART ISR is called but I don’t know which event fired. What should I do?
Read USART->SR and USART->CR1 at the start of the ISR, then check each flag against its enable bit: RXNE & RXNEIE, TC & TCIE, etc. Never assume RXNE was the trigger just because the ISR fired — the USART OR-gate means any enabled event can be the cause. Failing to clear the correct flag can leave the ISR firing in an infinite loop.
What happens when I change PRIGROUP while IRQs are active?
Changing PRIGROUP dynamically while interrupts are enabled is dangerous. The NVIC immediately re-evaluates all active priorities using the new grouping, which can cause unexpected preemption or silently break nesting assumptions. Always configure PRIGROUP once at startup before enabling any IRQs, and never change it at runtime.
Why does FreeRTOS need configMAX_SYSCALL_INTERRUPT_PRIORITY?
FreeRTOS uses BASEPRI to implement critical sections — it raises BASEPRI to block all IRQs with priority value ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY. ISRs with lower priority values (higher urgency) are not masked and continue to run, but they must never call FreeRTOS API because the scheduler’s data structures may be in an inconsistent state inside a critical section. ISRs that do call FreeRTOS API must have priority values ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY.
NVIC Priority Interrupt Priority Value Pre-empt Sub-priority PRIGROUP AIRCR NVIC_IPR Register USART Interrupt Mapping NVIC ISPR Pending STM32F4 16 Levels FreeRTOS Priority Free Embedded Course
© EmbeddedPathashala — Free Embedded Systems Course — ARM Cortex-M3/M4 Series

2 Comments