Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
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.
01 Reset Sequence Overview
When the STM32F407 is powered on or reset, the ARM Cortex-M4 core follows a deterministic startup sequence:
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 */
};
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))();
}
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
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:
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 */
}
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)
06 STM32F407 Clock Tree
07 Key RCC Registers
| Offset | Register | Key Bits |
|---|---|---|
| 0x00 | RCC_CR | HSION(0), HSIRDY(1), HSEON(16), HSERDY(17), PLLON(24), PLLRDY(25) |
| 0x04 | RCC_PLLCFGR | PLLM[5:0], PLLN[14:6], PLLP[17:16], PLLSRC(22), PLLQ[27:24] |
| 0x08 | RCC_CFGR | SW[1:0]=clock select, SWS[3:2]=status, HPRE[7:4]=AHB, PPRE1[12:10]=APB1, PPRE2[15:13]=APB2 |
| 0x0C | RCC_CIR | Clock interrupts (READY flags) |
| 0x30 | RCC_AHB1ENR | GPIOAEN(0)..GPIOGEN(6), DMA1EN(21), DMA2EN(22) |
| 0x40 | RCC_APB1ENR | TIM2EN(0)..TIM7EN(5), SPI2EN(14), SPI3EN(15), USART2EN(17), I2C1EN(21), DACEN(29) |
| 0x44 | RCC_APB2ENR | TIM1EN(0), USART1EN(4), ADC1EN(8), SPI1EN(12), SYSCFGEN(14) |
08 PLL Configuration Details
The PLL is configured via RCC_PLLCFGR. For 168 MHz from 8 MHz HSE:
| Field | Bits | Value | Effect |
|---|---|---|---|
| PLLM | [5:0] | 8 | HSE ÷ 8 = 1 MHz (VCO input; must be 1–2 MHz) |
| PLLN | [14:6] | 336 | VCO = 1 × 336 = 336 MHz (must be 192–432 MHz) |
| PLLP | [17:16] | 00 = ÷2 | SYSCLK = 336 ÷ 2 = 168 MHz |
| PLLSRC | [22] | 1 = HSE | PLL input from HSE (not HSI) |
| PLLQ | [27:24] | 7 | USB clock = 336 ÷ 7 = 48 MHz |
PLLCFGR value = (8) | (336 << 6) | (0 << 16) | (1 << 22) | (7 << 24) = 0x07405408
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 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) */
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 & Common Startup Pitfalls
- 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.VOSmust 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
HSERDYand 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