Classic MCUs jump to the vector and leave everything else to software — you write a PUSH/POP prologue. The NVIC does this in hardware and adds tail-chaining / late-arrival on top.
A GIC is a separate IP block with its own memory-mapped registers sitting across a bus. Exception entry costs ~hundreds of cycles because of the MMU & bus round-trip. NVIC is in the core and exception entry is 12 cycles on M3.
| # | Name | Priority | Purpose | Available on |
|---|---|---|---|---|
| 1 | Reset | -3 (fixed, highest) | Power-on, watchdog, SW reset via AIRCR.SYSRESETREQ | All |
| 2 | NMI | -2 (fixed) | Non-maskable — clock failure, tamper, SoC-routed critical input | All |
| 3 | HardFault | -1 (fixed) | Last-resort fault. Escalation target when another fault is disabled or nested. | All |
| 4 | MemManage | prog | MPU violation, XN (execute-never) violation | v7-M, v8-M Main |
| 5 | BusFault | prog | Bus error (abort from memory system) | v7-M, v8-M Main |
| 6 | UsageFault | prog | Undefined instr, unaligned, /0, stack limit hit, invalid EXC_RETURN | v7-M, v8-M Main |
| 7 | SecureFault | prog | TrustZone violation (NS access to S resource etc.) | v8-M only, Secure world |
| 11 | SVCall | prog | Supervisor call — SVC #imm. RTOS syscall trampoline. | All |
| 12 | DebugMonitor | prog | Debug events when halting debug is disabled | v7-M, v8-M |
| 14 | PendSV | prog | Software-pended exception — the classic RTOS context-switch vector | All |
| 15 | SysTick | prog | 24-bit down-counter interrupt — the standard OS tick | Mostly all (optional on M1) |
Offset Word
0x0000 Initial MSP (loaded into SP at reset)
0x0004 Reset_Handler
0x0008 NMI_Handler
0x000C HardFault_Handler
0x0010 MemManage_Handler
0x0014 BusFault_Handler
0x0018 UsageFault_Handler
0x001C SecureFault_Handler (v8-M)
0x0020 (reserved)
0x0024 (reserved)
0x0028 (reserved)
0x002C SVC_Handler
0x0030 DebugMon_Handler
0x0034 (reserved)
0x0038 PendSV_Handler
0x003C SysTick_Handler
0x0040 IRQ0_Handler ; vector 16
0x0044 IRQ1_Handler
... ...
SCB->VTOR, at 0xE000_ED08).(num_vectors × 4) — typically 128 or 256 bytes minimum, larger for big tables.VTOR_S (privileged-Secure) and VTOR_NS (Non-Secure).__DSB();__ISB(); after updating VTOR → instructions in flight may still use the old table.
/* Classic CMSIS-style vector table for STM32F4 */
extern uint32_t _estack; /* provided by linker */
void Reset_Handler(void);
void Default_Handler(void);
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));
void EXTI0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
/* … one per peripheral … */
__attribute__((section(".isr_vector"), used))
const void (* const g_vector_table[])(void) = {
(void (*)(void)) &_estack, /* 0x000 initial MSP */
Reset_Handler, /* 0x004 */
NMI_Handler, /* 0x008 */
HardFault_Handler, /* 0x00C */
/* system vectors … */
SysTick_Handler, /* 0x03C */
EXTI0_IRQHandler, /* 0x040 — IRQ0 */
/* peripherals … */
};
weak / alias pattern lets unused handlers collapse to Default_Handler, but a user who defines EXTI0_IRQHandler shadows the weak symbol without any registration API. Clean, zero-overhead.
| Core | Priority bits | Levels |
|---|---|---|
| M0, M0+, M23 | 2 | 4 |
| M1 | 1 or 2 | 2 – 4 |
| M3, M4, M7 | 3–8 (impl. choice) | 8 – 256 |
| M33, M55, M85 | 3–8 (impl. choice) | 8 – 256 |
Most ST/NXP STM32/LPC parts implement 4 bits → 16 levels. Nordic nRF52 implements 3 bits → 8 levels.
0x01 on a 4-bit-impl MCU evaluates to priority 0 (identical to 0x0F's visible bits masked). Always set priorities via NVIC_SetPriority(), which handles the shift.Each priority field is split into preempt priority (upper) and sub-priority (lower) by AIRCR.PRIGROUP.
Preempt = can I interrupt another ISR?
Sub = who runs first if multiple are simultaneously pending at the same preempt level?
configPRIO_BITS is the implemented priority-bit count; FreeRTOS demands all preempt (PRIGROUP = 0). Sub-priority complicates reasoning — pick one.xQueueSendFromISR), priority must be numerically ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY.BX lr with EXC_RETURN pattern triggers unstack.Two 12-cycle stacks + two 12-cycle unstacks = 48 cycles of pure overhead.
Frame stays on stack; only 6 cycles to fetch new vector & enter ISR2. Saves ~20 cycles per chained pair.
A low-priority IRQ fires → CPU starts stacking. Midway through, a higher-priority IRQ fires.
FPCCR.LSPACT = 1./* Enable lazy stacking (default on M4F with FPU on) */
#define FPCCR (*(volatile uint32_t*)0xE000EF34)
#define FPCCR_ASPEN (1U << 31)
#define FPCCR_LSPEN (1U << 30)
/* Most startup code sets both:
ASPEN = automatic FP state preservation on exception
LSPEN = lazy — defer until the handler uses FPU */
FPCCR |= FPCCR_ASPEN | FPCCR_LSPEN;
Needed because without the WIC, the NVIC cannot sense the IRQ line while its clock is off. With the WIC, sub-µA deep sleep with IRQ wake is possible.
SVC #imm instruction in user code.SVC from Non-Secure traps into NS handler; S-side has its own.SCB->ICSR.PENDSVSET.SYST_CSR — enable, tickint, clksource, countflag.SYST_RVR — reload value.SYST_CVR — current.SYST_CALIB — implementation-provided 10 ms calibration constant./* 1 kHz tick on a 168 MHz M4 */
#define SYST_CSR (*(volatile uint32_t*)0xE000E010)
#define SYST_RVR (*(volatile uint32_t*)0xE000E014)
#define SYST_CVR (*(volatile uint32_t*)0xE000E018)
void systick_init(uint32_t core_hz, uint32_t tick_hz)
{
SYST_RVR = core_hz / tick_hz - 1; /* 167 999 */
SYST_CVR = 0; /* clear */
SYST_CSR = (1U<<2) | /* CLKSOURCE = core */
(1U<<1) | /* TICKINT */
(1U<<0); /* ENABLE */
}
A single 24-bit counter means max interval at 168 MHz ≈ 99.9 ms. Tickless RTOS designs pair SysTick with the RTC for longer sleeps.
SCB->SHCSR.{MEMFAULTENA, BUSFAULTENA, USGFAULTENA} at boot so specific faults have distinct handlers. Otherwise every fault looks like HardFault, which is harder to debug.
SCB->HFSR — hard-fault status. FORCED=1 means an escalated configurable fault.SCB->CFSR — configurable fault status register. Three bytes: MMFSR / BFSR / UFSR.SCB->MMFAR — fault address if MMARVALID.SCB->BFAR — bus-fault address if BFARVALID.__attribute__((naked))
void HardFault_Handler(void)
{
__asm volatile (
"TST lr, #4 \n"
"ITE EQ \n"
"MRSEQ r0, MSP \n"
"MRSNE r0, PSP \n"
"B hard_fault_c \n");
}
void hard_fault_c(uint32_t *sf)
{
uint32_t pc = sf[6];
uint32_t lr = sf[5];
uint32_t psr = sf[7];
uint32_t cfsr = SCB->CFSR;
/* write to RTT / flash / UART, then NVIC_SystemReset */
}
Pattern: find the stack used at fault (MSP/PSP via bit 2 of LR/EXC_RETURN), grab stacked PC + CFSR, log it, reset.
| Symptom | CFSR bit | Typical cause |
|---|---|---|
| UsageFault — UNDEFINSTR | UFSR.UNDEFINSTR | Jumped into data; bit-rot flash; wrong Thumb bit in function pointer. |
| UsageFault — INVPC | UFSR.INVPC | EXC_RETURN corrupted (stack smash, bad handler asm). |
| UsageFault — UNALIGNED | UFSR.UNALIGNED | CCR.UNALIGN_TRP=1; unaligned word/halfword on v6-M or to Device memory. |
| UsageFault — DIVBYZERO | UFSR.DIVBYZERO | CCR.DIV_0_TRP=1; signed/unsigned divide by 0. |
| BusFault — PRECISERR | BFSR.PRECISERR | Illegal peripheral address; access to region with no mapped slave. |
| BusFault — IMPRECISERR | BFSR.IMPRECISERR | Write-buffered error — fault address is not in BFAR; force DSB after writes or disable write buffer while debugging. |
| MemManage — DACCVIOL / IACCVIOL | MMFSR.DACC/IACC | MPU denied the access — check permissions & region config. |
| Stack-limit hit | UFSR.STKOF (v8-M) | SP went below PSPLIM/MSPLIM — add overflow check via CMSIS. |
/* Enable a peripheral IRQ at priority 0x50 */
NVIC_SetPriority(EXTI0_IRQn, 0x50);
NVIC_EnableIRQ(EXTI0_IRQn);
/* Pend in software */
NVIC_SetPendingIRQ(EXTI0_IRQn);
/* Get active ISR# (nonzero in Handler mode) */
uint32_t vect = __get_IPSR();
/* Atomic critical section for BASEPRI-filterable IRQs */
uint32_t prev = __get_BASEPRI();
__set_BASEPRI(0x20);
/* … */
__set_BASEPRI(prev);
NVIC_EnableIRQ(n) → sets bit n%32 in NVIC->ISER[n/32].NVIC_SetPriority(n, p) → writes the upper __NVIC_PRIO_BITS bits of p to NVIC->IPR[n].SCB->SHPR[1..3] instead of IPR.ISR does the whole job — short, deterministic, no RTOS calls. Fits GPIO debouncing, simple UART byte echo, timer tick that just sets a flag.
ISR captures hardware state into a lock-free queue / ring buffer, then returns fast. Worker thread or deferred procedure handles the slow part. Use xQueueSendFromISR/xTaskNotifyFromISR with the pxHigherPriorityTaskWoken pattern.
Two priority bands — hard RT IRQs above configMAX_SYSCALL_INTERRUPT_PRIORITY must not touch the RTOS; soft RT IRQs below it may signal tasks.
The exact numerical threshold is configured per port. With 4 impl. bits on STM32, a typical split is kernel-safe above 0x50 and PendSV/SysTick at 0xF0.
FreeRTOS taskENTER_CRITICAL() sets BASEPRI — not PRIMASK — so high-priority "above-kernel" IRQs can still fire. Calling __disable_irq() in user code defeats this.
Changing priority of an ISR that is already pending has implementation-defined timing. Always disable IRQ, clear pending, set priority, then enable.
Silent corruption of kernel state; often manifests as a random HardFault hours later. Assert priority in the ISR.
NVIC pending bit clears on entry, but the peripheral flag still asserts → re-entry forever. Always acknowledge in the peripheral's status register.
Fault arrives cycles after the offending store — PC in the stacked frame is not where the bug is. DSB after suspect writes, or disable write-buffer via ACTLR.DISDEFWBUF during debug.
Partial frame + float op = spectacular crash. Mark fault handlers __attribute__((target("no-float"))) or disable FPU.
VTOR_S (Secure) and VTOR_NS (Non-Secure). Every exception is either S or NS depending on target state.AIRCR.BFHFNMINS).
Arm — Armv7-M / Armv8-M Architecture Reference Manual — section B1 (Exception model)
Arm — Cortex-M3 / M4 / M7 Devices Generic User Guide (DUI0553, DUI0552, DUI0646)
Joseph Yiu — Definitive Guide to Cortex-M3/M4 — chapters 7–9
Joseph Yiu — Cortex-M23/M33 Definitive Guide — chapters 10–12 for TrustZone exception entry
FreeRTOS — Cortex-M interrupt priority behaviour (freertos.org → "Cortex-M interrupt priorities")
ARM Community blog — "Cutting through the confusion with Cortex-M interrupt priorities" by Joseph Yiu
Segger — "Cortex-M Fault Analysis" application note
Presentation built with Reveal.js 4.6 · Playfair Display + DM Sans + JetBrains Mono
Educational use.