Electronics Design AU
STM32

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 GroupPreemption bitsSub-priority bitsPreemption levelsSub-priority levels
NVIC_PRIORITYGROUP_440161
NVIC_PRIORITYGROUP_33182
NVIC_PRIORITYGROUP_22244
NVIC_PRIORITYGROUP_11328
NVIC_PRIORITYGROUP_004116

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:

ExceptionFixed priorityNotes
Reset−3Always executes; cannot be masked
NMI (Non-Maskable Interrupt)−2Cannot be masked; used for clock failure, watchdog
HardFault−1Handles all unrecoverable faults; cannot be masked
SysTick (Cortex-M)ConfigurableFreeRTOS 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 valueRole
0–4Time-critical ISRs; must NOT call any FreeRTOS API
5–14Normal peripheral ISRs; may call ...FromISR() functions
15FreeRTOS 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:

  1. Identify the time-critical paths — which ISRs have the tightest latency requirements? These get numerically lower priority values.
  2. 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.
  3. 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.
  4. 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 call FromISR() APIs — confirm their priorities are at or above configMAX_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 configASSERT in FreeRTOS debug builds — FreeRTOS includes priority validation in debug builds when configASSERT is 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_3 or lower is used, sub-priority bits are included in the NVIC priority register fields. FreeRTOS's BASEPRI-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 priorityHAL_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_PRIORITY to 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

Related Forum Discussions