UNIT 09 · LESSON 3 OF 6

Latency, Priorities, and Nesting

What decides how long an interrupt waits, and how do you guarantee the important one never waits long?

INTERACTIVEWho runs first?
Three interrupt handlers scheduled by priority on a timelineUART RXtimer tickmotor fault10 µs20 µs30 µs40 µs50 µsUART RX (priority 1)waits 0 µs, done after 8 µstimer tick (priority 1)waits 6 µs, done after 26 µsmotor fault (priority 1)waits 23 µs, done after 27 µsThe fault is serviced 27 µs after it happened, behind handlers that are lessimportant. Give it the most urgent priority.
Three interrupt handlers scheduled by priority on a timelineUART RXtimer tickmotor fault10 µs20 µs30 µs40 µs50 µsUART RX (priority 1)wait 0, done 8 µstimer tick (priority 1)wait 6, done 26 µsmotor fault (priority 1)wait 23, done 27 µsThe fault is serviced 27 µs after it happened,behind handlers that are less important. Give it themost urgent priority.

Try this

UART RX priority
timer tick priority
motor fault priority
UART RX: done after 8 µs; timer tick: done after 26 µs; motor fault: done after 27 µs.

Three interrupts arrive close together: a UART receive at t = 10 (8 µs handler), a timer tick at t = 12 (20 µs) and a motor-fault input at t = 15 (4 µs). A more urgent interrupt preempts a less urgent one; equal priorities never preempt, and when several are pending the lowest IRQ number goes first (numbered here in the order listed). Change the priorities and watch how long the fault waits before its handler finishes.

What you will be able to do
  • Explain priority numbers (lower is more urgent) and how many levels a core implements.
  • Convert between a priority level and the NVIC register value, and avoid the CMSIS/raw-value trap.
  • Predict the order and response time of overlapping interrupts from their priorities.
  • List the components of worst-case interrupt latency and estimate it.
  • Choose between PRIMASK and BASEPRI for a critical section on Armv7-M.
Before you start
  • Pending, active, preemption and tail-chaining (lesson 2).
  • Bit fields and shifts (unit 2, lesson 1).
Steps in this lesson
  1. Priority numbers
  2. The encoding trap
  3. Nesting and grouping
  4. Worst-case latency
  5. Masking selectively: PRIMASK and BASEPRI
  6. Worked example: the 27 µs shutdown
  7. Common misconceptions

The puzzle

A motor controller has a fault input that must shut the power stage off within 10 µs. It also has a UART, a millisecond tick and a display. Under test, the shutdown usually takes 3 µs and occasionally 27. Nothing is wrong with the fault handler; it just had to wait. What decides how long an interrupt waits, and how do you guarantee the important one never waits long?

STEP 1

Priority numbers

Every interrupt has a priority, and lower numbers are more urgent: 0 beats 1. A more urgent interrupt preempts a less urgent handler (lesson 2); equal priorities never preempt each other. When several interrupts of equal priority are pending, the NVIC takes the one with the lowest exception (IRQ) number first, not the one that arrived first.

The NVIC stores 8 bits per interrupt but implements only the top few: 2 on the Cortex-M0+ (4 levels), 4 on the STM32F4 (16 levels). The unimplemented low bits read as zero.

↑ This step uses the figure at the top of the page.

STEP 2

The encoding trap

Two common APIs disagree on what the number means. CMSIS’s NVIC_SetPriority(irq, p) takes a level in the implemented range and shifts it into the top bits; the pico-sdk’s irq_set_priority(irq, p) takes the raw 8-bit value. On a Cortex-M0+:

register=(p≪(8−PRIO_BITS)) & 0xFF\text{register} = (p \ll (8 - \text{PRIO\_BITS}))\ \&\ \text{0xFF}

so NVIC_SetPriority(irq, 0x80) writes (0x80 << 6) & 0xFF = 0x00: the most urgent level, the opposite of the SDK’s default of 0x80, which is level 2 of 4.

INTERACTIVEPriority numbers are not portable
The NVIC priority byte written by a priority-setting call and the resulting levelpriority byte in the NVIC00000000teal: the 2 implemented bits; the rest always read as 0NVIC_SetPriority(irq, 0x80) writes 0x00: level 0 of 4 (0 = most urgent)p = 0x80 is outside 0–3: the shift pushes its bits out of the byte and the result islevel 0, probably not what was meant.
The NVIC priority byte written by a priority-setting call and the resulting levelpriority byte in the NVIC00000000teal: the 2 implemented bits; the rest always read as 0NVIC_SetPriority(irq, 0x80) writes 0x00: level 0 of4 (0 = most urgent)p = 0x80 is outside 0–3: the shift pushes its bitsout of the byte and the result is level 0, probablynot what was meant.
Function
p
Implemented priority bits
Register 0x00, level 0 of 4.

The NVIC keeps an 8-bit priority field per interrupt but implements only its top bits: 2 on the Cortex-M0+, as on the RP2040. CMSIS’s NVIC_SetPriority() takes a number in the implemented range and shifts it into place; the pico-sdk’s irq_set_priority() takes the raw 8-bit value (its default is 0x80). Passing a raw value to the CMSIS function silently becomes a different priority.

STEP 3

Nesting and grouping

Preemption nests: a level-0 handler can interrupt a level-1 handler that interrupted a level-2 one, each stacking its own frame, so the stack must hold one frame per level in use at once (unit 3, lesson 5). Armv7-M cores (Cortex-M3, M4) can also split the priority bits into a preemption part and a sub-priority part (NVIC_SetPriorityGrouping): only the preemption part decides preemption, while the sub-priority orders pending interrupts of equal preemption level.

STEP 4

Worst-case latency

The time from an event to the first instruction of its handler is at most:

tlatency≤tentry+tmasked+max⁡tequal priority+∑tmore urgentt_{\text{latency}} \le t_{\text{entry}} + t_{\text{masked}} + \max t_{\text{equal priority}} + \sum t_{\text{more urgent}}
  • entry: the core’s fixed exception-entry time (in the core’s technical reference manual), plus wait states if the vector table or handler is in slow flash;
  • masked: the longest stretch with interrupts disabled (critical sections, lesson 5);
  • handlers: the longest handler of equal priority that may be running, plus every more urgent handler that can arrive meanwhile.

To guarantee the fault input, give it the most urgent priority, keep critical sections short, and keep more urgent handlers (there should be none) short.

STEP 5

Masking selectively: PRIMASK and BASEPRI

PRIMASK masks all configurable interrupts: simple, and on the Cortex-M0+ the only option. The pico-sdk’s save_and_disable_interrupts() reads PRIMASK and sets it, and restore_interrupts() puts the old value back, so critical sections nest correctly. Armv7-M adds BASEPRI, which masks only interrupts at or below a chosen urgency: a critical section can then keep the fault handler alive while blocking the UART and tick handlers that share its data. RTOSes use this to keep their kernel data safe without delaying the most urgent interrupts.

STEP 6

Worked example: the 27 µs shutdown

With all three sources at the same priority (the figure’s defaults), a UART receive arrives at t = 10 µs (8 µs handler), a tick at t = 12 µs (20 µs) and the fault at t = 15 µs (4 µs). When the UART handler ends at 18 µs, the tick and the fault are both pending; the tick has the lower IRQ number here, so it goes first:

fault done at 10+8+20+4=42 μs⇒42−15=27 μs after the fault\text{fault done at } 10 + 8 + 20 + 4 = 42\ \mu\text{s} \quad\Rightarrow\quad 42 - 15 = 27\ \mu\text{s after the fault}

Had the fault had the lower IRQ number, it would have finished at 22 µs, 7 µs after it happened: IRQ numbers are a tie-breaker, not a design tool. Give the fault priority 0, the UART 1 and the tick 2: the fault preempts whatever is running and is done 4 µs after it happens (plus the entry time). The UART and tick handlers finish 4 µs later than before, which they can afford.

MYTHS AND FACTS

Common misconceptions

Higher number, higher priority

On Cortex-M it is the reverse: 0 is the most urgent.

All 256 priority values are distinct

Only the implemented top bits count: four levels on a Cortex-M0+.

NVIC_SetPriority takes the same numbers as the SDK

CMSIS shifts a level; the pico-sdk takes the raw byte.

Disabling interrupts briefly is harmless

Every masked microsecond adds directly to every interrupt’s worst-case latency.

Check yourself

Answer in your head, then open the card.

On a Cortex-M0+, what register value does NVIC_SetPriority(irq, 3) write, and what does irq_set_priority(irq, 3) write?

CMSIS: 3 << 6 = 0xC0, the least urgent of four levels. pico-sdk: 0x03, which reads back as 0x00 (only the top two bits exist): the most urgent level.

On an STM32F4 (4 priority bits), what does NVIC_SetPriority(irq, 5) write?

5 << 4 = 0x50.

The longest critical section is 12 µs, the most urgent handler is 3 µs, and entry takes about 0.3 µs. What is the worst-case latency of an interrupt that has that most urgent priority?

About 0.3 + 12 + 3 = 15.3 µs (one equal-priority handler may already be running); the critical section dominates.

Why can BASEPRI give better latency than PRIMASK for a critical section?

BASEPRI masks only interrupts at or below a chosen urgency, so the most urgent handlers keep running during the critical section; PRIMASK masks all of them.

Sources (4)
  1. Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_irq/irq.h — irq_set_priority: “Numerically-lower values indicate a higher priority. Hardware priorities range from 0 (highest priority) to 255 (lowest priority) though only the top 2 bits are significant on ARM Cortex-M0+”; PICO_DEFAULT_IRQ_PRIORITY 0x80, set for every IRQ at start-up
  2. Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h, core_cm4.h and m-profile/cmsis_gcc_m.h — __NVIC_SetPriority writes (priority << (8U − __NVIC_PRIO_BITS)) & 0xFF into IPR; __NVIC_PRIO_BITS defaults to 2 for the Cortex-M0+; core_cm4.h adds NVIC_SetPriorityGrouping (preemption and sub-priority); __set_PRIMASK and __set_BASEPRI (“Assigns the given value to the Base Priority register”)
  3. STMicroelectronics, cmsis-device-f4, stm32f407xx.h — “#define __NVIC_PRIO_BITS 4U /*!< STM32F4XX uses 4 Bits for the Priority Levels */”
  4. Raspberry Pi Ltd, pico-sdk 1.5.1, hardware_sync/sync.h — save_and_disable_interrupts: mrs PRIMASK, then cpsid i; restore_interrupts writes the saved PRIMASK back