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:

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

Worked example: Finding a handler address

Given: VTOR = 0x08000000; USART1 is IRQ 37

Find: Vector location.

Answer: 0x080000D4

Worked example: Relocating vectors for an application

Given: Bootloader occupies 0x08000000–0x08007FFF; App starts at 0x08008000

Find: What must the app do?

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.

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

Applications

Key takeaways

Going further