STM32 Bare-Metal Bring-Up
A staged STM32 bring-up flow from clocks and reset through one observable peripheral at a time.
Bare-metal bring-up is less about writing a complete application and more about proving that the chip, board, clock tree, memory map, debugger, and first peripherals match reality. A small, boring sequence catches most failures before the firmware has enough moving parts to hide them.
Start from reset facts
Record the exact part number, board revision, package, debugger, power source, and expected clock source. Check the reference manual, datasheet, errata, and schematic before writing code. For STM32 parts, the first questions are usually:
- which oscillator is populated and enabled,
- which pins are already claimed by ST-Link, boot straps, LEDs, buttons, or serial bridges,
- which voltage range and flash wait-state table apply,
- which alternate-function numbers map each peripheral signal,
- whether the board powers the target, the debugger, or both.
Do not assume the Nucleo board pin label is the MCU pin. Keep a table that maps connector label, MCU pin, peripheral, alternate function, and electrical constraint.
Bring up the minimum image
The first image should do three things only:
- install the vector table,
- initialize
.dataand.bss, - toggle one known GPIO.
Use the default internal clock first unless the external clock is the thing under test. Flash wait states, PLLs, prescalers, and USB clocks can wait until a known-good image runs and the debugger can halt and inspect memory.
A useful first GPIO test is slow enough to see on a scope or LED, and simple enough to audit from the disassembly. If it fails, the fault domain is small: power, reset, debug, linker script, startup code, clock enable, GPIO mode, or the pin mapping.
Prove the clock tree deliberately
After the minimal image works, move to the target clock plan in small steps:
- enable the oscillator,
- wait for ready with a timeout,
- program PLL factors while the PLL is off,
- set flash latency before increasing system clock,
- switch the system clock,
- verify
SystemCoreClockor equivalent against a measured output.
Route a timer output, MCO pin, UART baud, or periodic GPIO edge to measurement. A firmware constant is not evidence that the clock is correct. If UART output is garbled after a PLL change, suspect APB prescalers and peripheral clock selection before suspecting the UART driver.
Add one peripheral at a time
A good sequence is:
- GPIO output,
- timer delay or PWM,
- UART TX only,
- UART RX interrupt,
- SysTick or monotonic timer,
- I2C or SPI with a known device ID read,
- ADC or DMA,
- application tasks or async runtime.
Each step needs an observable result and a rollback point. For UART, first transmit a constant byte pattern at a known baud. For I2C, scan less and read one documented register more. For SPI, capture clock polarity, phase, chip select, and first command on a logic analyzer.
Keep fault domains narrow
Most bring-up bugs come from a small set of causes:
| Symptom | First checks |
|---|---|
| debugger will not attach | target voltage, reset held low, boot mode, SWD pins reused, clock failure |
| LED does not toggle | RCC enable, GPIO mode, wrong port, wrong board pin, linker/startup fault |
| UART baud wrong | system clock, APB prescaler, oversampling, alternate function |
| I2C bus stuck | pull-ups, address, open-drain mode, peripheral reset, device power |
| random reset | watchdog, brownout, stack collision, hard fault, clock security |
| hard fault on peripheral access | missing clock enable, wrong base address, unaligned access, MPU |
On Cortex-M, install a hard-fault handler that preserves stacked PC, LR, xPSR, CFSR, HFSR, BFAR, and MMFAR. A reset loop without fault registers is lost evidence.
Make the board observable
Minimum useful instrumentation:
- one serial console or RTT channel,
- one spare GPIO for timing markers,
- debugger attach under reset,
- rail and reset test points,
- a way to identify firmware version and build configuration,
- a panic or fault path that stops in a debugger before reset when possible.
A firmware banner should include board revision, git revision when available, clock configuration, reset cause, and boot mode. That makes later logs useful.
Cut over to production patterns late
Do not introduce an RTOS, async executor, DMA graph, logging framework, or power manager until basic hardware has been proven. These are useful patterns, but they widen the fault domain. Bring up the board in simple polling code, then replace pieces with the final architecture one subsystem at a time.
Release evidence
A credible bring-up record includes:
- schematic revision and board serial,
- debugger and toolchain versions,
- exact firmware image,
- clock configuration and measured timing evidence,
- rail, reset, and boot observations,
- per-peripheral proof such as UART capture, I2C register read, SPI logic trace, or PWM measurement,
- faults found, rework applied, and remaining deviations.
Related notes
PCB Design and Bring-Up, Measurement and Instrumentation, and Interrupts, DMA, and Ring Buffers.
Sources
- STMicroelectronics, STM32F411 reference manual RM0383.
- STMicroelectronics, STM32F411xC/E datasheet.
- Arm, Cortex-M4 Devices Generic User Guide.