Boot Sequence, Startup Code, HAL & BSP
What happens between power-on and main()? Learn the Cortex-M reset sequence, the vector table, Reset_Handler and C runtime initialisation, SystemInit and clock setup, CMSIS, vendor HALs and low-level drivers, the Board Support Package (BSP) layer, and how to structure portable code so the same application runs on different boards.
When power is applied, a Cortex-M does not start at main(). It reads the initial stack pointer from address 0x00000000, the reset handler address from 0x00000004, and jumps there. The reset handler prepares memory and clocks and only then calls main(). If any of this is wrong, the board appears 'dead' — even with perfect application code.
Above the startup code sit abstraction layers: CMSIS (standard core access), the vendor HAL/LL drivers (peripherals), and your BSP (what is connected to which pin on this particular board). Clean layering is what lets a product move from one MCU to another in weeks instead of months.
An analogy
Booting is like opening a shop: unlock the door (reset vector), switch on the lights and heating (clocks), stock the shelves (copy .data, zero .bss), set up the tills (peripherals) — then open to customers (main).
The reset sequence
After reset the core loads SP from vector 0 and PC from vector 1 (with bit 0 set for Thumb). Boot pins or option bytes decide what is mapped at address 0: user Flash, system memory (ROM bootloader) or SRAM. Reset_Handler then runs before any C variable is valid.
C runtime initialisation
Reset_Handler (from startup_xxx.s or a C equivalent) typically:
- Copies .data initial values from Flash to RAM
- Zeroes .bss
- Calls SystemInit(): FPU enable, clock tree, VTOR, external memory
- Calls __libc_init_array() for C++ constructors and init functions
- Calls main(); if main returns, loops forever
CMSIS, HAL and LL
CMSIS-Core gives standard names for core registers and functions (NVIC_EnableIRQ, SysTick_Config, __WFI) on every Cortex-M vendor. Vendor HALs (STM32 HAL, NXP MCUXpresso SDK, Nordic nrfx) provide portable, feature-rich peripheral APIs at some cost in size and speed; LL (low-layer) drivers are thin inline register wrappers. Many teams use HAL for complex peripherals (USB, Ethernet) and LL/registers for hot paths.
BSP design and portability
The BSP captures board facts: pin mapping, clock sources, which sensors are fitted, revision detection. Keep drivers hardware-agnostic by injecting dependencies (bus handles, pin callbacks). For multi-product platforms use Device Tree (Zephyr), Kconfig or board-description headers. The same structure supports unit tests: link application code against a 'fake BSP' on a PC.
Formulas
- Vector table entries:
[0] initial MSP, [1] Reset_Handler, [2] NMI, [3] HardFault, … [16+n] IRQn - Vector address:
addr(handler n) = VTOR + 4 × (16 + IRQn) - Boot sequence:
POR → load MSP & PC → Reset_Handler → .data/.bss init → SystemInit → (C++ ctors) → main
Worked example: Finding a handler address
Given: VTOR = 0x08000000; USART1 is IRQ 37
Find: Vector location.
- Index = 16 + 37 = 53
- Address = 0x08000000 + 4 × 53 = 0x080000D4
Answer: 0x080000D4
Worked example: Relocating vectors for an application
Given: Bootloader occupies 0x08000000–0x08007FFF; App starts at 0x08008000
Find: What must the app do?
- Link app at 0x08008000
- Set SCB->VTOR = 0x08008000 in SystemInit
Answer: Otherwise interrupts jump into the bootloader's handlers
Worked example: Dead board diagnosis
Given: Debugger shows PC stuck in Default_Handler right after enabling a timer
Find: Likely cause.
- An enabled interrupt has no handler with the exact name
Answer: Handler name typo (e.g. TIM2_IRQ_Handler) — the weak default was used
A vector table and reset handler in C (GCC)
#include <stdint.h>
extern uint32_t _estack;
void Reset_Handler(void);
void Default_Handler(void) { for (;;) { } } /* trap unexpected IRQs */
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 USART1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
__attribute__((section(".isr_vector"), used))
void (* const vector_table[])(void) = {
(void (*)(void))&_estack, /* 0: initial stack pointer */
Reset_Handler, /* 1: reset */
NMI_Handler, /* 2 */
HardFault_Handler, /* 3 */
/* ... MemManage, BusFault, UsageFault, reserved, SVC, PendSV ... */
[15] = SysTick_Handler,
[16 + 37] = USART1_IRQHandler, /* IRQ 37 on STM32F4 */
};Weak aliases let any .c file override a handler just by defining a function with the same name. Unused interrupts fall into Default_Handler — a breakpoint there catches 'mystery' interrupts.
A BSP header separating board wiring from drivers
/* bsp_board_v2.h -- everything specific to this PCB revision */
#pragma once
#include "stm32f4xx_hal.h"
#define BSP_LED_STATUS_PORT GPIOC
#define BSP_LED_STATUS_PIN GPIO_PIN_13
#define BSP_TEMP_SPI (&hspi2)
#define BSP_TEMP_CS_PORT GPIOB
#define BSP_TEMP_CS_PIN GPIO_PIN_12
#define BSP_HAS_EEPROM 1
void bsp_init(void); /* clocks, pins, peripherals for this board */
void bsp_led_set(int on);
int bsp_read_hw_revision(void); /* strap resistors on ADC pin */
/* Application code calls bsp_led_set(1) and never mentions GPIOC,
so moving the LED on board v3 only changes this file. */Drivers (e.g. a temperature-sensor driver) receive the SPI handle and CS pin from the BSP rather than hard-coding them — that is what makes them reusable across boards.
Common mistakes
- Wrong: Using globals in code that runs before .data/.bss init. — Right: Only stack/local variables are valid before Reset_Handler finishes.
- Wrong: Forgetting VTOR when the app is not at the start of Flash. — Right: Set SCB->VTOR before enabling interrupts.
- Wrong: Board pin numbers sprinkled through application code. — Right: Centralise in the BSP.
Applications
- Board bring-up: First code on a new PCB: clocks, LED, UART console.
- Product families: One application, several boards via BSPs.
- Bootloaders: VTOR relocation and jump to application.
- Unit testing: Fake BSP/HAL to run firmware logic on a PC.
Key takeaways
- Reset: load MSP & PC from the vector table.
- Startup copies .data, zeroes .bss, runs SystemInit, then main.
- CMSIS → HAL/LL → drivers → application.
- BSP isolates board details for portability.
Going further
- Secure boot ROM: Immutable boot code verifies the first-stage image.
- Device tree (Zephyr): Board hardware described in data, not code.
- C++ static init order: Constructors run before main — beware dependencies.