What is NVIC Interrupt Priority-Embedded C Training in Hyderabad

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.

USART Interrupt Mapping (OR-gate network)
TX Events
TC & TCIE → Transmit Complete
TXE & TXEIE → TX Data Register Empty
CTSIF & CTSIE → CTS Line Change
⊕
RX Events
IDLE & IDLEIE → Idle Line Detected
ORE & RXNEIE → Overrun Error
RXNE & RXNEIE → RX Not Empty
PE & PEIE → Parity Error
⊕
USART
Interrupt
→ NVIC
Error / LIN Events (also feed the OR gate above)
LBD & LBDIE → LIN Break Detected
FE / NE / ORE + EIE + DMAR → Framing Error / Noise / Overrun (in DMA receive mode)
Each pair is: (status flag) AND (enable bit). Any AND gate output of 1 drives the final OR gate, producing the single USART IRQ signal. The ISR always reads SR to determine which flag(s) caused the interrupt.
Status FlagEnable BitEvent meaningClear method
TCTCIE (CR1)Transmit complete — shift register empty, last byte sentWrite 0 to TC in SR, or read SR then write DR
TXETXEIE (CR1)TX data register empty — ready for next byteWrite to DR
RXNERXNEIE (CR1)Receive data register not empty — byte ready to readRead DR
ORERXNEIE or EIE+DMAROverrun error — new byte arrived before previous was readRead SR then read DR
IDLEIDLEIE (CR1)Idle line detected — useful for end-of-packet detectionRead SR then read DR
PEPEIE (CR1)Parity errorRead SR then read DR
CTSIFCTSIE (CR3)CTS input toggled (hardware flow control)Write 0 to CTSIF in SR
LBDLBDIE (CR2)LIN break detectedWrite 0 to LBD in SR
FE / NEEIE (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.

NVIC_ISPR0 and NVIC_ISPR1 Layout
NVIC_ISPR0 (0xE000E200) — covers IRQ0 to IRQ31
Bit 31
IRQ31
SETPEND bits [30:1] → IRQ30 .. IRQ1
Bit 0
IRQ0
NVIC_ISPR1 (0xE000E204) — covers IRQ32 to IRQ63
Bit 31
IRQ63
SETPEND [30:4] → IRQ62..IRQ36
Bit 3
IRQ35
Bit 2
IRQ34
Bit 1
IRQ33
Bit 0
IRQ32
Orange bits: IRQ32-35 (e.g., IRQ39 for USART3 is bit 7 of ISPR1)
SETPEND bit behaviour:
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
RegisterAddressIRQs covered
NVIC_ISPR00xE000E200IRQ0 – IRQ31 (bit 0 = IRQ0, bit 31 = IRQ31)
NVIC_ISPR10xE000E204IRQ32 – IRQ63 (bit 0 = IRQ32, bit 7 = IRQ39 = USART3)
NVIC_ISPR20xE000E208IRQ64 – IRQ95
NVIC_ISPR3–70xE000E20C–1CIRQ96 – 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

Priority Value vs Urgency Example
ADC
Priority value = 5
Lower urgency
vs
TIMER
Priority value = 4
Higher urgency
→
NVIC decision
TIMER ISR runs first
(value 4 < value 5)
Both IRQs arrive at the NVIC simultaneously. TIMER’s value (4) is numerically less than ADC’s value (5), so TIMER has higher urgency. The NVIC accepts TIMER first. ADC remains pending until TIMER ISR exits.
  • 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.

8-bit Priority Field — Vendor Implementation
ARM spec: 8-bit priority field per IRQ (256 levels possible)
Bit 7
Bit 6
Bit 5
Bit 4
Bit 3
Bit 2
Bit 1
Bit 0
← implemented by STM32F4 (bits 7:4)
read as 0 →
STM32F4x — 4 bits implemented → 16 priority levels (0..15)
[7] ✓
[6] ✓
[5] ✓
[4] ✓
[3] 0
[2] 0
[1] 0
[0] 0
Priority value 5 is stored as: 0101_0000 = 0x50 in the 8-bit field. Bits [3:0] are always 0.
TI TM4C123Gx — 3 bits implemented → 8 priority levels (0..7)
[7] ✓
[6] ✓
[5] ✓
[4] 0
[3] 0
[2] 0
[1] 0
[0] 0
MCU FamilyBits ImplementedPriority LevelsValid ValuesNotes
STM32F4xx (Cortex-M4)4 (bits 7:4)160–15 (written as 0x00, 0x10, 0x20 … 0xF0)Most STM32 HAL functions accept 0–15; CMSIS shifts automatically
STM32F0xx (Cortex-M0)2 (bits 7:6)40–3 (written as 0x00, 0x40, 0x80, 0xC0)Cortex-M0 NVIC is simpler; no preemption grouping
TI TM4C123Gx (Cortex-M4)3 (bits 7:5)80–7 (written as 0x00, 0x20, 0x40 … 0xE0)3-bit implementation despite full Cortex-M4 core
nRF52840 (Cortex-M4F)3 (bits 7:5)80–7Nordic BLE SoC; software IRQs used for task-like scheduling
Maximum possible (ARM spec)82560–255No 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).

NVIC_IPR0 layout — stores priorities for IRQ0, IRQ1, IRQ2, IRQ3
bits 31:24
IRQ3
[7:4] priority
[3:0] = 0
bits 23:16
IRQ2
[7:4] priority
[3:0] = 0
bits 15:8
IRQ1
[7:4] priority
[3:0] = 0
bits 7:0
IRQ0
[7:4] priority
[3:0] = 0
Address of NVIC_IPR0 = 0xE000E400. NVIC_IPR1 = 0xE000E404 (IRQ4–7), and so on.

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:

Priority Field Split (example: 4 bits total, 3:1 split)
Pre-empt Priority
bits [7:5] of IPR byte
Determines nesting
Sub-Priority
bit [4] of IPR byte
Tie-breaker
PropertyPre-empt PrioritySub-Priority
PurposeDetermines whether a new interrupt can preempt (nest inside) a running ISRTie-breaker between two pending interrupts with the same pre-empt priority
Preemption ruleA new IRQ can preempt the active ISR only if its pre-empt priority is numerically lowerSub-priority never causes preemption — only determines which one runs first when both are pending simultaneously
Configured byPRIGROUP in AIRCR[10:8]PRIGROUP in AIRCR[10:8]
Typical usageAssign 3–4 bits of preempt priority for nesting control0–1 bits; often all bits given to pre-empt (no sub-priority used)

PRIGROUP settings for STM32F4 (4 implemented bits)

PRIGROUP valuePre-empt bitsSub-priority bitsPre-empt levelsSub levels
0 (AIRCR bits = 0b011)40161 (no sub)
1 (AIRCR bits = 0b100)3182
2 (AIRCR bits = 0b101)2244
3 (AIRCR bits = 0b110)1328
4 (AIRCR bits = 0b111)041 (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

EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *