Interrupts and the NVIC: priorities and short handlers
Priority groups, preemption and sub-priority, why handlers are kept short, and how to handle shared variables safely.
Three NVIC settings
- Enable the interrupt in the peripheral (for example
USART1->CR1 |= RXNEIE) - Enable that interrupt number in the NVIC (
NVIC_EnableIRQ(USART1_IRQn)) - Set the priority (
NVIC_SetPriority)
If any one is missing, the interrupt never arrives.
Priority
On STM32, a smaller number means a higher priority. A higher interrupt cuts in even while a lower one is being handled (preemption).
| Interrupt | Example priority |
|---|---|
| Overcurrent protection | 0 (highest) |
| Current-control ADC complete | 1 |
| Communication receive | 5 |
| Button | 10 |
With an RTOS, any interrupt that calls RTOS APIs must have a lower priority than configMAX_SYSCALL_INTERRUPT_PRIORITY.
Handler rules
- Set a flag, put data in a buffer, save a register value, and leave.
- No
printf, noHAL_Delay, no long calculations - Clear the flag that caused the interrupt
Shared variables
volatile uint32_t ticks;
uint32_t get_ticks(void) {
__disable_irq(); uint32_t t = ticks; __enable_irq(); // 32 bits is atomic, but for a struct this is required
return t;
}
When reading a multi-byte value or several variables together, turn interrupts off briefly or work on a copy.
Three places must all be opened to enable an interrupt
- Peripheral: which event raises a request (for example
USART1->CR1 |= USART_CR1_RXNEIE) - NVIC: whether that request line is accepted, and at what priority
- Global enable: whether the CPU is accepting interrupts at all
NVIC_SetPriority(USART1_IRQn, 2);
NVIC_EnableIRQ(USART1_IRQn);
USART1->CR1 |= USART_CR1_RXNEIE;
If any of the three is missing, the handler is never called. The safe order is to set the priority first and enable afterwards.
3D labRun what you have read so far→Priority: the smaller the number, the more urgent
On Cortex-M, a smaller value means a higher priority. 0 is the most urgent.
| Example | Priority | Reason |
|---|---|---|
| Overcurrent protection | 0 | Parts burn if it is late |
| Motor current control (PWM period) | 1 | Control falls apart if the period jitters |
| Communication receive | 2 | Only needs to be read within one byte time |
| System tick | 3 | Can be a little late |
A higher-priority interrupt cuts into a lower one's handler partway through (preemption). Interrupts of the same priority do not interrupt each other; they wait their turn.
In a picture: preemption
Keep critical sections short
If the main loop and a handler work on the same data, block interrupts on the main side only while it touches that data.
uint32_t prim = __get_PRIMASK();
__disable_irq();
copy = shared_value; // a few lines at most
__set_PRIMASK(prim); // restore the previous state
The reason to restore the previous state instead of always calling __enable_irq() is that this code might be called from a place where interrupts are already off, and it must not turn them on by itself.
Problems that come up in practice
- Number of priority bits: many MCUs implement only the upper 4 of 8 bits. When writing the register directly you must align the value;
NVIC_SetPriorityhandles it for you. - Together with an RTOS: an interrupt that calls RTOS APIs must have a lower priority (a larger number) than the value the RTOS sets. Break this and you get a bug that crashes rarely.
- Long work inside a handler: all lower-priority interrupts are delayed meanwhile, and UART bytes get lost.
- Missing
volatileon a shared variable: the symptom appears only in optimised builds.