The puzzle
When a timer interrupt fires, the core has a few cycles to find the right handler among dozens, without running any code to look it up. It does it with one multiplication and one memory read. What is the table it reads, and what goes wrong when one word of it is wrong?
STEP 1
The table, word by word
The vector table is an array of 32-bit words. Word n belongs to exception number n; word 0 is the exception to that rule: it holds the initial stack pointer. The core finds an entry with
On Armv6-M, numbers 1–15 are the architecture’s own exceptions and from 16 up each word belongs to a peripheral interrupt, IRQ n − 16. The numbering is the same on every vendor’s chip; only the meaning of each IRQ line is chip-specific.
↑ This step uses the figure at the top of the page.
| n | exception | can be disabled? |
|---|---|---|
| 0 | (initial SP, not an exception) | — |
| 1 | Reset | no |
| 2 | NMI | no |
| 3 | HardFault | no |
| 4–10 | reserved on Armv6-M | — |
| 11 | SVCall | — (raised by the svc instruction) |
| 12–13 | reserved on Armv6-M | — |
| 14 | PendSV | — (raised by software) |
| 15 | SysTick | yes |
| 16 + k | IRQ k | yes, in the NVIC |
Armv7-M and later cores fill some of the reserved slots with extra fault exceptions; the formula stays the same.
STEP 2
Every handler address is odd
Cortex-M cores execute only the Thumb instruction set, and the core takes bit 0 of every vector as the Thumb state bit. A handler at 0x1000_0310 is therefore stored as 0x1000_0311. The linker does this for you when the symbol is a Thumb function, which is why tables are written as arrays of function names rather than numbers.
At reset the core reads word 0 of the vector table into SP and word 1 into PC, taking bit 0 of word 1 as the Thumb state bit. Cortex-M cores execute only Thumb code, so an entry with bit 0 clear makes the very first instruction fault. The values are illustrative for the RP2040 application table at 0x1000_0100, where boot2 performs the same two loads in software (lesson 2).
A reset vector with bit 0 clear asks for a state the core does not have, and the very first instruction faults: HardFault before a single line of your code has run. A hand-typed address or a data symbol placed in the table by mistake produces exactly this.
STEP 3
Filling the gaps: weak default handlers
A real table has dozens of entries and a program handles a few. Start-up files therefore declare every handler as a weak symbol pointing at one default. In Arm’s CMSIS start-up file each is __attribute__((weak, alias("Default_Handler"))), an infinite loop; the pico-sdk’s defaults execute bkpt #0, which stops in the debugger if one is attached; without one, the breakpoint escalates to HardFault, whose default handler is also bkpt #0, and the core locks up. Defining a function with the exact name (SysTick_Handler, isr_systick) overrides the weak one at link time (unit 4, lesson 2).
This makes a classic bug easy to diagnose. A program that enables an interrupt but misspells its handler, Systick_Handler instead of SysTick_Handler, links without error; when the interrupt fires, the core lands in the default loop. A debugger stopped in Default_Handler means “an exception without a handler of its own”: an enabled interrupt, or a fault if, as in ST’s template, HardFault is aliased to the default too. The active exception number (IPSR, the low bits of xPSR) says which; 3 is HardFault.
STEP 4
Moving the table, and the alignment rule
Where VTOR exists, it can point the core at another table. Boot ROMs use it to hand over (lesson 2); firmware uses it to put the table in RAM, so handlers can be installed at run time and, on some chips, so the vector fetch avoids slow flash. The pico-sdk’s runtime_init() copies the table from flash into ram_vector_table and sets VTOR to it before main(), and CMSIS’s NVIC_SetVector requires the table to be in RAM already.
VTOR does not hold every bit of an address. On the Cortex-M0+ as CMSIS and the RP2040 define it, the table-offset field starts at bit 8, so the table must be 256-byte aligned: a 48-word table (192 bytes) at 0x2000_0100 is fine, at 0x2000_0080 it cannot be expressed. Later cores have their own rules, tied to the size of the table; check your core’s manual.
STEP 5
The entry point is for tools, not for the core
An ELF file also records an entry point (ENTRY() in the linker script). The hardware never reads it: after reset the core uses the reset vector. The entry point is for debuggers and loaders that start a program without a reset. The pico-sdk shows why they can differ: its entry point is _entry_point, which for a flash build sets VTOR to 0 and sends the chip back through the boot ROM and boot2, because a debugger that has just loaded flash left the flash interface in its slow mode. The reset vector points straight to _reset_handler.
STEP 6
Worked example: finding IRQ 5 and checking the entry
An RP2040 application’s table is at 0x1000_0100. IRQ 5 is exception 16 + 5 = 21:
If the handler’s code starts at 0x1000_0350, that word must hold 0x1000_0351. After runtime_init() copies the table to RAM, the same entry sits at the RAM table’s base + 0x54: the offset depends only on the exception number.
MYTHS AND FACTS
Common misconceptions
Word 0 is the reset handler
Word 0 is the initial stack pointer; the reset vector is word 1.
IRQ n uses word n
IRQ n uses word n + 16; the first 16 words belong to the architecture.
An unhandled interrupt is ignored
It runs the default handler, usually an infinite loop or a breakpoint, and the program appears to hang.
The ELF entry point is where the chip starts
The chip starts at the reset vector; the entry point is only for debuggers and loaders.
VTOR can point anywhere
It holds only the upper bits: on the Cortex-M0+ the table must be 256-byte aligned.
Check yourself
Answer in your head, then open the card.
A table is at 0x0800_0000. At what address is the entry for IRQ 10, and what must it hold if the handler starts at 0x0800_0400?
Exception 26: 0x0800_0000 + 4 × 26 = 0x0800_0068. It must hold 0x0800_0401.
A program enables the UART interrupt and hangs the first time a byte arrives. The debugger shows the core in Default_Handler. What is the likely cause?
The UART handler is not linked into the table: its name is misspelled, it is declared static, or it is in a file that was not linked (or was compiled as C++ without extern "C"), so the weak default remains.
Why must the vector table be in RAM before NVIC_SetVector is called?
It writes a new handler address into the table at VTOR. Flash cannot be written like RAM, so the table must first be copied to RAM and VTOR pointed at the copy.
The reset vector in a hand-written table holds 0x0000_00C0, the exact address of Reset_Handler. What happens at power-on?
Bit 0 is clear, so the core would have to switch to the Arm state it does not support: it takes a HardFault on the first instruction, before Reset_Handler runs.
Sources (5)
- Arm, CMSIS-DFP, Device/ARMCM0plus/Source/startup_ARMCM0plus.c — a 48-entry __VECTOR_TABLE: initial SP, Reset_Handler, NMI, HardFault, seven reserved zeros, SVC, two reserved, PendSV, SysTick, then interrupts; every handler declared weak and, except HardFault, aliased to Default_Handler (an infinite loop)
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — SCB_VTOR_TBLOFF_Pos 8, mask 0xFFFFFF << 8 (VTOR present only when __VTOR_PRESENT = 1); NVIC_SetVector writes into the table at VTOR, which “must been relocated to SRAM before”
- Raspberry Pi Ltd, pico-sdk 1.5.1, src/rp2_common/pico_standard_link/crt0.S — the .vectors table (__StackTop, _reset_handler, isr_nmi …, isr_irq0–isr_irq31); weak default handlers that execute bkpt #0; _entry_point, the ELF entry, which for flash builds sets VTOR = 0 and vectors back through the boot ROM
- Raspberry Pi Ltd, pico-sdk 1.5.1, src/rp2_common/pico_runtime/runtime.c and pico_standard_link/memmap_default.ld — runtime_init() copies the table at VTOR into ram_vector_table and sets VTOR to it; the linker script declares ENTRY(_entry_point) and KEEP (*(.vectors))
- Arm, Armv6-M Architecture Reference Manual (DDI0419), §B1.5.2 “Exception number definition” and §B1.5.3 “The vector table” — exception numbers 1–15 for Reset, NMI, HardFault, SVCall, PendSV and SysTick with the others reserved; external interrupts from 16; each table entry is a word with bit 0 giving the Thumb state