Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
NVIC — Nested Vector Interrupt Controller
Master ARM Cortex-M4 interrupt architecture: enable/disable registers, priority configuration, preemption, nesting, and writing robust ISRs without the HAL library.
What is NVIC and Why Do You Need It?
In a sequential bare-metal program, your CPU works through instructions one by one. But hardware events don’t cooperate with your loop timing — a UART byte arrives mid-calculation, a timer fires while you’re reading a sensor, a button is pressed between two function calls. Without interrupts, you’d poll every peripheral constantly, burning CPU cycles checking flags that are almost always clear.
The NVIC (Nested Vector Interrupt Controller) is the hardware block inside every ARM Cortex-M4 core that solves this. When a peripheral signals an event, the NVIC can suspend the CPU’s current work, transparently save all working registers onto the stack, execute the Interrupt Service Routine (ISR), restore the saved state, and resume exactly where it left off — all in hardware, in about 12 clock cycles.
NVIC Key Facts for STM32F407
| Feature | Value | Notes |
|---|---|---|
| Interrupt lines supported | 240 external IRQs | STM32F407 uses ~91 |
| Exception lines | 16 system | Reset, NMI, HardFault, SysTick, etc. |
| Priority register bits | 8 bits per IRQ | Only top 4 bits used = 16 levels |
| Lowest priority number | 0 | Highest priority (runs over everything) |
| Highest priority number | 15 | Lowest priority |
| Nesting support | Yes | Higher-priority ISR preempts lower |
| Tail-chaining | Yes | Back-to-back ISRs without full save/restore |
| Late arrival | Yes | Higher-priority IRQ during exception entry takes over |
| Vector table location | 0x08000000 | Configurable via SCB_VTOR register |
Interrupt Lifecycle
Event
Set
& Priority OK?
8 Registers
Executes
in ISR
Restores
The CPU saves: R0,R1,R2,R3,R12,LR,PC,xPSR automatically on interrupt entry (hardware stacking)
Interrupt Vector Table
The vector table is an array of 32-bit addresses stored at the start of Flash (0x08000000). Entry 0 is the initial stack pointer value. Entry 1 is the Reset_Handler address. After 16 system exception slots, peripheral IRQ handlers begin at offset 0x40 (IRQ0 = WWDG).
When an interrupt fires, the CPU reads the address at VectorTable_Base + (IRQ_number + 16) * 4 and jumps there. This is purely hardware — no software lookup needed.
/* startup_stm32f407xx.s — vector table excerpt */
g_pfnVectors:
.word _estack /* 0x00: Initial SP */
.word Reset_Handler /* 0x04: IRQ -15 */
.word NMI_Handler /* 0x08: IRQ -14 */
.word HardFault_Handler /* 0x0C: IRQ -13 */
.word MemManage_Handler /* 0x10: IRQ -12 */
.word BusFault_Handler /* 0x14: IRQ -11 */
.word UsageFault_Handler /* 0x18: IRQ -10 */
.word 0 /* 0x1C-0x28: Reserved */
.word SVC_Handler /* 0x2C: IRQ -5 */
.word DebugMon_Handler /* 0x30: IRQ -4 */
.word 0 /* 0x34: Reserved */
.word PendSV_Handler /* 0x38: IRQ -2 */
.word SysTick_Handler /* 0x3C: IRQ -1 */
/* Peripheral IRQs start at 0x40 */
.word WWDG_IRQHandler /* IRQ 0 */
.word PVD_IRQHandler /* IRQ 1 */
.word TAMP_STAMP_IRQHandler /* IRQ 2 */
.word RTC_WKUP_IRQHandler /* IRQ 3 */
.word FLASH_IRQHandler /* IRQ 4 */
.word RCC_IRQHandler /* IRQ 5 */
.word EXTI0_IRQHandler /* IRQ 6 */
.word EXTI1_IRQHandler /* IRQ 7 */
.word EXTI2_IRQHandler /* IRQ 8 */
.word EXTI3_IRQHandler /* IRQ 9 */
.word EXTI4_IRQHandler /* IRQ 10 */
/* ... more handlers ... */
.word TIM2_IRQHandler /* IRQ 28 */
.word TIM3_IRQHandler /* IRQ 29 */
.word TIM4_IRQHandler /* IRQ 30 */
.word I2C1_EV_IRQHandler /* IRQ 31 */
.word I2C1_ER_IRQHandler /* IRQ 32 */
.word SPI1_IRQHandler /* IRQ 35 */
.word USART1_IRQHandler /* IRQ 37 */
.word USART2_IRQHandler /* IRQ 38 */
.word USART3_IRQHandler /* IRQ 39 */
| IRQ# | Handler Name | Peripheral | ISER Reg/Bit |
|---|---|---|---|
| 0 | WWDG_IRQHandler | Window Watchdog | ISER[0] bit 0 |
| 5 | RCC_IRQHandler | RCC Clock ready flags | ISER[0] bit 5 |
| 6 | EXTI0_IRQHandler | EXTI Line 0 (PA0/PB0…) | ISER[0] bit 6 |
| 10 | EXTI4_IRQHandler | EXTI Line 4 | ISER[0] bit 10 |
| 11-17 | DMA1_Stream0-6 | DMA1 streams | ISER[0] bits 11-17 |
| 18 | ADC_IRQHandler | ADC1/2/3 global | ISER[0] bit 18 |
| 23 | EXTI9_5_IRQHandler | EXTI Lines 5-9 shared | ISER[0] bit 23 |
| 28 | TIM2_IRQHandler | General purpose timer 2 | ISER[0] bit 28 |
| 31 | I2C1_EV_IRQHandler | I2C1 event | ISER[0] bit 31 |
| 32 | I2C1_ER_IRQHandler | I2C1 error | ISER[1] bit 0 |
| 35 | SPI1_IRQHandler | SPI1 | ISER[1] bit 3 |
| 37 | USART1_IRQHandler | USART1 | ISER[1] bit 5 |
| 38 | USART2_IRQHandler | USART2 | ISER[1] bit 6 |
| 40 | EXTI15_10_IRQHandler | EXTI Lines 10-15 shared | ISER[1] bit 8 |
| 56 | CAN1_RX0_IRQHandler | CAN1 RX FIFO 0 | ISER[1] bit 24 |
| 70 | DMA2_Stream0_IRQHandler | DMA2 stream 0 | ISER[2] bit 6 |
NVIC Register Block — Base: 0xE000E100
All NVIC registers live in the Cortex-M System Control Space (SCS). These are core registers, identical on every Cortex-M4 chip regardless of manufacturer. Each register set has 8 x 32-bit registers, giving 256 bits total for up to 240 IRQs (with 16 bits spare).
NVIC_ISER — Interrupt Set Enable Registers
Writing 1 to a bit enables that IRQ. Writing 0 has absolutely no effect — this is intentional. The write-1-to-set design means you can enable one interrupt without accidentally disabling another (no read-modify-write needed, no race condition).
Bit and field descriptions
| Bit / Field | Description |
|---|---|
| 31 I2C1_EV | IRQ 31 — I2C1_EV*(volatile uint32_t*)0xE000E100 |= (1U << 31); |
| 30 TIM4 | IRQ 30 — TIM4*(volatile uint32_t*)0xE000E100 |= (1U << 30); |
| 29 TIM3 | IRQ 29 — TIM3*(volatile uint32_t*)0xE000E100 |= (1U << 29); |
| 28 TIM2 | IRQ 28 — TIM2*(volatile uint32_t*)0xE000E100 |= (1U << 28); |
| 27 TIM1_CC | IRQ 27 — TIM1_CC*(volatile uint32_t*)0xE000E100 |= (1U << 27); |
| 26 TIM1_TRG | IRQ 26 — TIM1_TRG*(volatile uint32_t*)0xE000E100 |= (1U << 26); |
| 25 TIM1_UP | IRQ 25 — TIM1_UP*(volatile uint32_t*)0xE000E100 |= (1U << 25); |
| 24 TIM1_BRK | IRQ 24 — TIM1_BRK*(volatile uint32_t*)0xE000E100 |= (1U << 24); |
| 23 EXTI9_5 | IRQ 23 — EXTI9_5*(volatile uint32_t*)0xE000E100 |= (1U << 23); |
| 22 CAN1_SCE | IRQ 22 — CAN1_SCE*(volatile uint32_t*)0xE000E100 |= (1U << 22); |
| 21 CAN1_RX1 | IRQ 21 — CAN1_RX1*(volatile uint32_t*)0xE000E100 |= (1U << 21); |
| 20 CAN1_RX0 | IRQ 20 — CAN1_RX0*(volatile uint32_t*)0xE000E100 |= (1U << 20); |
| 19 CAN1_TX | IRQ 19 — CAN1_TX*(volatile uint32_t*)0xE000E100 |= (1U << 19); |
| 18 ADC | IRQ 18 — ADC*(volatile uint32_t*)0xE000E100 |= (1U << 18); |
| 17 DMA1_S6 | IRQ 17 — DMA1_S6*(volatile uint32_t*)0xE000E100 |= (1U << 17); |
| 16 DMA1_S5 | IRQ 16 — DMA1_S5*(volatile uint32_t*)0xE000E100 |= (1U << 16); |
| 15 DMA1_S4 | IRQ 15 — DMA1_S4*(volatile uint32_t*)0xE000E100 |= (1U << 15); |
| 14 DMA1_S3 | IRQ 14 — DMA1_S3*(volatile uint32_t*)0xE000E100 |= (1U << 14); |
| 13 DMA1_S2 | IRQ 13 — DMA1_S2*(volatile uint32_t*)0xE000E100 |= (1U << 13); |
| 12 DMA1_S1 | IRQ 12 — DMA1_S1*(volatile uint32_t*)0xE000E100 |= (1U << 12); |
| 11 DMA1_S0 | IRQ 11 — DMA1_S0*(volatile uint32_t*)0xE000E100 |= (1U << 11); |
| 10 EXTI4 | IRQ 10 — EXTI4*(volatile uint32_t*)0xE000E100 |= (1U << 10); |
| 9 EXTI3 | IRQ 9 — EXTI3*(volatile uint32_t*)0xE000E100 |= (1U << 9); |
| 8 EXTI2 | IRQ 8 — EXTI2*(volatile uint32_t*)0xE000E100 |= (1U << 8); |
| 7 EXTI1 | IRQ 7 — EXTI1*(volatile uint32_t*)0xE000E100 |= (1U << 7); |
| 6 EXTI0 | IRQ 6 — EXTI0*(volatile uint32_t*)0xE000E100 |= (1U << 6); |
| 5 RCC | IRQ 5 — RCC*(volatile uint32_t*)0xE000E100 |= (1U << 5); |
| 4 FLASH | IRQ 4 — FLASH*(volatile uint32_t*)0xE000E100 |= (1U << 4); |
| 3 RTC_WKUP | IRQ 3 — RTC_WKUP*(volatile uint32_t*)0xE000E100 |= (1U << 3); |
| 2 TAMP_STAMP | IRQ 2 — TAMP_STAMP*(volatile uint32_t*)0xE000E100 |= (1U << 2); |
| 1 PVD | IRQ 1 — PVD*(volatile uint32_t*)0xE000E100 |= (1U << 1); |
| 0 WWDG | IRQ 0 — WWDG*(volatile uint32_t*)0xE000E100 |= (1U << 0); |
#define NVIC_BASE 0xE000E100UL
#define NVIC_ISER(n) (*((volatile uint32_t*)(NVIC_BASE + 0x000 + (n)*4)))
#define NVIC_ICER(n) (*((volatile uint32_t*)(NVIC_BASE + 0x080 + (n)*4)))
#define NVIC_ISPR(n) (*((volatile uint32_t*)(NVIC_BASE + 0x100 + (n)*4)))
#define NVIC_ICPR(n) (*((volatile uint32_t*)(NVIC_BASE + 0x180 + (n)*4)))
#define NVIC_IABR(n) (*((volatile uint32_t*)(NVIC_BASE + 0x200 + (n)*4)))
/* Priority registers: 8 bits per IRQ, 4 IRQs per 32-bit word */
#define NVIC_IPR(n) (*((volatile uint8_t*)(0xE000E400UL + (n))))
/* Generic enable/disable macros */
#define NVIC_EnableIRQ(irq) NVIC_ISER((irq)/32) = (1UL << ((irq)%32))
#define NVIC_DisableIRQ(irq) NVIC_ICER((irq)/32) = (1UL << ((irq)%32))
#define NVIC_SetPending(irq) NVIC_ISPR((irq)/32) = (1UL << ((irq)%32))
#define NVIC_ClearPending(irq) NVIC_ICPR((irq)/32) = (1UL << ((irq)%32))
#define NVIC_IsActive(irq) ((NVIC_IABR((irq)/32) >> ((irq)%32)) & 1UL)
/* Priority: write to upper nibble (bits 7:4), lower 4 bits ignored */
#define NVIC_SetPriority(irq, prio) NVIC_IPR(irq) = (uint8_t)((prio) << 4)
/* Usage example — enable USART1 (IRQ 37) at priority 3 */
NVIC_SetPriority(37, 3); /* Set priority FIRST */
NVIC_EnableIRQ(37); /* Then enable */
NVIC_ICER — Interrupt Clear Enable Registers
Write 1 to a bit to disable (mask) that IRQ. Again, write-1-to-clear — writing 0 has no effect. A disabled interrupt can still become pending (its flag is still set), but the CPU won’t act on it until re-enabled.
/* Disable USART1 IRQ during critical section */
NVIC_DisableIRQ(37);
/* ... critical section code ... */
NVIC_EnableIRQ(37);
/* The IRQ flag in USART1->SR is NOT cleared by disabling NVIC */
/* When you re-enable, any pending interrupt will fire immediately */
NVIC_ISPR — Interrupt Set Pending Registers
Writing 1 to a bit forces that IRQ into the pending state, as if the peripheral had triggered it. This is used for software-triggered interrupts — useful for testing ISRs, or deferring processing from a higher-priority ISR to a lower-priority one (the “bottom half” pattern).
/* Software-trigger an interrupt for testing */
NVIC_EnableIRQ(28); /* Enable TIM2 IRQ */
NVIC_SetPriority(28, 5);
NVIC_SetPending(28); /* Immediately triggers TIM2_IRQHandler */
/* Common pattern: high-priority ISR sets a lower-priority IRQ pending */
/* so heavy processing happens at lower priority, not in the fast ISR */
void USART1_IRQHandler(void) {
ring_buf_push(&rx_buf, USART1->DR); /* Fast: just buffer the byte */
NVIC_SetPending(PROCESS_DATA_IRQ); /* Defer heavy work */
}
NVIC_ICPR — Interrupt Clear Pending Registers
Writing 1 cancels the pending state of an IRQ. Note: if the interrupt source (the peripheral flag) is still set, the IRQ will immediately go pending again. You must always clear the peripheral’s own interrupt flag inside the ISR.
/* Clear a spurious pending IRQ before enabling */
NVIC_ClearPending(6); /* Clear EXTI0 pending before enabling */
NVIC_EnableIRQ(6); /* Now safe to enable */
/* Inside an ISR — always clear the SOURCE flag, not just NVIC pending */
void EXTI0_IRQHandler(void) {
/* Clear EXTI pending bit — without this, ISR fires again immediately */
EXTI->PR |= (1 << 0); /* EXTI_PR: write 1 to clear */
/* ... handle the interrupt ... */
}
NVIC_IABR — Interrupt Active Bit Registers (Read-Only)
A bit here is set while the corresponding ISR is actively executing, and cleared when it returns. These are read-only — you cannot write to them. They’re useful for debugging: if a bit stays set after an ISR should have returned, it means the ISR is stuck or was re-entered.
/* Check if USART1 ISR is currently executing (e.g., for nested check) */
if (NVIC_IsActive(37)) {
/* USART1_IRQHandler is currently on the stack */
}
/* Reading raw IABR register */
uint32_t active = NVIC_IABR(0); /* Active IRQs 0-31 */
if (active & (1 << 6)) {
/* EXTI0_IRQHandler is running */
}
Interrupt Priority System
The STM32F407’s Cortex-M4 implements 4 bits of priority (out of the maximum 8 that ARM allows). This gives 16 priority levels: 0 (highest) to 15 (lowest). These 4 bits are stored in the upper nibble of each byte in the NVIC_IPR registers.
How Priority Register Bytes Work
Each NVIC_IPR register is 32 bits, but accessed as 4 bytes. Each byte holds the priority for one IRQ. Only the top 4 bits (bits 7:4) are implemented — the bottom 4 bits (bits 3:0) are always 0 and writes are ignored.
/* NVIC_IPR register layout */
/* Bits 7:4 — Priority value (0-15) */
/* Bits 3:0 — Not implemented, reads as 0 */
/* To set priority 3 for IRQ 37 (USART1): */
/* The IPR byte for IRQ 37 is at address 0xE000E400 + 37 = 0xE000E425 */
*((volatile uint8_t*)0xE000E425) = (3 << 4); /* = 0x30 = 48 */
/* Using our macro: */
NVIC_SetPriority(37, 3); /* Writes 0x30 to IPR[37] */
/* Reading back — shift right by 4 to get the value */
uint8_t prio = NVIC_IPR(37) >> 4; /* Returns 3 */
Urgent
High
Normal
Normal
Idle
Priority Grouping (Preemption vs Sub-priority)
The 4-bit priority field can be split into two sub-fields: preemption priority (upper bits) and sub-priority (lower bits). This split is configured by the PRIGROUP field in the SCB_AIRCR register.
Preemption priority determines nesting: a higher preemption priority ISR can interrupt a running lower preemption priority ISR. Sub-priority only matters when two interrupts with the same preemption priority are pending simultaneously — the one with lower sub-priority number runs first, but neither can interrupt the other.
| PRIGROUP (AIRCR[10:8]) | Preemption bits | Sub-priority bits | Preemption levels | Sub-priority levels |
|---|---|---|---|---|
| 0b011 (3) — default | 4 bits [7:4] | 0 bits | 16 | 1 (none) |
| 0b100 (4) | 3 bits [7:5] | 1 bit [4] | 8 | 2 |
| 0b101 (5) | 2 bits [7:6] | 2 bits [5:4] | 4 | 4 |
| 0b110 (6) | 1 bit [7] | 3 bits [6:4] | 2 | 8 |
| 0b111 (7) | 0 bits | 4 bits [7:4] | 1 (no nesting) | 16 |
/* SCB_AIRCR register — Application Interrupt and Reset Control Register */
#define SCB_AIRCR (*((volatile uint32_t*)0xE000ED0C))
#define VECTKEY 0x05FA0000UL /* Must write this key to modify AIRCR */
/* Set PRIGROUP = 3 (all 4 bits = preemption, default) */
SCB_AIRCR = VECTKEY | (3 << 8);
/* Set PRIGROUP = 5 (2 preemption + 2 sub-priority bits) */
SCB_AIRCR = VECTKEY | (5 << 8);
/* With PRIGROUP=5, priority encoding: */
/* Preemption: 2 bits → values 0,1,2,3 */
/* Sub-prio: 2 bits → values 0,1,2,3 */
/* IPR byte = (preempt << 6) | (sub << 4) */
/* Priority 2-1 (preempt=2, sub=1): IPR = (2<<6)|(1<<4) = 0x90 */
NVIC_IPR(37) = (2 << 6) | (1 << 4);
Writing Interrupt Service Routines (ISRs)
In bare-metal C, an ISR is just a regular C function whose name exactly matches the handler name in the vector table. No special attributes are needed — the Cortex-M hardware handles saving and restoring registers automatically.
ISR Naming Convention
The function name must match exactly what’s in your startup file. Common mistakes are typos in names — if the name doesn’t match, the CPU will call the default Default_Handler (which is an infinite loop) instead.
/* ISR names MUST match the vector table exactly */
void USART1_IRQHandler(void); /* UART1 */
void TIM2_IRQHandler(void); /* Timer 2 */
void EXTI0_IRQHandler(void); /* External interrupt line 0 */
void DMA1_Stream0_IRQHandler(void); /* DMA1 stream 0 */
void SysTick_Handler(void); /* SysTick timer */
/* These are wrong — will silently use Default_Handler: */
void USART1_Handler(void) { } /* Missing _IRQ */
void usart1_irqhandler(void) { } /* Wrong case */
ISR Best Practices
- Keep ISRs short: Do the minimum — set a flag, read a register, write to a buffer. Heavy processing belongs in main loop or a lower-priority task.
- Always clear the interrupt source: Clear the peripheral’s status flag that triggered the interrupt. If you don’t, the ISR fires again immediately after returning.
- Use volatile for shared variables: Variables modified in ISRs and read in main loop MUST be declared volatile. Without it, the compiler may cache the value in a register and never re-read memory.
- Avoid blocking operations: Never call delay(), printf(), or functions that wait in a loop inside an ISR — you’ll block all interrupts at the same or lower priority.
- Avoid dynamic memory: Don’t call malloc() or free() in ISRs — they’re not reentrant and will corrupt the heap.
Complete Example: Timer Interrupt with LED Toggle
#include <stdint.h>
/* Peripheral addresses */
#define RCC_BASE 0x40023800UL
#define GPIOD_BASE 0x40020C00UL
#define TIM2_BASE 0x40000000UL
#define RCC_AHB1ENR (*((volatile uint32_t*)(RCC_BASE + 0x30)))
#define RCC_APB1ENR (*((volatile uint32_t*)(RCC_BASE + 0x40)))
#define GPIOD_MODER (*((volatile uint32_t*)(GPIOD_BASE + 0x00)))
#define GPIOD_ODR (*((volatile uint32_t*)(GPIOD_BASE + 0x14)))
#define TIM2_CR1 (*((volatile uint32_t*)(TIM2_BASE + 0x00)))
#define TIM2_PSC (*((volatile uint32_t*)(TIM2_BASE + 0x28)))
#define TIM2_ARR (*((volatile uint32_t*)(TIM2_BASE + 0x2C)))
#define TIM2_DIER (*((volatile uint32_t*)(TIM2_BASE + 0x0C)))
#define TIM2_SR (*((volatile uint32_t*)(TIM2_BASE + 0x10)))
#define NVIC_ISER(n) (*((volatile uint32_t*)(0xE000E100 + (n)*4)))
#define NVIC_IPR(n) (*((volatile uint8_t*)(0xE000E400 + (n))))
/* Shared variable — MUST be volatile (modified in ISR, read in main) */
volatile uint32_t tick_count = 0;
/* ISR — called by hardware every 1 second */
void TIM2_IRQHandler(void) {
/* STEP 1: Check which flag fired (TIM2 has multiple interrupt sources) */
if (TIM2_SR & (1 << 0)) { /* Bit 0 = Update Interrupt Flag (UIF) */
/* STEP 2: ALWAYS clear the flag first (write 0 to clear) */
TIM2_SR &= ~(1 << 0);
/* STEP 3: Do work — toggle LED on PD12 */
GPIOD_ODR ^= (1 << 12);
tick_count++;
}
}
int main(void) {
/* Enable clocks */
RCC_AHB1ENR |= (1 << 3); /* GPIOD clock */
RCC_APB1ENR |= (1 << 0); /* TIM2 clock */
/* Configure PD12 as output */
GPIOD_MODER &= ~(3 << 24);
GPIOD_MODER |= (1 << 24); /* PD12 = output */
/* Configure TIM2 for 1-second period */
/* APB1 = 42MHz, TIM2 clock = 84MHz (×2 multiplier) */
/* PSC=8399 → tick rate = 84MHz/(8399+1) = 10000 Hz */
/* ARR=9999 → overflow every 9999+1 = 10000 ticks = 1 second */
TIM2_PSC = 8399;
TIM2_ARR = 9999;
TIM2_DIER |= (1 << 0); /* Enable Update Interrupt (UIE) */
TIM2_CR1 |= (1 << 0); /* Start the timer (CEN=1) */
/* Configure NVIC for TIM2 (IRQ 28) */
NVIC_IPR(28) = (5 << 4); /* Priority 5 */
NVIC_ISER(0) = (1UL << 28); /* Enable IRQ 28 (28/32=0, 28%32=28) */
while (1) {
/* Main loop is free to do other work */
/* LED toggles happen automatically via interrupt */
uint32_t count = tick_count; /* Read once into local var */
(void)count;
}
}
Nested Interrupt Example
Cortex-M4 supports nesting automatically: if a higher-priority ISR fires while a lower-priority ISR is running, the CPU transparently saves another stack frame and jumps to the higher-priority handler. No special code is needed — it’s all hardware.
/* TIM2 ISR at priority 5 — can be interrupted by USART1 (priority 2) */
NVIC_IPR(28) = (5 << 4); /* TIM2 = priority 5 (lower urgency) */
NVIC_IPR(37) = (2 << 4); /* USART1 = priority 2 (higher urgency) */
/* Scenario: TIM2_IRQHandler is running... */
void TIM2_IRQHandler(void) {
TIM2_SR &= ~1;
/* CPU is here, toggling LED... */
GPIOD_ODR ^= (1 << 12);
/* SUDDENLY a UART byte arrives: */
/* Hardware checks: USART1 priority(2) < TIM2 priority(5)? */
/* YES (2 < 5 means higher priority) → USART1_IRQHandler preempts */
/* CPU saves current context on stack and jumps to USART1_IRQHandler */
/* After USART1_IRQHandler returns, CPU resumes here */
/* ... rest of TIM2 handler ... */
}
void USART1_IRQHandler(void) {
/* This ISR preempted TIM2_IRQHandler */
char byte = (char)(USART1->DR & 0xFF);
ring_buf_push(&rx_buf, byte);
/* Return: hardware restores context and resumes TIM2_IRQHandler */
}
/* NOTE: Two ISRs at the SAME preemption priority CANNOT nest. */
/* If USART1 and TIM2 both had priority 5, whichever started first */
/* runs to completion before the other gets to execute. */
Global Interrupt Enable/Disable
For truly atomic operations (critical sections where you need to guarantee no interrupt fires between two instructions), use the PRIMASK and BASEPRI special registers.
PRIMASK — Disable All Maskable Interrupts
/* Using GCC inline assembly */
static inline void __disable_irq(void) {
__asm volatile ("CPSID I" ::: "memory");
}
static inline void __enable_irq(void) {
__asm volatile ("CPSIE I" ::: "memory");
}
/* Save and restore pattern (allows nested critical sections) */
static inline uint32_t enter_critical(void) {
uint32_t primask;
__asm volatile ("MRS %0, PRIMASK" : "=r"(primask));
__asm volatile ("CPSID I" ::: "memory");
return primask; /* Return previous state */
}
static inline void exit_critical(uint32_t primask) {
__asm volatile ("MSR PRIMASK, %0" :: "r"(primask) : "memory");
}
/* Usage: */
uint32_t saved = enter_critical();
shared_counter++; /* Atomic — no interrupt can fire here */
exit_critical(saved); /* Restore previous interrupt state */
BASEPRI — Mask Below a Priority Threshold
More surgical than PRIMASK: only interrupts with priority >= BASEPRI are masked. Interrupts with lower numbers (higher priority) still fire. Set BASEPRI=0 to disable the masking.
/* Mask all interrupts at priority 4 or lower (4,5,...,15) */
/* Interrupts at priority 0,1,2,3 still work normally */
uint32_t mask = 4 << 4; /* Shift to bits 7:4 */
__asm volatile ("MSR BASEPRI, %0" :: "r"(mask) : "memory");
/* Re-enable all (BASEPRI=0 means no masking) */
uint32_t zero = 0;
__asm volatile ("MSR BASEPRI, %0" :: "r"(zero) : "memory");
Complete Multi-Interrupt Example
This example sets up three interrupts with different priorities and demonstrates preemption, showing the complete initialization sequence for a real embedded application.
/*
* Multi-interrupt demo: Button (EXTI0) + UART + SysTick
* Button ISR at priority 1 (highest) — instant response
* USART1 ISR at priority 3 — data buffering
* SysTick ISR at priority 8 — 1ms tick (lowest)
*/
#include <stdint.h>
/* Register definitions */
#define RCC_AHB1ENR (*((volatile uint32_t*)0x40023830))
#define RCC_APB1ENR (*((volatile uint32_t*)0x40023840))
#define RCC_APB2ENR (*((volatile uint32_t*)0x40023844))
#define SYSCFG_EXTICR1 (*((volatile uint32_t*)0x40013808))
#define EXTI_IMR (*((volatile uint32_t*)0x40013C00))
#define EXTI_RTSR (*((volatile uint32_t*)0x40013C08))
#define EXTI_PR (*((volatile uint32_t*)0x40013C14))
#define GPIOA_MODER (*((volatile uint32_t*)0x40020000))
#define GPIOA_PUPDR (*((volatile uint32_t*)0x4002000C))
#define USART1_SR (*((volatile uint32_t*)0x40011000))
#define USART1_DR (*((volatile uint32_t*)0x40011004))
#define USART1_CR1 (*((volatile uint32_t*)0x4001100C))
#define SYST_CSR (*((volatile uint32_t*)0xE000E010))
#define SYST_RVR (*((volatile uint32_t*)0xE000E014))
#define NVIC_ISER(n) (*((volatile uint32_t*)(0xE000E100+(n)*4)))
#define NVIC_IPR(n) (*((volatile uint8_t*)(0xE000E400+(n))))
#define SCB_SHPR3 (*((volatile uint32_t*)0xE000ED20))
volatile uint32_t ms_ticks = 0;
volatile uint8_t button_pressed = 0;
volatile uint8_t rx_buf[64];
volatile uint8_t rx_head = 0;
void SysTick_Handler(void) {
ms_ticks++; /* 1ms tick — priority 8, lowest urgency */
}
void EXTI0_IRQHandler(void) {
if (EXTI_PR & 1) { /* Line 0 pending? */
EXTI_PR = 1; /* Clear by writing 1 */
button_pressed = 1; /* Set flag — priority 1, preempts everything */
}
}
void USART1_IRQHandler(void) {
if (USART1_SR & (1 << 5)) { /* RXNE: receive data register not empty */
rx_buf[rx_head++ & 63] = (uint8_t)USART1_DR;
/* Reading DR clears RXNE automatically */
}
}
void setup_interrupts(void) {
/* --- SysTick at 1ms (assuming 16MHz HSI) --- */
SYST_RVR = 16000 - 1; /* 16MHz / 16000 = 1kHz = 1ms */
SYST_CSR = (1<<2) | (1<<1) | (1<<0); /* Use core clock, enable interrupt, start */
SCB_SHPR3 |= (8 << 4) << 24; /* SysTick priority = 8 (bits 31:24 of SHPR3) */
/* --- EXTI0 for button on PA0 --- */
RCC_AHB1ENR |= (1 << 0); /* GPIOA clock */
RCC_APB2ENR |= (1 << 14); /* SYSCFG clock */
GPIOA_MODER &= ~(3 << 0); /* PA0 = input */
GPIOA_PUPDR &= ~(3 << 0); /* No pull (board has external pull-down) */
SYSCFG_EXTICR1 &= ~(0xF << 0); /* EXTI0 mapped to PA0 */
EXTI_RTSR |= 1; /* Rising edge trigger */
EXTI_IMR |= 1; /* Unmask line 0 */
NVIC_IPR(6) = (1 << 4); /* EXTI0 priority = 1 */
NVIC_ISER(0) = (1UL << 6); /* Enable IRQ 6 */
/* --- USART1 receive interrupt --- */
USART1_CR1 |= (1 << 5); /* RXNEIE: enable receive interrupt */
NVIC_IPR(37) = (3 << 4); /* USART1 priority = 3 */
NVIC_ISER(1) = (1UL << 5); /* Enable IRQ 37 (ISER[1] bit 5) */
}
int main(void) {
setup_interrupts();
while (1) {
if (button_pressed) {
button_pressed = 0;
/* Handle button press in main context */
}
}
}
Fault Handlers — What to Do When Things Go Wrong
When your code tries to access an invalid memory address, executes an undefined instruction, or violates memory protection, the CPU triggers a fault exception. The default startup file leaves these as infinite loops — that’s fine for debugging but terrible for production.
/* Useful fault handler for debugging */
void HardFault_Handler(void) {
/* The stack frame saved by hardware contains: */
/* [SP+0]=R0, [SP+4]=R1, [SP+8]=R2, [SP+12]=R3 */
/* [SP+16]=R12, [SP+20]=LR, [SP+24]=PC, [SP+28]=xPSR */
/* PC at [SP+24] tells you WHERE the fault occurred */
volatile uint32_t *sp;
/* Get stack pointer depending on which stack was used */
__asm volatile (
"TST LR, #4\n" /* Check EXC_RETURN bit 2 */
"ITE EQ\n"
"MRSEQ %0, MSP\n" /* Main stack */
"MRSNE %0, PSP\n" /* Process stack */
: "=r"(sp)
);
uint32_t fault_pc = sp[6]; /* Instruction that caused fault */
uint32_t fault_lr = sp[5]; /* Return address before fault */
(void)fault_pc;
(void)fault_lr;
/* Break into debugger if connected, otherwise loop */
__asm volatile("BKPT #0");
while(1);
}
Summary
NVIC_ISER
0xE000E100. Write 1 to enable IRQ. Writing 0 has no effect. One bit per IRQ.
NVIC_ICER
0xE000E180. Write 1 to disable IRQ. Does not clear pending state.
NVIC_ISPR
0xE000E200. Write 1 to force-pend an IRQ. Use for software-triggered interrupts.
NVIC_ICPR
0xE000E280. Write 1 to cancel pending. Won’t help if peripheral flag is still set.
NVIC_IABR
0xE000E300. Read-only. Bit set while ISR is executing. Useful for debug.
NVIC_IPR
0xE000E400. 8-bit per IRQ, only top 4 bits used. 0=highest, 15=lowest priority.
Quick Setup Checklist
- Enable the peripheral clock (RCC_AHB1ENR / APB1ENR / APB2ENR)
- Configure the peripheral itself (set mode, enable its interrupt flag in its own register)
- For GPIO interrupts: configure SYSCFG_EXTICR, EXTI_IMR, EXTI_RTSR/FTSR
- Set priority:
NVIC_IPR(irq) = priority << 4 - Enable in NVIC:
NVIC_ISER(irq/32) = 1 << (irq%32) - Write ISR with exact name matching the vector table
- Clear the peripheral flag inside the ISR — always
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments