Embedded & FirmwareInternubiquitous

NVIC: Priority Grouping, Preemption, Sub-Priority

Learn how NVIC priority grouping splits preemption and sub-priority bits, and how to configure PRIGROUP correctly in C for real-time deadlines.

9 min readAhmet Zahid ArıcanUpdated 30 Oct 2026
NVIC Priority Grouping, Preemption & Sub-Priority
Contents & prerequisites

Why interrupt priority matters in real-time systems

Interrupt priority determines which of several simultaneously pending events the processor services first, and in a real-time system that ordering is the difference between meeting a deadline and missing one. The Nested Vectored Interrupt Controller (NVIC) on ARM Cortex-M parts doesn't just queue interrupts in arrival order — it ranks them by a priority value you assign, and that ranking decides both which interrupt runs next and whether a running handler gets suspended mid-execution.

How preemption prevents missed deadlines

Preemption lets a higher-priority interrupt interrupt a lower-priority handler that is already executing, so a time-critical event doesn't wait behind a less urgent one. Without preemption, every interrupt would run to completion before the next could start, and a 2 ms UART handler could delay a 10 µs control-loop tick well past its deadline. The NVIC's preemption priority field is what makes this suspension possible: a pending interrupt with a numerically lower preemption priority value (which means higher urgency on Cortex-M — 0 is the highest) will stop a lower-priority handler and run immediately.

When sub-priorities resolve interrupt conflicts

Sub-priority only matters when two or more pending interrupts share the same preemption priority level. In that case the NVIC cannot preempt one with the other — neither will interrupt a running handler at that level — but it still has to pick which one fires first when both are pending at the same moment. Sub-priority is that tiebreaker: a purely ordering mechanism with no preemption effect of its own.

Trade-offs between priority depth and code complexity

More preemption levels give finer-grained real-time control but increase the number of nesting paths you must reason about and test. Every additional preemption level multiplies the possible interleavings between handlers, each of which can touch shared data, so stack depth, critical-section discipline, and worst-case latency analysis all get harder as the hierarchy deepens. Many production systems settle for two or three effective preemption levels — critical, normal, background — rather than using the full depth the hardware exposes.

How priority grouping splits the priority field

Priority grouping decides how the bits in each interrupt's priority register are divided between the preemption priority and the sub-priority. Cortex-M implements this with a single configuration register, and until you set it, the split between "can preempt" and "just tie-break" is undefined for your application's purposes.

What the PRIGROUP bits in AIRCR control

The PRIGROUP field lives in the Application Interrupt and Reset Control Register (AIRCR) and selects the binary split point within the implemented priority bits. Most Cortex-M implementations expose only the top 3 to 4 bits of each 8-bit priority field (check your device's reference manual for the exact number — it's commonly 3 bits on Cortex-M0/M0+-class parts and 4 bits on many M3/M4/M7 parts), with the unimplemented low bits reading as zero. PRIGROUP tells the NVIC where, within that implemented field, to draw the line between preemption bits (the upper group) and sub-priority bits (the lower group).

How to calculate preemption and sub-priority bit widths

If a device implements N priority bits and PRIGROUP selects a split at position P, you get (N − P) bits of preemption priority and P bits of sub-priority, subject to P not exceeding N. With N = 4 implemented bits, for example:

PRIGROUP settingPreemption bitsSub-priority bitsPreemption levelsSub-priority levels
All preemption40161
3 / 1 split3182
2 / 2 split2244
1 / 3 split1328
All sub-priority04116

The exact PRIGROUP encoding (which numeric value maps to which split) is defined by the architecture and documented in the ARM Cortex-M Architecture Reference Manual and in CMSIS headers — don't hand-calculate it from guesswork when CMSIS gives you NVIC_SetPriorityGrouping() to do it correctly.

Why you need to set grouping before assigning priorities

Priority values you write before changing PRIGROUP are reinterpreted the moment you change it, because the hardware doesn't remember which bits you meant as preemption versus sub-priority — it just re-slices whatever is stored. Set the grouping once, early in startup, before any interrupt is enabled or any priority is assigned, and treat it as fixed for the life of the application. Changing PRIGROUP after interrupts are live is a common source of "it worked yesterday" bugs.

Configuring preemption priority to control nesting

Preemption priority is the field that actually governs whether one handler can interrupt another, and it is the only field that does. Everything else in the priority value is bookkeeping for ties.

How high preemption priority interrupts suspend lower ones mid-execution

When an interrupt with a numerically lower preemption priority value becomes pending while a handler with a higher value is executing, the processor stacks the current context — following the exception entry sequence detailed in ARM Cortex-M Pipeline Stages (M0 to M55) — and jumps to the new handler immediately. The interrupted handler resumes only after the preempting one, and anything it in turn preempts, returns.

When equal preemption priority prevents nesting

Two interrupts at the same preemption priority level never preempt each other, regardless of their sub-priority values. If interrupt A is executing and interrupt B at the same preemption level becomes pending, B waits until A's handler returns; sub-priority only decided which of A or B went first if both were pending simultaneously before either started.

Setting preemption levels for deterministic latency

Assign the fewest preemption levels that still separate truly time-critical sources from everything else, then verify worst-case latency by adding up the longest critical sections and lower-priority handler runtimes that can block your most urgent interrupt. A motor-control tick, a safety watchdog, and a UART byte receiver rarely need three separate preemption levels when two will do with less analysis burden.

Using sub-priority for non-preemptive ordering

Sub-priority exists to make ordering deterministic among interrupts that share a preemption level, not to add another layer of preemption.

Why sub-priority matters when preemption priority is equal

If two interrupts at the same preemption level can both be pending at once — say, two peripheral DMA-complete flags that commonly arrive together — sub-priority guarantees a repeatable service order instead of leaving it to whichever bit the NVIC happens to sample first.

How the NVIC queues interrupts with the same preemption level

The NVIC holds all pending interrupts and, among those with the lowest (most urgent) preemption value, dispatches the one with the lowest sub-priority value first; ties at both fields fall back to the exception number, with lower numbers winning.

When sub-priority avoids context-switching overhead

Keeping related, similarly time-sensitive interrupts at one preemption level and separating them only by sub-priority avoids the stack-switch and pipeline-refill cost of full preemption between them, which matters on cores where that overhead is a measurable fraction of a short handler's runtime.

Reading and writing priority registers in C

CMSIS gives you functions that handle the bit-shifting PRIGROUP implies; use them instead of writing raw values.

How to use NVIC_SetPriority() and NVIC_GetPriority() correctly

/* Set grouping once at startup: e.g. 2 preemption bits, 2 sub-priority bits */
NVIC_SetPriorityGrouping(5);   /* value per CMSIS/ARM encoding table, not a bit count */

uint32_t pri = NVIC_EncodePriority(5, 1, 2);  /* grouping, preempt=1, sub=2 */
NVIC_SetPriority(TIM2_IRQn, pri);

uint32_t readback = NVIC_GetPriority(TIM2_IRQn);
uint32_t preempt, sub;
NVIC_DecodePriority(readback, 5, &preempt, &sub);

NVIC_EncodePriority() and NVIC_DecodePriority() take the PRIGROUP-derived grouping value and produce or parse the combined 8-bit field correctly for your part's implemented bit width.

Why raw register writes require bit-shifting based on PRIGROUP

Writing NVIC->IP[n] = priority directly means you must left-shift your priority value into the implemented high bits yourself, and that shift amount depends on how many priority bits your specific Cortex-M part implements — a detail the Bit-Field Struct for Register Mapping approach makes easy to get subtly wrong if the struct doesn't match the real bit width. CMSIS functions abstract this away; raw writes should be reserved for cases CMSIS doesn't cover.

Avoiding priority inversion by setting grouping before handler code runs

Call NVIC_SetPriorityGrouping() in your reset or system-init code, before main() enables any peripheral interrupt, and never call it again. A later change silently reinterprets every priority you've already assigned, which can turn what you thought was a low-sub-priority tiebreak into a full preemption level or vice versa — a subtle form of priority inversion that only shows up under load.

Common priority grouping patterns and their trade-offs

Three configurations cover most applications; pick based on whether your problem is nesting depth or tie-breaking among simultaneous events.

All-preemption vs. all-sub-priority configurations

All-preemption (0 sub-priority bits) gives maximum nesting granularity but means every interrupt can potentially preempt every other distinct-priority one, multiplying test cases. All-sub-priority (0 preemption bits) gives you only ordering among pending interrupts with zero preemption at all — fine for a system where nothing is truly time-critical relative to anything else, but wrong for hard real-time control loops.

Balanced grouping for mixed real-time and background tasks

A split such as 2 preemption bits / 2 sub-priority bits (on a 4-bit-priority device) is a common middle ground: four genuine preemption tiers (e.g., safety, control loop, communications, background), each with four sub-priority slots for ordering same-tier events. This keeps the nesting diagram small enough to review by hand while still giving deterministic ordering within each tier.

How to verify your grouping choice prevents priority inversion

Trace every shared resource — buffers, peripheral registers, RTOS queues accessed from Register-Level Peripheral Access via CMSIS code — and confirm that no low-preemption handler holds a lock that a higher-preemption handler also needs, since the NVIC's preemption mechanism offers no protection against that; it only governs execution order, not resource access. Static analysis tools and a hand-drawn interrupt dependency chart catch most of these before they reach hardware.

Debugging priority conflicts and unexpected preemption behavior

Most "wrong priority" bugs trace back to PRIGROUP being set differently than the developer assumed, or to a priority value written before grouping was finalized.

Why interrupt A doesn't preempt interrupt B even though its priority is higher

Check whether A and B land in the same preemption group after the PRIGROUP split is applied — a priority value that looks numerically lower in the full 8-bit field can still map to the same preemption level as a higher-looking one if several low bits fall into the sub-priority portion. Also confirm B's handler hasn't raised the current execution priority via BASEPRI or entered a critical section that masks A.

How to inspect PRIGROUP and priority register state

Read AIRCR and each relevant NVIC_IPRx register directly in your debugger's register view, then run the raw values back through NVIC_DecodePriority() by hand or in a watch expression to see the actual preemption/sub-priority split rather than trusting the value you think you wrote.

Tools and techniques to log preemption events during development

Toggle a spare GPIO pin at handler entry and exit and capture the trace on a logic analyzer or oscilloscope to see actual nesting in time; on cores with Instrumentation Trace Macrocell (ITM) or Embedded Trace Macrocell (ETM) support, timestamped trace output gives the same picture without consuming a pin, and is worth the setup cost once interrupt counts grow past a handful.

Learning

Sign in to track your progress.

Evidence

Public projects engineers linked to NVIC: Priority Grouping, Preemption, Sub-Priority.

Add evidence

No engineer has linked a project to this topic yet. Built something that proves it? Add the project and tag it with embedded-firmware-nvic-priority-grouping-preemption-sub-priority — it then shows here and on your public profile.