NVIC — Nested Vector Interrupt Controller-Embedded Systems Training in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Tutorial 07 — Interrupts

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.

⚡ Interrupt-driven Programming📋 5 Core Register Sets🔢 16 Priority Levels💡 Complete ISR Examples
SECTION 1

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

FeatureValueNotes
Interrupt lines supported240 external IRQsSTM32F407 uses ~91
Exception lines16 systemReset, NMI, HardFault, SysTick, etc.
Priority register bits8 bits per IRQOnly top 4 bits used = 16 levels
Lowest priority number0Highest priority (runs over everything)
Highest priority number15Lowest priority
Nesting supportYesHigher-priority ISR preempts lower
Tail-chainingYesBack-to-back ISRs without full save/restore
Late arrivalYesHigher-priority IRQ during exception entry takes over
Vector table location0x08000000Configurable via SCB_VTOR register
NVIC vs EXTI: These are different hardware blocks. EXTI detects pin-level signal changes and converts them into interrupt requests on specific lines. NVIC receives those interrupt requests and manages CPU context switching. A GPIO interrupt goes: Pin event → EXTI detects edge → NVIC receives IRQ → CPU jumps to ISR. Peripheral interrupts (UART RX full, SPI TX empty, Timer overflow) skip EXTI and go directly to NVIC.

Interrupt Lifecycle

Diagram: Interrupt Lifecycle
Peripheral
Event
→
IRQ Flag
Set
→
NVIC Enabled
& Priority OK?
→
CPU Saves
8 Registers
→
ISR
Executes
→
Clear Flag
in ISR
→
EXC_RETURN
Restores

The CPU saves: R0,R1,R2,R3,R12,LR,PC,xPSR automatically on interrupt entry (hardware stacking)

SECTION 2

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 NamePeripheralISER Reg/Bit
0WWDG_IRQHandlerWindow WatchdogISER[0] bit 0
5RCC_IRQHandlerRCC Clock ready flagsISER[0] bit 5
6EXTI0_IRQHandlerEXTI Line 0 (PA0/PB0…)ISER[0] bit 6
10EXTI4_IRQHandlerEXTI Line 4ISER[0] bit 10
11-17DMA1_Stream0-6DMA1 streamsISER[0] bits 11-17
18ADC_IRQHandlerADC1/2/3 globalISER[0] bit 18
23EXTI9_5_IRQHandlerEXTI Lines 5-9 sharedISER[0] bit 23
28TIM2_IRQHandlerGeneral purpose timer 2ISER[0] bit 28
31I2C1_EV_IRQHandlerI2C1 eventISER[0] bit 31
32I2C1_ER_IRQHandlerI2C1 errorISER[1] bit 0
35SPI1_IRQHandlerSPI1ISER[1] bit 3
37USART1_IRQHandlerUSART1ISER[1] bit 5
38USART2_IRQHandlerUSART2ISER[1] bit 6
40EXTI15_10_IRQHandlerEXTI Lines 10-15 sharedISER[1] bit 8
56CAN1_RX0_IRQHandlerCAN1 RX FIFO 0ISER[1] bit 24
70DMA2_Stream0_IRQHandlerDMA2 stream 0ISER[2] bit 6
SECTION 3

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).

Bit formula: For IRQ number n — register index = n / 32, bit position = n % 32. So IRQ 37: register[1], bit 5. IRQ 6: register[0], bit 6.

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).

NVIC_ISER0 — 0xE000E100 — IRQ0 to IRQ31
31I2C1_EV
30TIM4
29TIM3
28TIM2
27TIM1_CC
26TIM1_TRG
25TIM1_UP
24TIM1_BRK
23EXTI9_5
22CAN1_SCE
21CAN1_RX1
20CAN1_RX0
19CAN1_TX
18ADC
17DMA1_S6
16DMA1_S5
15DMA1_S4
14DMA1_S3
13DMA1_S2
12DMA1_S1
11DMA1_S0
10EXTI4
9EXTI3
8EXTI2
7EXTI1
6EXTI0
5RCC
4FLASH
3RTC_WKUP
2TAMP_STAMP
1PVD
0WWDG
Bit and field descriptions
Bit / FieldDescription
31 I2C1_EVIRQ 31 — I2C1_EV
*(volatile uint32_t*)0xE000E100 |= (1U << 31);
30 TIM4IRQ 30 — TIM4
*(volatile uint32_t*)0xE000E100 |= (1U << 30);
29 TIM3IRQ 29 — TIM3
*(volatile uint32_t*)0xE000E100 |= (1U << 29);
28 TIM2IRQ 28 — TIM2
*(volatile uint32_t*)0xE000E100 |= (1U << 28);
27 TIM1_CCIRQ 27 — TIM1_CC
*(volatile uint32_t*)0xE000E100 |= (1U << 27);
26 TIM1_TRGIRQ 26 — TIM1_TRG
*(volatile uint32_t*)0xE000E100 |= (1U << 26);
25 TIM1_UPIRQ 25 — TIM1_UP
*(volatile uint32_t*)0xE000E100 |= (1U << 25);
24 TIM1_BRKIRQ 24 — TIM1_BRK
*(volatile uint32_t*)0xE000E100 |= (1U << 24);
23 EXTI9_5IRQ 23 — EXTI9_5
*(volatile uint32_t*)0xE000E100 |= (1U << 23);
22 CAN1_SCEIRQ 22 — CAN1_SCE
*(volatile uint32_t*)0xE000E100 |= (1U << 22);
21 CAN1_RX1IRQ 21 — CAN1_RX1
*(volatile uint32_t*)0xE000E100 |= (1U << 21);
20 CAN1_RX0IRQ 20 — CAN1_RX0
*(volatile uint32_t*)0xE000E100 |= (1U << 20);
19 CAN1_TXIRQ 19 — CAN1_TX
*(volatile uint32_t*)0xE000E100 |= (1U << 19);
18 ADCIRQ 18 — ADC
*(volatile uint32_t*)0xE000E100 |= (1U << 18);
17 DMA1_S6IRQ 17 — DMA1_S6
*(volatile uint32_t*)0xE000E100 |= (1U << 17);
16 DMA1_S5IRQ 16 — DMA1_S5
*(volatile uint32_t*)0xE000E100 |= (1U << 16);
15 DMA1_S4IRQ 15 — DMA1_S4
*(volatile uint32_t*)0xE000E100 |= (1U << 15);
14 DMA1_S3IRQ 14 — DMA1_S3
*(volatile uint32_t*)0xE000E100 |= (1U << 14);
13 DMA1_S2IRQ 13 — DMA1_S2
*(volatile uint32_t*)0xE000E100 |= (1U << 13);
12 DMA1_S1IRQ 12 — DMA1_S1
*(volatile uint32_t*)0xE000E100 |= (1U << 12);
11 DMA1_S0IRQ 11 — DMA1_S0
*(volatile uint32_t*)0xE000E100 |= (1U << 11);
10 EXTI4IRQ 10 — EXTI4
*(volatile uint32_t*)0xE000E100 |= (1U << 10);
9 EXTI3IRQ 9 — EXTI3
*(volatile uint32_t*)0xE000E100 |= (1U << 9);
8 EXTI2IRQ 8 — EXTI2
*(volatile uint32_t*)0xE000E100 |= (1U << 8);
7 EXTI1IRQ 7 — EXTI1
*(volatile uint32_t*)0xE000E100 |= (1U << 7);
6 EXTI0IRQ 6 — EXTI0
*(volatile uint32_t*)0xE000E100 |= (1U << 6);
5 RCCIRQ 5 — RCC
*(volatile uint32_t*)0xE000E100 |= (1U << 5);
4 FLASHIRQ 4 — FLASH
*(volatile uint32_t*)0xE000E100 |= (1U << 4);
3 RTC_WKUPIRQ 3 — RTC_WKUP
*(volatile uint32_t*)0xE000E100 |= (1U << 3);
2 TAMP_STAMPIRQ 2 — TAMP_STAMP
*(volatile uint32_t*)0xE000E100 |= (1U << 2);
1 PVDIRQ 1 — PVD
*(volatile uint32_t*)0xE000E100 |= (1U << 1);
0 WWDGIRQ 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 */
Critical sections: For a true critical section (prevent ALL interrupts), use the __disable_irq() instruction which sets the PRIMASK register, not NVIC_ICER. NVIC_ICER only masks specific IRQs.

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 */
}
SECTION 4

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.

Priority is inverted from what you might expect: Priority 0 is the HIGHEST (most urgent, can preempt everything). Priority 15 is the LOWEST (gets preempted by everything). Think of it like military rank — lower number = higher command.

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 */
Diagram: How Priority Register Bytes Work
0
HIGHEST
Urgent
1
Very
High
2
High
3
Above
Normal
4
Normal
5
Below
Normal
14
Low
15
LOWEST
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 bitsSub-priority bitsPreemption levelsSub-priority levels
0b011 (3) — default4 bits [7:4]0 bits161 (none)
0b100 (4)3 bits [7:5]1 bit [4]82
0b101 (5)2 bits [7:6]2 bits [5:4]44
0b110 (6)1 bit [7]3 bits [6:4]28
0b111 (7)0 bits4 bits [7:4]1 (no nesting)16
Recommendation: For most applications, use the default PRIGROUP=3 (all 4 bits = preemption, no sub-priority). This gives 16 nesting levels and is the simplest model. Only change PRIGROUP if you have a specific reason, as it can make priority calculations confusing.
/* 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);
SECTION 5

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. */
SECTION 6

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");
SECTION 7

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 */
        }
    }
}
SECTION 8: FAULT HANDLERS

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);
}
Debugging tip: When you hit a HardFault, connect a debugger and set a breakpoint in HardFault_Handler. Look at the saved PC value — it will point to the exact instruction that caused the fault. Common causes: NULL pointer dereference, stack overflow, unaligned access.
SUMMARY

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

  1. Enable the peripheral clock (RCC_AHB1ENR / APB1ENR / APB2ENR)
  2. Configure the peripheral itself (set mode, enable its interrupt flag in its own register)
  3. For GPIO interrupts: configure SYSCFG_EXTICR, EXTI_IMR, EXTI_RTSR/FTSR
  4. Set priority: NVIC_IPR(irq) = priority << 4
  5. Enable in NVIC: NVIC_ISER(irq/32) = 1 << (irq%32)
  6. Write ISR with exact name matching the vector table
  7. 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

Leave a Reply

Your email address will not be published. Required fields are marked *