What is STM32F407 Startup & Reset Sequence-Embedded C Training Institute in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Tutorial 20 — System Initialization

STM32F407 Startup & Reset Sequence

Follow every step from power-on to the first line of main(): vector table fetch, Reset_Handler, .data/.bss init, SystemInit, PLL clock configuration, and the complete RCC clock tree.

~1ms Boot time168 MHz Target clockPLL Clock sourceHSE 8 MHz crystal5 WS Flash wait states
===== 1: OVERVIEW =====

01 Reset Sequence Overview

When the STM32F407 is powered on or reset, the ARM Cortex-M4 core follows a deterministic startup sequence:

Diagram: 01 Reset Sequence Overview
1
Hardware reset asserted
NRST pin pulled low (power-on, manual reset, or watchdog). Core held in reset state.
↓
2
Vector table fetch
Core reads address 0x00000000 (= 0x08000000 aliased by BOOT0=0) to load the initial Stack Pointer (MSP) from vector[0].
↓
3
PC ← Reset_Handler
Core reads vector[1] (offset 0x04) which contains the address of Reset_Handler. PC is set to this address.
↓
4
Reset_Handler runs
Assembly routine: copies .data from Flash to SRAM, zeroes .bss, calls SystemInit(), then calls main().
↓
5
SystemInit()
Configures Flash wait states, enables PLL (HSE → 168 MHz), sets AHB/APB prescalers, switches SYSCLK to PLL.
↓
6
main()
Your application starts. Clock is 168 MHz, SRAM initialized, FPU optionally enabled.
Clock at reset: After reset, the chip runs on the internal RC oscillator (HSI) at 16 MHz. SystemInit() switches to the external crystal (HSE = 8 MHz on Discovery board) and PLL to reach 168 MHz.
===== 2: VECTOR TABLE =====

02 The Vector Table

The vector table is an array of 32-bit addresses placed at the very start of Flash (0x08000000). Entry 0 is the initial stack pointer value, not an address. Every other entry is a function pointer to an ISR (Interrupt Service Routine). Missing/default handlers are set to a Default_Handler that loops forever.

C — Minimal vector table (first 16 core + 82 STM32 IRQs)

#include <stdint.h>

extern uint32_t _estack;      /* from linker: top of SRAM */
extern void Reset_Handler(void);

/* Weak default handlers — application can override by defining
   the same name (without __attribute__((weak))) */
void Default_Handler(void) { while(1); }

#define WEAK_ALIAS(name) \
  void name(void) __attribute__((weak, alias("Default_Handler")))

WEAK_ALIAS(NMI_Handler);
WEAK_ALIAS(HardFault_Handler);
WEAK_ALIAS(MemManage_Handler);
WEAK_ALIAS(BusFault_Handler);
WEAK_ALIAS(UsageFault_Handler);
WEAK_ALIAS(SVC_Handler);
WEAK_ALIAS(DebugMon_Handler);
WEAK_ALIAS(PendSV_Handler);
WEAK_ALIAS(SysTick_Handler);
/* ... peripheral IRQs ... */
WEAK_ALIAS(WWDG_IRQHandler);
WEAK_ALIAS(TIM1_UP_TIM10_IRQHandler);
WEAK_ALIAS(TIM2_IRQHandler);
WEAK_ALIAS(TIM3_IRQHandler);
WEAK_ALIAS(TIM4_IRQHandler);
WEAK_ALIAS(USART1_IRQHandler);
WEAK_ALIAS(USART2_IRQHandler);
WEAK_ALIAS(EXTI0_IRQHandler);
WEAK_ALIAS(EXTI1_IRQHandler);
WEAK_ALIAS(EXTI15_10_IRQHandler);
WEAK_ALIAS(DMA1_Stream0_IRQHandler);
WEAK_ALIAS(DMA2_Stream0_IRQHandler);
WEAK_ALIAS(ADC_IRQHandler);
WEAK_ALIAS(CAN1_TX_IRQHandler);
WEAK_ALIAS(CAN1_RX0_IRQHandler);
/* (all 82 IRQs from reference manual Table 61) */

/* Vector table — must go in .isr_vector section (linker places at 0x08000000) */
typedef void (*IRQ_Handler_t)(void);

__attribute__((section(".isr_vector")))
const IRQ_Handler_t g_vectors[] = {
    (IRQ_Handler_t)&_estack,  /* [0] Initial MSP */
    Reset_Handler,             /* [1] Reset */
    NMI_Handler,               /* [2] NMI */
    HardFault_Handler,         /* [3] Hard Fault */
    MemManage_Handler,         /* [4] Memory Management */
    BusFault_Handler,          /* [5] Bus Fault */
    UsageFault_Handler,        /* [6] Usage Fault */
    0, 0, 0, 0,               /* [7-10] Reserved */
    SVC_Handler,               /* [11] SVCall */
    DebugMon_Handler,          /* [12] Debug Monitor */
    0,                          /* [13] Reserved */
    PendSV_Handler,            /* [14] PendSV */
    SysTick_Handler,           /* [15] SysTick */
    /* STM32F407 peripheral IRQs (IRQ 0 = index 16): */
    WWDG_IRQHandler,           /* [16] IRQ0: WWDG */
    Default_Handler,           /* [17] IRQ1: PVD */
    Default_Handler,           /* [18] IRQ2: TAMP_STAMP */
    Default_Handler,           /* ... continue for all 82 IRQs */
};
VTOR — Vector Table Offset Register: After booting, you can relocate the vector table to SRAM by writing the new address to SCB_VTOR (0xE000ED08). This is required for bootloaders that jump to application code at a non-zero offset.

C — Relocate vector table to SRAM (for bootloader use)

#define SCB_VTOR  (*(volatile uint32_t *)0xE000ED08)

/* Bootloader: jump to application at 0x08020000 (sector 5) */
void jump_to_application(void) {
    uint32_t app_start = 0x08020000;

    /* Read app's initial MSP and Reset_Handler from its vector table */
    uint32_t app_msp = *((volatile uint32_t *)(app_start + 0));
    uint32_t app_reset = *((volatile uint32_t *)(app_start + 4));

    /* Disable all interrupts */
    __asm__ volatile("cpsid i");

    /* Set VTOR to point at app's vector table */
    SCB_VTOR = app_start;

    /* Set stack pointer */
    __asm__ volatile("msr msp, %0" :: "r"(app_msp));

    /* Jump to app's Reset_Handler */
    ((void(*)())(app_reset))();
}
===== 3: RESET HANDLER =====

03 Reset_Handler in Assembly

The startup file (startup_stm32f407xx.s) provides Reset_Handler in ARM assembly. Here is a minimal but complete version:

ARM Assembly (startup.s)

/* startup.s — Minimal Reset_Handler for STM32F407 */
    .syntax unified
    .cpu cortex-m4
    .fpu softvfp
    .thumb

    .global Reset_Handler
    .global Default_Handler

/*──────────────────────────────────────────────
  Reset_Handler: entry point after reset
  1. Copy .data from Flash to SRAM
  2. Zero .bss
  3. Call SystemInit() — configure clocks
  4. Call main()
  5. Loop forever if main() returns
──────────────────────────────────────────────*/
    .section .text.Reset_Handler
    .weak Reset_Handler
    .type Reset_Handler, %function
Reset_Handler:

  /* ── Copy .data section (Flash → SRAM) ── */
  ldr   r0, =_sdata     /* SRAM destination start */
  ldr   r1, =_edata     /* SRAM destination end */
  ldr   r2, =_sidata    /* Flash source (LMA) */
  movs  r3, #0
  b     .data_copy_end

.data_copy_loop:
  ldr   r4, [r2, r3]   /* load word from Flash */
  str   r4, [r0, r3]   /* store word to SRAM */
  adds  r3, r3, #4

.data_copy_end:
  adds  r4, r0, r3
  cmp   r4, r1
  bcc   .data_copy_loop    /* loop while dst < _edata */

  /* ── Zero .bss section ── */
  ldr   r2, =_sbss
  ldr   r4, =_ebss
  movs  r3, #0
  b     .bss_zero_end

.bss_zero_loop:
  str   r3, [r2]
  adds  r2, r2, #4

.bss_zero_end:
  cmp   r2, r4
  bcc   .bss_zero_loop     /* loop while ptr < _ebss */

  /* ── Call SystemInit (configure PLL, clocks) ── */
  bl    SystemInit

  /* ── Call C++ static constructors (if any) ── */
  bl    __libc_init_array

  /* ── Call main() ── */
  bl    main

  /* ── Infinite loop if main() returns ── */
.loop_forever:
  b     .loop_forever

    .size Reset_Handler, . - Reset_Handler

/*──────────────────────────────────────────────
  Default_Handler: catches unhandled interrupts
──────────────────────────────────────────────*/
    .section .text.Default_Handler
    .weak Default_Handler
    .type Default_Handler, %function
Default_Handler:
.loop_default:
  b     .loop_default
    .size Default_Handler, . - Default_Handler
===== 4: DATA BSS =====

04 .data and .bss Initialization

The C standard requires that global/static variables with initializers are set to their initial values before main(), and uninitialized globals are zero. The startup code handles both:

Why .data is copied from Flash

Flash is read-only during normal operation. Initialized globals must be modifiable (writable), so they live in SRAM at runtime. But their initial values are stored in Flash (part of the binary). The linker’s AT> FLASH directive creates this dual addressing.

C — What happens with these declarations

int x = 42;           /* .data: 42 stored in Flash, copied to SRAM */
int y;                 /* .bss: 0 at startup (zeroed by startup code) */
const int z = 100;    /* .rodata: stays in Flash, never copied */
static int w = 5;     /* .data: same as x */
static int v;         /* .bss: same as y */

void foo(void) {
    int local = 10;    /* stack: nothing in .data/.bss — pure runtime */
}
===== 5: SYSTEM INIT =====

05 SystemInit() — Clock Configuration

SystemInit() is called before main(). Its job: configure the STM32F407 to run at 168 MHz from HSE (8 MHz crystal) via the PLL. This is the most critical startup step for timing-sensitive peripherals.

RCC Reset State

After reset, RCC defaults are:

  • SYSCLK source = HSI (16 MHz internal RC oscillator)
  • HSE disabled
  • PLL disabled
  • AHB prescaler = 1 (HCLK = SYSCLK)
  • APB1 prescaler = 1 (PCLK1 = HCLK)
  • APB2 prescaler = 1 (PCLK2 = HCLK)
===== 6: CLOCK TREE =====

06 STM32F407 Clock Tree

Diagram: 06 STM32F407 Clock Tree
Clock Sources: HSI = 16 MHz (internal RC, always available) HSE = 8 MHz (external crystal on Discovery board) LSI = 32 kHz (for IWDG, RTC) LSE = 32.768 kHz (external RTC crystal) PLL (configured for 168 MHz): Input = HSE / M → VCO input = 8MHz / 8 = 1 MHz VCO = input × N → 1 MHz × 336 = 336 MHz SYSCLK = VCO / P → 336 MHz / 2 = 168 MHz ← SYSCLK USB48 = VCO / Q → 336 MHz / 7 = 48 MHz ← USB/SDIO/RNG Bus clocks from SYSCLK: HCLK = SYSCLK / AHB prescaler (1) = 168 MHz ← AHB, Core, DMA, Mem PCLK1 = HCLK / APB1 prescaler (4) = 42 MHz ← APB1 peripherals PCLK2 = HCLK / APB2 prescaler (2) = 84 MHz ← APB2 peripherals Timer clocks (doubled when APB prescaler > 1): TIM1,8,9,10,11 clock = PCLK2 × 2 = 168 MHz ← on APB2 TIM2–7,12–14 clock = PCLK1 × 2 = 84 MHz ← on APB1 ADC clock: ADC_CLK = PCLK2 / ADC_CCR_ADCPRE = 84MHz / 4 = 21 MHz Flash: At 168 MHz + VCC=3.3V: LATENCY = 5 WS (wait states)
===== 7: RCC REGISTERS =====

07 Key RCC Registers

OffsetRegisterKey Bits
0x00RCC_CRHSION(0), HSIRDY(1), HSEON(16), HSERDY(17), PLLON(24), PLLRDY(25)
0x04RCC_PLLCFGRPLLM[5:0], PLLN[14:6], PLLP[17:16], PLLSRC(22), PLLQ[27:24]
0x08RCC_CFGRSW[1:0]=clock select, SWS[3:2]=status, HPRE[7:4]=AHB, PPRE1[12:10]=APB1, PPRE2[15:13]=APB2
0x0CRCC_CIRClock interrupts (READY flags)
0x30RCC_AHB1ENRGPIOAEN(0)..GPIOGEN(6), DMA1EN(21), DMA2EN(22)
0x40RCC_APB1ENRTIM2EN(0)..TIM7EN(5), SPI2EN(14), SPI3EN(15), USART2EN(17), I2C1EN(21), DACEN(29)
0x44RCC_APB2ENRTIM1EN(0), USART1EN(4), ADC1EN(8), SPI1EN(12), SYSCFGEN(14)
===== 8: PLL CONFIG =====

08 PLL Configuration Details

The PLL is configured via RCC_PLLCFGR. For 168 MHz from 8 MHz HSE:

PLLCFGR bit fields for 168 MHz
FieldBitsValueEffect
PLLM[5:0]8HSE ÷ 8 = 1 MHz (VCO input; must be 1–2 MHz)
PLLN[14:6]336VCO = 1 × 336 = 336 MHz (must be 192–432 MHz)
PLLP[17:16]00 = ÷2SYSCLK = 336 ÷ 2 = 168 MHz
PLLSRC[22]1 = HSEPLL input from HSE (not HSI)
PLLQ[27:24]7USB clock = 336 ÷ 7 = 48 MHz

PLLCFGR value = (8) | (336 << 6) | (0 << 16) | (1 << 22) | (7 << 24) = 0x07405408

===== 9: FULL STARTUP IN C =====

09 Complete SystemInit() in C

C — SystemInit.c (bare-metal, no CMSIS)

#include <stdint.h>

/* ── RCC register addresses ── */
#define RCC_BASE    0x40023800UL
#define RCC_CR      (*(volatile uint32_t *)(RCC_BASE + 0x00))
#define RCC_PLLCFGR (*(volatile uint32_t *)(RCC_BASE + 0x04))
#define RCC_CFGR    (*(volatile uint32_t *)(RCC_BASE + 0x08))
#define FLASH_ACR   (*(volatile uint32_t *)0x40023C00)

/* ── Power register (needed to scale voltage for 168 MHz) ── */
#define RCC_APB1ENR (*(volatile uint32_t *)(RCC_BASE + 0x40))
#define PWR_CR      (*(volatile uint32_t *)0x40007000)

/*=========================================================
  SystemInit — Configure clocks for 168 MHz
  HSE (8MHz) → PLL (M=8, N=336, P=2, Q=7) → 168 MHz
  AHB=1, APB1=/4=42MHz, APB2=/2=84MHz
=========================================================*/
void SystemInit(void) {

    /* ── Step 1: Enable HSI (already on at reset, just ensure) ── */
    RCC_CR |= (1<<0);              /* HSION */
    while (!(RCC_CR & (1<<1)));   /* wait HSIRDY */

    /* ── Step 2: Switch SYSCLK to HSI (so we can safely reconfigure PLL) ── */
    RCC_CFGR &= ~(0x3);           /* SW = 00 = HSI */
    while ((RCC_CFGR & (0xC)) != 0); /* wait SWS=00 */

    /* ── Step 3: Disable PLL and HSE ── */
    RCC_CR &= ~((1<<24) | (1<<16));  /* PLLON=0, HSEON=0 */

    /* ── Step 4: Enable HSE (8MHz crystal) ── */
    RCC_CR |= (1<<16);              /* HSEON */
    uint32_t timeout = 100000;
    while (!(RCC_CR & (1<<17)) && --timeout); /* wait HSERDY */
    if (!timeout) {
        /* HSE failed — fall back to HSI */
        RCC_CR &= ~(1<<16);
        return;
    }

    /* ── Step 5: Enable Power controller for voltage scaling ── */
    RCC_APB1ENR |= (1<<28);      /* PWREN */
    PWR_CR |= (3<<14);           /* VOS = 11 = Scale 1 (needed for 168 MHz) */

    /* ── Step 6: Configure Flash: 5 wait states + caches ── */
    FLASH_ACR = (5<<0) |   /* LATENCY = 5 WS */
                (1<<8) |   /* PRFTEN */
                (1<<9) |   /* ICEN */
                (1<<10);  /* DCEN */

    /* ── Step 7: Configure PLL ──
       PLLM=8, PLLN=336, PLLP=00(÷2), PLLSRC=HSE, PLLQ=7
       → SYSCLK = 8/8 × 336 / 2 = 168 MHz
       → USB    = 8/8 × 336 / 7 = 48 MHz */
    RCC_PLLCFGR = (8<<0)  |    /* PLLM = 8 */
                  (336<<6)  |   /* PLLN = 336 */
                  (0<<16) |   /* PLLP = 00 = ÷2 */
                  (1<<22) |   /* PLLSRC = HSE */
                  (7<<24);    /* PLLQ = 7 */

    /* ── Step 8: Enable PLL ── */
    RCC_CR |= (1<<24);              /* PLLON */
    while (!(RCC_CR & (1<<25)));  /* wait PLLRDY */

    /* ── Step 9: Configure bus prescalers ── */
    RCC_CFGR &= ~((0xF<<4) | (0x7<<10) | (0x7<<13));
    RCC_CFGR |= (0<<4) |    /* HPRE = 0000 = ÷1 → HCLK=168MHz */
                (5<<10) |   /* PPRE1 = 101 = ÷4 → PCLK1=42MHz */
                (4<<13);   /* PPRE2 = 100 = ÷2 → PCLK2=84MHz */

    /* ── Step 10: Switch SYSCLK to PLL ── */
    RCC_CFGR &= ~(0x3);
    RCC_CFGR |= (0x2);            /* SW = 10 = PLL */
    while ((RCC_CFGR & (0xC)) != (0x8)); /* wait SWS=10 (PLL active) */

    /* System now running at 168 MHz from PLL/HSE */
}
===== 10: FPU =====

10 Enabling the FPU

The Cortex-M4F has a hardware floating-point unit. It is disabled by default for fault-protection. Before using float or double, enable it by setting CPACR in the System Control Block:

C — Enable FPU (call before any float operations)

#define SCB_CPACR  (*(volatile uint32_t *)0xE000ED88)

void FPU_Enable(void) {
    /* Enable CP10 (bits 21:20) and CP11 (bits 23:22) = full access */
    SCB_CPACR |= (0xF << 20);  /* 1111 = full access to CP10+CP11 */
    /* Memory barrier to ensure the write takes effect before FPU use */
    __asm__ volatile("dsb");
    __asm__ volatile("isb");
}

/* In GCC: compile with -mfpu=fpv4-sp-d16 -mfloat-abi=hard for HW FP */
/* Without -mfloat-abi=hard: compiler uses soft-FP (slow, no FPU regs) */
UsageFault on FPU instruction without CPACR set: If you call a float function without enabling CPACR, a UsageFault (NOCP bit in UFSR) fires. Enable FPU at the top of SystemInit() if you use any floating-point math.
===== 11: MINIMAL MAIN =====

11 Minimal Bare-Metal main()

With startup and SystemInit complete, here is the minimal skeleton for a bare-metal STM32F407 project. No HAL, no CMSIS, no standard library overhead:

C — main.c (bare-metal, 168 MHz, LED blink)

#include <stdint.h>

/* RCC + GPIOD for Discovery board LEDs (PD12–PD15) */
#define RCC_AHB1ENR  (*(volatile uint32_t *)0x40023830)
#define GPIOD_MODER  (*(volatile uint32_t *)0x40020C00)
#define GPIOD_ODR    (*(volatile uint32_t *)0x40020C14)

void delay(volatile uint32_t n) { while(n--); }

int main(void) {
    /* SystemInit() has already run from startup.s — we're at 168 MHz */

    /* Enable GPIOD clock */
    RCC_AHB1ENR |= (1<<3);  /* GPIODEN */

    /* PD12, PD13, PD14, PD15 → output */
    GPIOD_MODER &= ~(0xFF<<24);
    GPIOD_MODER |=  (0x55<<24);  /* 01 = output for all 4 pins */

    while (1) {
        GPIOD_ODR ^= (0xF<<12);  /* toggle all 4 LEDs */
        delay(1000000);
    }
}
===== 12: TIPS =====

12 Tips & Common Startup Pitfalls

Common Mistakes
  • Not setting Flash wait states before PLL: If you switch SYSCLK to PLL at 168 MHz without first setting LATENCY=5 in FLASH_ACR, you get random crashes from incorrect Flash reads. Always configure Flash BEFORE enabling PLL.
  • Forgetting voltage scaling: For 168 MHz, PWR_CR.VOS must be set to Scale 1 (0x11). Default after reset is Scale 2 (0x10) which limits HCLK to 144 MHz max.
  • HSE not starting: On the Discovery board, HSE requires the 8 MHz crystal. Always add a timeout for HSERDY and fall back to HSI if it fails.
  • main() returning: If main() returns, the startup code enters an infinite loop. This is intentional — the board should never return from main in embedded use.
  • Wrong PLLQ for USB: If you use USB, SDIO, or RNG, PLLQ must produce exactly 48 MHz. With 336 MHz VCO: PLLQ=7 → 336/7=48 ✓
  • APB timer clocks: APB1 timers (TIM2–7) run at 84 MHz (not 42 MHz), because when PPRE1>1, timer clocks are doubled. Check your ARR/PSC calculations against the correct timer clock.
  • Not calling SystemInit from startup: If you write custom startup assembly and forget bl SystemInit, you run at 16 MHz HSI and all timing-sensitive peripherals (UART baud, I2C timing) will be wrong.

C — Verify clock speeds at runtime

/* Read back CFGR to verify clock status */
uint32_t get_sysclk_source(void) {
    uint32_t sws = (RCC_CFGR >> 2) & 0x3;  /* SWS field */
    /* 00=HSI, 01=HSE, 10=PLL */
    return sws;
}

/* Use DWT cycle counter to measure elapsed cycles */
#define DWT_CTRL  (*(volatile uint32_t *)0xE0001000)
#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004)
#define CoreDebug_DEMCR (*(volatile uint32_t *)0xE000EDFC)

void DWT_Init(void) {
    CoreDebug_DEMCR |= (1<<24);  /* TRCENA — enable DWT */
    DWT_CYCCNT = 0;
    DWT_CTRL |= (1<<0);          /* CYCCNTENA */
}

uint32_t get_tick(void) { return DWT_CYCCNT; }

/* Cycle-accurate delay: */
void delay_us_dwt(uint32_t us) {
    uint32_t start = DWT_CYCCNT;
    uint32_t ticks = us * 168;  /* 168 cycles/µs at 168MHz */
    while ((DWT_CYCCNT - start) < ticks);
}

STM32F407 Tutorial Series — Tutorial 20: Startup & Reset  | ← Memory Map | Low Power →

Bare-metal register-level programming  ·  No HAL library  ·  STM32F407VGT6

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

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