The puzzle
Lesson 1 ended with the core reading two words from address 0. Something has to be there, and it has to be the right thing: your application on a normal day, a built-in boot loader when the flash is empty, perhaps code a debugger just loaded into RAM. Who decides what the core finds at address 0, and what happens when the answer is “nothing valid”?
STEP 1
Something valid must sit at address 0
A Cortex-M0+ reads its initial SP and reset vector from the start of the address space, so the chip must make a valid vector table appear there at every reset. Chips solve this in one of three ways:
- A fixed boot ROM at 0. The vendor’s ROM always runs first and later points the core at your code. The RP2040 does this: address 0 is always its boot ROM.
- Aliasing. Boot pins or option bits sampled at reset select which memory appears at address 0 (flash, a system ROM with the vendor’s boot loader, or SRAM). The memory also stays at its own address.
- Flash directly at 0. The simplest chips just put flash there, with any boot loader stored in a protected corner of it.
The core always reads its first two words from the bottom of the address space. Many chips decide what appears there with boot pins or option bits sampled at reset, by aliasing one memory at address 0. The three choices shown are offered by the STM32F4 HAL’s remap macros (the SYSCFG MEMRMP register): main flash, system flash holding ST’s boot loader, and embedded SRAM; the F405/F407 can also map an external-memory bank. The memory stays at its own address too. Stack and handler values are illustrative. The RP2040 does not remap: address 0 is always its boot ROM.
The alias is a window, not a copy. On the STM32F4, used here as a labelled example, flash is always at 0x0800_0000 and the HAL’s remap macros can make it, the system flash (ST’s boot loader) or SRAM appear at 0x0000_0000 (on the F405/F407 also an external-memory bank). Because the linker placed your code at 0x0800_0000, every handler address in the table is a 0x0800_… address: the core fetches the table through the alias, but after the first jump it runs from flash’s real address.
Chips whose core has a vector-table offset register (VTOR; optional on the Cortex-M0+) can move the table after reset. That is how a boot ROM or a boot stage hands over: it sets VTOR to the application’s table and loads SP and PC from it, the same two loads the hardware does at reset (but without resetting anything else). On the RP2040 it is boot2 that does this, as the next sections show. Without VTOR, handlers can be changed at run time by aliasing RAM at address 0 (what CMSIS’s NVIC_SetVector assumes) or by handlers in flash that dispatch through a table of function pointers in RAM.
STEP 2
The RP2040’s boot ROM, decision by decision
The RP2040’s boot ROM source is published, so every decision can be read. Both cores start in the ROM; core 1 goes to a holding loop and core 0 makes these checks in order:
↑ This step uses the figure at the top of the page.
- Rescue flag. A debugger can set a flag in the chip-reset block and reset the chip; the ROM then halts core 0 at once, so code in flash that crashes the chip or disables the debug pins never runs.
- Watchdog direct boot. If watchdog scratch registers 4–7 hold a magic number, a check value, a stack pointer and an entry point, the ROM jumps straight to that entry. The scratch registers persist through a soft reset such as the watchdog’s, not through a power-on.
- BOOTSEL. The ROM waits for the pull-up on the flash chip-select line to charge the trace, then samples the pin 9 times and takes a majority vote. Held low (the BOOTSEL button on most boards), it skips flash and starts the USB boot loader.
- Flash checksum. Otherwise it reads the first 256 bytes of flash into the top of SRAM (0x2004_1F00) and accepts them only if a CRC-32 of the first 252 bytes matches the last 4. It retries up to 128 times, cycling through the four SPI clock modes, then falls back to USB boot.
The majority vote and the delay are sized for the worst case. The delay loop costs 3 cycles per count and the ROM waits delay(100 × ROSC_MHZ_MAX / 3) with ROSC_MHZ_MAX = 12, so 400 counts, 1200 cycles: 100 µs at 12 MHz (above the 11.3 MHz maximum of lesson 1’s table) and longer on any slower chip. The pull-up always gets at least 100 µs.
STEP 3
Why a second stage exists
The ROM can read flash only in the slowest, most compatible way, with a basic serial read command, because it cannot know which flash chip the board uses. Executing code from that would be very slow. The 256 bytes it checks are therefore not your application but a second-stage boot loader (boot2), written for a specific flash chip: it configures the flash and the QSPI interface for fast execute-in-place (XIP), then sets VTOR to 0x1000_0100, the application’s vector table right after boot2 in flash, loads SP and PC from it, and branches. From there on the chip behaves like any Cortex-M after reset.
The RP2040 boot ROM reads 256 bytes from the start of flash and accepts them only if a CRC-32 of the first 252 bytes (polynomial 0x04C11DB7, initial value 0xFFFFFFFF) equals the 32-bit value stored little-endian in the last four. The block here is illustrative random data with a correct checksum; flip one bit anywhere and the check fails.
The checksum is a CRC-32 with polynomial 0x04C11DB7, processed most significant bit first, starting from 0xFFFFFFFF, without a final inversion (the variant usually called CRC-32/MPEG-2). The SDK’s build script appends it when it pads boot2. Any single flipped bit changes it, so a half-written or blank flash is rejected rather than executed.
STEP 4
Worked example: how long does a blank board take to give up?
A new board with empty flash reads as all 0xFF. The CRC of 252 bytes of 0xFF does not equal 0xFFFFFFFF, so every attempt fails. The ROM’s comment says each attempt takes about 4 ms at the 6.5 MHz boot clock:
About half a second after power-on, the board appears as a USB drive, without anyone pressing BOOTSEL. On a slow chip (1.8 MHz) the same loop takes about 6.5/1.8 ≈ 3.6 times longer, close to 2 s.
MYTHS AND FACTS
Common misconceptions
Address 0 is where flash is
Only on some chips. On the RP2040 it is ROM, and on aliasing chips it depends on the boot configuration.
The alias copies flash to address 0
It is the same memory seen at two addresses; nothing is copied.
A corrupt image bricks the RP2040
The ROM rejects a bad boot2 and falls back to USB boot, so a board with its USB port wired up can be reflashed that way (or through the debug port). The ROM checks only boot2: a corrupt application behind a valid boot2 still runs, and if it hangs, recovery needs BOOTSEL or the debug port.
The boot ROM runs my application
It runs boot2, which configures the flash and then vectors into the application.
A watchdog reset and a power-on take the same path
Scratch registers survive a watchdog reset, so the ROM can take the direct-boot path; a power-on resets them to 0.
Check yourself
Answer in your head, then open the card.
An STM32F4 boots with main flash aliased at 0x0000_0000. Its reset vector reads 0x0800_02D1. Where does the first instruction execute, and in which state?
At 0x0800_02D0, in Thumb state (bit 0 set). The table is read through the alias, but the reset vector holds flash’s real address.
An RP2040 board with good flash appears as a USB drive at every power-on, even though nobody presses BOOTSEL. Which checks could be responsible?
The chip-select line reads low in the majority vote (a short or a missing pull-up on the board, or the button stuck), or the boot2 checksum fails on every attempt (the wrong flash, a damaged image, or a flash chip that does not answer the ROM’s read command).
Why does the RP2040 boot ROM verify only 256 bytes and not the whole application?
It cannot know how big the application is or which flash chip is fitted. It verifies only boot2, whose size is fixed; the application is not checked at all by the ROM.
Byte 17 of boot2 has one bit flipped. What does the ROM do?
The computed CRC no longer matches the stored one. The ROM retries all 128 attempts, never gets a match, and starts the USB boot loader.
Sources (6)
- Raspberry Pi Ltd, pico-bootrom-rp2040, bootrom/bootrom_rt0.S — core 1 goes to a holding loop (wait_for_vector); core 0 checks the rescue flag (CHIP_RESET PSM_RESTART_FLAG) and halts; the watchdog direct-boot check (scratch 4 = 0xb007c0d3, scratch 5 = entry XOR −0xb007c0d3, scratch 6 = SP, scratch 7 = entry); then main(). pico-sdk 1.5.1 hardware/regs/watchdog.h: each scratch register “persists through soft reset of the chip” and resets to 0
- Raspberry Pi Ltd, pico-bootrom-rp2040, bootrom/bootrom_main.c — main(): delay(100 × ROSC_MHZ_MAX / 3) for the pull-ups, then 9 samples of the QSPI chip select with a majority vote (sum ≥ 5 → flash boot); _flash_boot(): up to FLASH_MAX_ATTEMPTS = 128, cycling the four CPOL/CPHA modes, reads 256 bytes to SRAM_END − 256 and compares crc32_small of the first 252 with the last 4; “Each attempt takes around 4 ms total with a 6.5 MHz boot clock”; otherwise _usb_boot()
- Raspberry Pi Ltd, pico-bootrom-rp2040, bootrom/bootrom_misc.S (crc32_small), and pico-sdk 1.5.1, src/rp2_common/boot_stage2/pad_checksum and CMakeLists.txt — polynomial 0x04C11DB7 processed MSB first; the SDK pads boot2 to 252 bytes and appends the checksum little-endian, called with -s 0xffffffff (seed); equivalent to CRC-32/MPEG-2, whose check value for the ASCII string 123456789 is 0x0376E6E7
- Raspberry Pi Ltd, pico-sdk 1.5.1, boot_stage2/asminclude/boot2_helpers/exit_from_boot2.S and boot2_w25q080.S — entered from the boot ROM with lr = 0, boot2 sets VTOR = XIP_BASE + 0x100, loads MSP and the reset vector from that table and branches; boot2_w25q080.S configures the flash and the SSI for execute-in-place
- STMicroelectronics, stm32f4xx-hal-driver, Inc/stm32f4xx_hal.h (SYSCFG memory remap macros), and cmsis-device-f4, stm32f407xx.h — __HAL_SYSCFG_REMAPMEMORY_FLASH / _SYSTEMFLASH / _SRAM: “Main Flash memory mapped at 0x00000000”, “System Flash memory mapped at 0x00000000”, “Embedded SRAM mapped at 0x00000000” (SYSCFG MEMRMP MEM_MODE); FLASH_BASE 0x08000000, SRAM1_BASE 0x20000000
- Arm, CMSIS 6, CMSIS/Core/Include/core_cm0plus.h — VTOR is optional on the Cortex-M0+ (__VTOR_PRESENT); NVIC_SetVector: “VTOR must been relocated to SRAM before. If VTOR is not present address 0 must be mapped to SRAM”