All work

Own project

2024 · STM32F429I-DISC1 · Embedded C

Clock tree & peripherals from first principles

Taking an ARM Cortex-M4 to its maximum rated frequency with nothing but the reference manual and direct register writes - and then proving on a logic analyser that it actually got there.

Year
2024
Target
STM32F429ZI
From
16 MHz HSI
To
180 MHz SYSCLK
Abstraction
none - registers only
HSE 8 MHz ÷ M 8 PLL × N = 360 ÷ P 2 SYSCLK 180 MHz AHB ÷ 1 HCLK 180 MHz GPIO · DMA · SDRAM APB1 ÷ 4 45 MHz I2C · TIM2-7 · UART APB2 ÷ 2 90 MHz SPI · USART1 · ADC written from the reference manual verified on ST-Link + logic analyser
HSE through the PLL to a 180 MHz core, then down the AHB and APB prescalers.

01Why start at the clock

Every peripheral on an STM32 is downstream of the clock tree, and almost nothing tells you when the tree is wrong. A misconfigured PLL does not raise a fault - it just makes your UART baud rate wrong, your SysTick delays the wrong length, and your SPI too fast for the panel you are talking to. The symptoms all appear somewhere else.

So this project was deliberately the boring one: bring an ARM Cortex-M4 from its internal 16 MHz startup oscillator to its rated 180 MHz, using nothing but the reference manual and direct register writes. No HAL, no CubeMX generation, no middleware. If you have never done it by hand, the abstraction that normally does it for you is a black box - and black boxes are exactly what you cannot debug at three in the morning.

02The tree, locking in order

The sequence matters as much as the values. You cannot switch the system clock to the PLL before the PLL is locked, and you cannot run flash at 180 MHz with the wait states left at their reset value. Get the order wrong and the core either stalls or executes garbage.

Bring-up sequence

Each stage locks in the order the firmware performs it. Wave density is proportional to the real frequency.

HSE crystalexternal 8 MHz oscillator
0MHz
PLL/M 8 · xN 360 · /P 2
0MHz
SYSCLK → AHBprescaler /1
0MHz
APB2prescaler /2 - high-speed bus
0MHz
APB1prescaler /4 - low-speed bus
0MHz
SysTickreload 180000 - 1 ms tick
0Hz

One 8 MHz crystal multiplied to 180 MHz, then divided back down for the peripheral buses - 45 MHz on APB1, 90 MHz on APB2 - and finally down to a 1 ms scheduling tick.

The step that catches people is flash latency. At 180 MHz with 3.3 V supply the flash needs 5 wait states, and it must be set before the switch to the faster clock, not after. Raise the clock first and the core starts fetching instructions the flash cannot deliver yet.

03What the registers actually do

StepRegisterWhat it does
1RCC_CREnable HSE, spin until HSERDY is set. A crystal takes milliseconds to stabilise; using it early gives you a clock that drifts as it warms.
2PWR_CRSelect the voltage scaling mode that permits 180 MHz. Below it the part is not rated for the frequency.
3FLASH_ACRSet 5 wait states and enable the prefetch, instruction and data caches - before the clock rises.
4RCC_PLLCFGRM = 8, N = 360, P = 2, and the HSE selected as PLL source. 8 / 8 x 360 / 2 = 180 MHz.
5RCC_CREnable the PLL, spin until PLLRDY.
6RCC_CFGRAHB /1, APB1 /4, APB2 /2. APB1 has a 45 MHz ceiling - exceed it and peripherals misbehave rather than fault.
7RCC_CFGRSwitch SYSCLK to the PLL and confirm via the SWS status bits that the switch was accepted.
8SysTickReload 180000 - 1 for a 1 ms tick, then enable the counter and its interrupt.

Step 7 is the one people skip. Writing the switch bits is a request; the hardware reports back through a different field whether it took effect. Assume rather than verify, and a silently rejected switch leaves you running at 16 MHz while every timing calculation in the firmware believes otherwise.

04Peripherals, and proving the timing is real

With the tree up, the peripheral drivers were written the same way - straight to the registers, with the datasheet open. GPIO mode, speed and pull configuration; SPI prescalers derived from the APB2 rate rather than guessed; SysTick-based millisecond delays.

The important habit here is refusing to trust the arithmetic. A clock tree that is wrong by a factor of two still runs, still blinks an LED, and still looks entirely healthy from inside the debugger.

  • Toggle a pin in a tight loop and put a logic analyser on it. The measured frequency is the ground truth; the calculated one is a hypothesis.
  • Read the RCC status bits back after every switch rather than assuming the write landed.
  • Check SysTick against a wall clock over a long interval - a 1 ms tick that is really 1.1 ms is invisible over one period and obvious over ten thousand.
  • Inspect register state on the ST-Link after bring-up, comparing each field against the manual rather than against what the code intended to write.

Everything after this project - the Battleship engine, the CAN work, and eventually the arena allocator in Cerberus - assumed a clock tree that was known-correct because it had been measured. That is the whole reason this one exists.