How Do You Configure STM32 NVIC Interrupt Priorities?
Last updated 29 June 2026 · 9 min read
Direct Answer
STM32 uses 4 priority bits giving 16 levels (0 = highest, 15 = lowest). Configure with HAL_NVIC_SetPriority(IRQn, preemptionPriority, subPriority) and keep the priority grouping at NVIC_PRIORITYGROUP_4 (all 4 bits as preemption priority, no sub-priority) — this is the only grouping compatible with FreeRTOS. When using FreeRTOS, any ISR that calls a FromISR() API must have a numeric priority equal to or greater than configMAX_SYSCALL_INTERRUPT_PRIORITY. ISRs with a numerically lower priority (higher hardware priority) than this threshold will cause a hard fault if they call any FreeRTOS API.
Detailed Explanation
The ARM Cortex-M NVIC (Nested Vectored Interrupt Controller) handles all interrupt and exception routing on STM32 microcontrollers. Getting priorities right is critical in any non-trivial firmware design — and the consequences of getting them wrong, particularly with FreeRTOS, are immediate hard faults and unpredictable runtime failures. This guide covers the F4, G4, H7, L4, and U5 mainstream series — for an overview of how these families differ, see Which STM32 Family Should You Use?.
The Priority Model: Numbers and Levels
STM32 microcontrollers implement 4 priority bits in the NVIC (on most series — a few lower-end parts use 2 or 3 bits; always check the reference manual for the specific part). Four bits gives 16 priority levels, numbered 0 to 15.
The most important counter-intuitive fact: lower number = higher priority.
- Priority 0 is the highest priority — it can preempt everything else.
- Priority 15 is the lowest priority — it is preempted by everything.
Priority values are stored in the NVIC priority registers as left-justified values in an 8-bit field: the 4 implemented bits occupy bits [7:4], and bits [3:0] are not implemented (read as zero, writes ignored). The CMSIS NVIC_SetPriority() function and the STM32 HAL both handle this bit-shifting automatically — you work with logical priority values 0–15, and the hardware encoding is managed for you.
Priority Grouping: Preemption vs Sub-Priority
The 4 priority bits are split between two functions, configurable via the PRIGROUP field in the ARM AIRCR register:
- Preemption priority (upper bits): determines whether one ISR can interrupt another currently running ISR. An ISR with a numerically lower preemption priority can preempt an ISR with a numerically higher preemption priority.
- Sub-priority (lower bits): a tiebreaker when two pending interrupts share the same preemption priority level. The one with the lower sub-priority number is served first, but neither can preempt the other.
STM32 HAL provides five grouping options:
| Priority Group | Preemption bits | Sub-priority bits | Preemption levels | Sub-priority levels |
|---|---|---|---|---|
NVIC_PRIORITYGROUP_4 | 4 | 0 | 16 | 1 |
NVIC_PRIORITYGROUP_3 | 3 | 1 | 8 | 2 |
NVIC_PRIORITYGROUP_2 | 2 | 2 | 4 | 4 |
NVIC_PRIORITYGROUP_1 | 1 | 3 | 2 | 8 |
NVIC_PRIORITYGROUP_0 | 0 | 4 | 1 | 16 |
For almost all STM32 designs — and mandatorily for any design using FreeRTOS — use NVIC_PRIORITYGROUP_4. This dedicates all 4 bits to preemption priority, giving 16 distinct preemption levels and no sub-priority. CubeMX defaults to NVIC_PRIORITYGROUP_4.
Configuring Priorities with the HAL
Set the priority grouping once at startup, then configure each interrupt source:
/* Set priority grouping — call once before HAL_Init or in SystemClock_Config */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Configure and enable an interrupt */
HAL_NVIC_SetPriority(USART1_IRQn, 6, 0); /* preemption priority 6, sub-priority 0 */
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* Another interrupt at higher priority */
HAL_NVIC_SetPriority(TIM2_IRQn, 3, 0); /* preemption priority 3 — can preempt USART1 ISR */
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_NVIC_SetPriority takes three arguments: the interrupt request line (IRQn_Type), the preemption priority, and the sub-priority. With NVIC_PRIORITYGROUP_4, the sub-priority argument is always 0 — it has no effect.
CubeMX generates these calls automatically in the MX_xxx_Init() functions when you configure NVIC settings in the Pinout & Configuration tab. Check the generated code to confirm that your CubeMX priority assignments match your design intent.
Fixed Exception Priorities
Some ARM exceptions have fixed, non-configurable priorities that override the NVIC:
| Exception | Fixed priority | Notes |
|---|---|---|
| Reset | −3 | Always executes; cannot be masked |
| NMI (Non-Maskable Interrupt) | −2 | Cannot be masked; used for clock failure, watchdog |
| HardFault | −1 | Handles all unrecoverable faults; cannot be masked |
| SysTick (Cortex-M) | Configurable | FreeRTOS uses this for the RTOS tick |
HardFault always runs regardless of NVIC priority assignments. This is why priority misconfigurations often surface as hard faults — the corrupted RTOS state or bad memory access escalates to a HardFault that cannot be intercepted.
FreeRTOS Interrupt Priority Constraints
FreeRTOS on Cortex-M uses the CPU's BASEPRI register to implement critical sections. When FreeRTOS enters a critical section, it sets BASEPRI to configMAX_SYSCALL_INTERRUPT_PRIORITY — this masks all interrupts at or below that priority level (numerically equal to or greater than). Interrupts with a numerically lower priority (higher hardware priority) than configMAX_SYSCALL_INTERRUPT_PRIORITY bypass this mask entirely.
This creates a strict two-tier interrupt model:
Tier 1: Above FreeRTOS (priorities 0 to configMAX_SYSCALL_INTERRUPT_PRIORITY − 1)
These ISRs run regardless of FreeRTOS critical sections — they cannot be masked by BASEPRI. They must never call any FreeRTOS API function, including ...FromISR() variants. Calling FreeRTOS from this tier causes a hard fault. Use this tier only for safety-critical hardware interactions that must not be delayed by RTOS activity: emergency stop inputs, safety watchdog feeds.
Tier 2: FreeRTOS-managed (priorities configMAX_SYSCALL_INTERRUPT_PRIORITY to configKERNEL_INTERRUPT_PRIORITY)
These ISRs can safely call FreeRTOS ...FromISR() API functions (e.g. xQueueSendFromISR(), xSemaphoreGiveFromISR()). This is where the overwhelming majority of ISRs should operate: UART receive, SPI transfer complete, ADC conversion done, GPIO edge.
Tier 3: FreeRTOS kernel (SysTick and PendSV)
These run at configKERNEL_INTERRUPT_PRIORITY — the numerically highest value (lowest hardware priority). FreeRTOS itself should always have the lowest interrupt priority so that it cannot preempt user ISRs.
Practical priority assignment with FreeRTOS
A typical configuration for a 4-priority-bit STM32 with FreeRTOS:
/* In FreeRTOSConfig.h */
#define configKERNEL_INTERRUPT_PRIORITY 15 /* SysTick + PendSV — lowest priority */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5 /* ISRs at 5-14 may call FromISR APIs */
Under this configuration:
| Priority value | Role |
|---|---|
| 0–4 | Time-critical ISRs; must NOT call any FreeRTOS API |
| 5–14 | Normal peripheral ISRs; may call ...FromISR() functions |
| 15 | FreeRTOS kernel (SysTick, PendSV) — lowest priority |
Setting configMAX_SYSCALL_INTERRUPT_PRIORITY to 5 leaves priorities 0–4 available for time-critical hardware-only ISRs while providing 10 levels (5–14) for FreeRTOS-aware peripheral ISRs. Adjust to match the application's needs — a value of 5 is a common and safe starting point.
Important: NVIC_PRIORITYGROUP_4 is not optional with FreeRTOS. The BASEPRI mechanism that FreeRTOS relies on works correctly only when all priority bits are preemption bits. Any other priority grouping corrupts FreeRTOS's interrupt masking behaviour.
Bare-Metal Priority Assignment
Without FreeRTOS, priority assignment is less constrained but still important. A practical approach:
- Identify the time-critical paths — which ISRs have the tightest latency requirements? These get numerically lower priority values.
- Separate independent ISRs — if UART receive and TIM2 are at the same preemption priority, TIM2 cannot preempt a long UART ISR (or vice versa). Assign different preemption levels to independent high-urgency peripherals.
- Keep housekeeping at high numbers — SysTick, DMA completion handlers, and other housekeeping interrupts typically belong at 14–15. For DMA interrupt priority requirements in FreeRTOS-based designs, see how to configure STM32 HAL DMA.
- Use
NVIC_PRIORITYGROUP_4— even without FreeRTOS, using all bits for preemption priority is simpler and avoids confusion when reading or modifying the code later.
Design Considerations
- Configure priority grouping once — call
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)once at startup, before configuring any individual interrupt priorities. Changing the grouping after interrupts are configured invalidates all stored priority values. - Verify CubeMX output — CubeMX sets NVIC priorities in the generated
MX_xxx_Init()functions. Review the generated NVIC configuration before enabling FreeRTOS, particularly for peripherals that will callFromISR()APIs — confirm their priorities are at or aboveconfigMAX_SYSCALL_INTERRUPT_PRIORITY. - Never call FreeRTOS from a HardFault, BusFault, or UsageFault handler — these are fixed-priority exceptions that run above the BASEPRI mask. The same constraint applies as for Tier 1 ISRs.
- Use
configASSERTin FreeRTOS debug builds — FreeRTOS includes priority validation in debug builds whenconfigASSERTis defined. If an ISR calls a FreeRTOS function from an invalid priority, the assert fires at the moment of the call, pointing directly to the offending ISR rather than to a later hard fault. - Trace unexpected resets to interrupt priority — hard faults caused by NVIC misconfiguration often appear as unexpected MCU resets in production firmware if the HardFault handler just calls
NVIC_SystemReset(). Implement a HardFault handler that captures the fault address and registers to a persistent RAM area so the root cause survives the reset. See why does my STM32 keep resetting for how to read the reset cause register.
For complex interrupt priority schemes in production STM32 firmware — including FreeRTOS integration, DMA IRQ coordination, and safety-critical ISR design — Zeus Design's embedded firmware team designs interrupt architectures for commercial STM32 products.
Common Mistakes
- Calling xQueueSendFromISR() from a priority-0 ISR — the most common FreeRTOS-related hard fault. Any ISR that calls a FreeRTOS API must have a numeric priority equal to or greater than
configMAX_SYSCALL_INTERRUPT_PRIORITY. Set the ISR to priority 5 (or whatever the configured threshold is), not priority 0. - Using a non-4 priority grouping with FreeRTOS — if
NVIC_PRIORITYGROUP_3or lower is used, sub-priority bits are included in the NVIC priority register fields. FreeRTOS'sBASEPRI-based masking treats the full 4-bit field as preemption priority and masks incorrectly, leading to subtle race conditions and eventual hard faults. - Reversing "higher" and "lower" priority — in STM32 NVIC, priority 0 is the highest hardware priority (preempts everything). Priority 15 is the lowest (preempted by everything). Confusing numeric value with conceptual "level" leads to ISRs that cannot preempt when required, or that preempt when they should not.
- Forgetting to enable the interrupt after setting priority —
HAL_NVIC_SetPriority()configures the priority but does not enable the interrupt.HAL_NVIC_EnableIRQ()must also be called. CubeMX generates both calls in the init code, but manually written init sequences often miss the enable step. - Placing the FreeRTOS kernel at priority 0 — setting
configKERNEL_INTERRUPT_PRIORITYto 0 means the FreeRTOS tick ISR has the highest hardware priority and preempts all user ISRs, preventing time-critical ISRs from running promptly. The FreeRTOS kernel should always be at the numerically highest value (15 on a 4-bit STM32) to give it the lowest hardware priority.
Frequently Asked Questions
- Why does my STM32 hard fault when calling xQueueSendFromISR() from an ISR?
- The most common cause is that the ISR's numeric priority is lower than configMAX_SYSCALL_INTERRUPT_PRIORITY (that is, the ISR has a higher hardware priority than FreeRTOS allows for ISR API calls). FreeRTOS uses BASEPRI to mask interrupts during critical sections, but it can only mask interrupts at or below configMAX_SYSCALL_INTERRUPT_PRIORITY — higher-priority ISRs run regardless of this mask. If such an ISR calls a FreeRTOS API function, it corrupts internal RTOS state and causes a hard fault. Fix: raise the ISR's numeric priority to be equal to or greater than configMAX_SYSCALL_INTERRUPT_PRIORITY, or remove the FreeRTOS API call from the ISR.
- What is the difference between preemption priority and sub-priority in STM32?
- Preemption priority determines whether one interrupt can interrupt another currently running ISR — only a higher preemption priority (lower number) can preempt. Sub-priority is a tiebreaker: when two interrupts with the same preemption priority are both pending, the one with the lower sub-priority number is served first, but neither can preempt the other. FreeRTOS requires NVIC_PRIORITYGROUP_4 (all 4 bits as preemption priority, no sub-priority bits) because it uses BASEPRI, which operates on the full priority field. Mixed groupings with sub-priority bits can cause FreeRTOS's BASEPRI mask to work incorrectly.
- Can I change the NVIC priority grouping after HAL_Init()?
- Technically yes, but it is strongly discouraged. HAL_Init() calls HAL_MspInit() and sets the priority grouping — changing it afterwards invalidates all previously configured interrupt priorities, since their stored register values are interpreted differently under the new grouping. Set the priority grouping once at startup (or rely on CubeMX to set it), then configure all interrupts consistently under that grouping. With FreeRTOS, never deviate from NVIC_PRIORITYGROUP_4.
References
Related Questions
Which STM32 Family Should You Use?
Compare STM32 families for new designs: G0, G4, F4, H7, L4, U5, WB, and WL — performance tiers, power profiles, peripheral sets, and which to choose.
Why Does My STM32 Keep Resetting Unexpectedly?
STM32 unexpected resets are caused by watchdog timeout, brown-out, hard fault, or power decoupling issues. Use the RCC reset flags to identify the root cause.
How Does the STM32 Clock Tree Work?
The STM32 clock tree routes HSE or HSI through a PLL to generate SYSCLK, then divides it across AHB and APB buses. Learn how it works and how to configure it.
How Do You Configure STM32 HAL DMA for UART, SPI, and ADC?
Configure STM32 HAL DMA for UART, SPI, and ADC — normal vs circular mode, interrupt callbacks, double buffering, and cache coherency on STM32H7/F7.
How Do You Configure STM32 Peripherals with HAL and CubeMX?
STM32CubeMX generates HAL initialisation code for UART, SPI, and I2C from a GUI. This guide explains key settings and how generated code maps to the hardware.
What Are Interrupts in Embedded Systems and How Do They Work?
Interrupts let a microcontroller respond to hardware events instantly without polling. Learn how ISRs, NVIC priority, and interrupt latency work.
Related Forum Discussions
STM32F401 UART printing garbage after switching to 84 MHz PLL — same 115200 baud in CubeMX and PuTTY
Got a WeAct Black Pill (STM32F401CCU6) project that's been running happily on the default HSI clock at 16 MHz. Using USART1 on PA9/PA10 thro
STM32H743 HAL_UART_Receive_DMA fires error callback immediately — TEIF1 set, RxCplt never fires
Upgrading a project from STM32F4 to STM32H743. UART DMA receive worked on the F4 without any issues — standard CubeMX setup, call HAL_UART_R
STM32 GPIO interrupt configured but ISR never fires — what am I missing?
Trying to use a button on PA0 to trigger an interrupt on an STM32F411 Nucleo board. Using HAL, generated the init code with CubeMX. The GPIO
STM32 USB not detected by Windows after jumping to bootloader mode
Working on a custom STM32F411 board, trying to jump into the built-in USB DFU bootloader from application code instead of holding BOOT0 on p