STM32 Boot Secrets: Your Code’s Journey to main()

Previously in Part 1, we explored our project structure. Now, let’s uncover what actually happens when you power on your STM32F429ZI microcontroller.

Before Your Code Runs

Furthermore, while our application code begins at the main() function, several critical initialization steps occur before this point. Understanding this hidden journey is essential for debugging boot issues and building robust embedded systems.

The Hardware Reset Sequence

Additionally, when the STM32F429ZI first receives power, here’s what happens in those crucial first microseconds:

1. Power-On Reset Circuit Activation

First, the power-on reset circuit springs into action:

  • Monitors the supply voltage
  • Holds the processor in reset until voltage stabilizes
  • Ensures clean, predictable startup

2. Core Register Initialization

Next, all core registers are set to their default values:

  • General-purpose registers (R0-R12) cleared
  • Stack pointer (SP) undefined
  • Program counter (PC) undefined
  • Status registers reset

3. Reading the Initial Stack Pointer

Moreover, the processor performs its first memory read:

Address 0x00000000 → Initial Stack Pointer Value

This value tells the processor where the top of the stack should be – typically at the end of RAM.

4. Reading the Reset Vector

Subsequently, the processor reads the second critical value:

Address 0x00000004 → Reset Handler Address

This address points to the very first instruction your processor will execute.

Boot Source Selection

Furthermore, the STM32 checks the BOOT0 pin to determine where to boot from:

BOOT0 = 0: Normal Application Mode

When BOOT0 is connected to ground:

  • Boot from Flash memory (starting at 0x08000000)
  • Your application code runs
  • This is the normal operating mode

BOOT0 = 1: System Memory Mode

When BOOT0 is connected to VDD:

  • Boot from System memory (bootloader)
  • ST’s factory bootloader runs
  • Used for programming via UART, USB, etc.

The Path Through Flash Memory (Normal Boot)

Additionally, when booting from Flash (BOOT0 = 0), here’s the detailed journey:

1. Jump to Reset_Handler

The processor reads the reset vector and jumps to Reset_Handler in startup_stm32f429zitx.s. This is assembly code that looks like:

Reset_Handler:
    ldr   sp, =_estack       /* Set stack pointer */
    
    /* Copy initialized data from Flash to RAM */
    movs  r1, #0
    b     LoopCopyDataInit

CopyDataInit:
    ldr   r3, =_sidata
    ldr   r3, [r3, r1]
    str   r3, [r0, r1]
    adds  r1, r1, #4

LoopCopyDataInit:
    ldr   r0, =_sdata
    ldr   r3, =_edata
    adds  r2, r0, r1
    cmp   r2, r3
    bcc   CopyDataInit

2. Memory Section Initialization

Moreover, Reset_Handler performs crucial memory setup:

Copy .data Section:

  • Contains initialized variables
  • Stored in Flash, but needs to run from RAM
  • Copied from Flash to RAM addresses

Clear .bss Section:

  • Contains uninitialized global variables
  • Must be zeroed before use
  • Prevents random initial values

3. System Clock Setup

Next, SystemInit() is called to configure clocks:

  • Sets up HSI, HSE, or PLL
  • Configures bus prescalers
  • Prepares system for desired speed

4. C Library Initialization

Subsequently, the C runtime is initialized:

  • Sets up heap for malloc()
  • Initializes stdio
  • Prepares C++ constructors (if used)

5. The Grand Entrance to main()

Finally, after all this preparation:

bl    main    /* Branch with Link to main */

Our Application Begins: Entering main()

Furthermore, when we finally reach our main() function, the first thing we do is verify the system is correctly configured:

int main(void)
{
    /* Verify interrupt vector table */
    uint32_t *vector_table = (uint32_t *)SCB->VTOR;
    uint32_t rtc_wkup_handler_addr = vector_table[RTC_WKUP_IRQn + 16];

Understanding the Interrupt Vector Table

Additionally, for readers new to microcontroller programming, let’s break down this critical concept:

What is an Interrupt Vector Table?

Think of the interrupt vector table as a phone directory for emergencies. When certain events happen (like a button press or timer expiring), the microcontroller needs to know who to call – which function should handle this event.

The interrupt vector table is exactly that – a list of addresses pointing to functions that handle specific events.

Key Components Explained

SCB (System Control Block):

  • Central control panel for processor core functions
  • Contains configuration and status registers
  • Manages interrupts, system exceptions, and more

VTOR (Vector Table Offset Register):

  • Tells processor where to find the interrupt vector table
  • Can be relocated for bootloader/application separation
  • Default points to start of Flash (0x08000000)

RTC_WKUP_IRQn:

  • RTC = Real-Time Clock
  • WKUP = Wake-up feature
  • IRQn = Interrupt Request number

Moreover, the RTC keeps time even when the system sleeps. Its wake-up feature acts like an alarm clock, waking the system at predetermined times.

Breaking Down the Verification Code

Let’s examine each line in detail:

Line 1: Accessing the Vector Table

uint32_t *vector_table = (uint32_t *)SCB->VTOR;

This line does three things:

  1. Accesses the SCB (control panel)
  2. Reads VTOR register (vector table address)
  3. Creates a pointer to navigate the table

Furthermore, uint32_t * means we’re creating a pointer that can access 32-bit numbers (addresses).

Line 2: Looking Up the Wake-up Handler

uint32_t rtc_wkup_handler_addr = vector_table[RTC_WKUP_IRQn + 16];

Here’s what happens:

  • RTC_WKUP_IRQn is the interrupt number
  • We add 16 because first 16 slots are system exceptions
  • We retrieve the function address for RTC wake-up

This is like looking up “Fire Department” in our emergency directory.

The Validation Check

if (rtc_wkup_handler_addr == 0 || 
    rtc_wkup_handler_addr < 0x08000000 || 
    rtc_wkup_handler_addr > 0x08100000) {
    UART_SendString("[STARTUP] ERROR: Invalid RTC_WKUP_IRQHandler!\r\n");
} else {
    UART_SendString("[STARTUP] RTC_WKUP_IRQHandler address looks valid\r\n");
}

We’re verifying that:

  1. Not Zero: Handler exists (not empty entry)
  2. Valid Range: Points to Flash memory (0x08000000-0x08100000)
  3. Reasonable Address: Within our application space

Why This Verification Matters

Additionally, this validation is crucial because:

  1. Power Management Depends on It: Our system uses RTC wake-up for low-power modes
  2. Prevents Forever Sleep: Without valid handler, device might never wake up
  3. Early Error Detection: Catches configuration issues immediately
  4. IoT Reliability: Critical for devices that must wake periodically

Imagine an IoT sensor that goes to sleep to save battery but can’t wake up – it becomes a brick!

System Memory Boot Mode Details

Furthermore, if BOOT0 = 1, the system enters bootloader mode:

  1. Processor jumps to ST’s bootloader
  2. Bootloader scans for active interfaces:
    • USART1, USART3
    • USB DFU
    • I2C
    • SPI
    • CAN
  3. Enters protocol handling based on detected interface
  4. Allows firmware update without debugger

This mode is invaluable for field updates and recovery.

The Complete Boot Timeline

Here’s the complete sequence with timing:

Time 0µs:    Power applied
Time 100µs:  Reset circuit stabilizes
Time 150µs:  Read stack pointer from 0x00000000
Time 151µs:  Read reset vector from 0x00000004
Time 152µs:  Jump to Reset_Handler
Time 200µs:  Memory initialization begins
Time 1ms:    SystemInit() configures clocks
Time 2ms:    C library initialization
Time 3ms:    main() function entered
Time 3.1ms:  Vector table verification
Time 3.2ms:  Continue with application

Code Organization Reference

Moreover, for readers following along with our GitHub repository:

Key Files:

  • Src/main.c – Main entry point (where we verify vector table)
  • Startup/startup_stm32f429zitx.s – Contains Reset_Handler
  • Inc/ – Header files with system definitions

When examining the code, start with main.c after understanding the boot sequence.

Key Takeaways

  1. Boot is Deterministic: Same sequence every power-on
  2. Multiple Stages: Hardware → Assembly → C runtime → Your code
  3. Vector Table is Critical: Links hardware events to software
  4. Verification Prevents Issues: Early checks save debugging time
  5. BOOT0 Controls Path: Hardware pin determines boot source

Common Boot Issues and Solutions

Stuck in Reset_Handler:

  • Check linker script memory definitions
  • Verify RAM/Flash sizes

main() Never Reached:

  • SystemInit() may be hanging
  • Check clock configuration

Vector Table Invalid:

  • Verify VTOR points to correct location
  • Check linker script sections

What’s Next?

Now that we understand how we reach main() and why vector table verification matters, we’re ready to explore the first major initialization – the SysTick timer. In Part 3, we’ll dive deep into how SysTick_Init() creates the 1ms heartbeat that drives our entire system.

Next: Part 3 – Deep Dive into SysTick Timer →


This is Part 2 of the STM32 IoT Framework series. Find the complete code on GitHub.

💡 Did You Know? The STM32F429ZI can boot from three sources: Flash, System Memory, or SRAM, controlled by BOOT0 and BOOT1 pins!

Leave a Comment

Your email address will not be published. Required fields are marked *

2 thoughts on “STM32 Boot Secrets: Your Code’s Journey to main()”

  1. Pingback: How to Structure STM32 IoT Projects Without HAL - Learn By Building

  2. Pingback: STM32 SysTick: Build a Reliable 1ms Heartbeat (Code Walkthrough) - Learn By Building