STM32F401 UART printing garbage after switching to 84 MHz PLL — same 115200 baud in CubeMX and PuTTY
Asked by stale_biscuit_03 ·
Got a WeAct Black Pill (STM32F401CCU6) project that's been running happily on the default HSI clock at 16 MHz. Using USART1 on PA9/PA10 through a CP2102 USB-to-serial adapter for printf debugging — worked perfectly for weeks.
Today I finally bumped the clock up to 84 MHz so I could run the ADC faster. Enabled HSE (the board has a 25 MHz crystal), set up the PLL in CubeMX with M=25, N=336, P=4 to get 84 MHz SYSCLK. APB2 at no prescaler (84 MHz), APB1 at /2 (42 MHz). The CubeMX clock tree view shows USART1 getting 84 MHz and says the baud rate is still 115200. Regenerated code.
Problem: UART output is now completely garbled. PuTTY at 115200 shows random characters and question marks. I've tried every standard baud rate — 9600, 57600, 230400, 921600 — nothing gives me readable output at any of them.
One thing I should mention: I have my SystemClock_Config() inside a USER CODE block because I customised it a while back with a snippet I found on a forum for Black Pill PLL setup. I assumed CubeMX would still correctly update the peripheral init code as long as I called SystemClock_Config() before MX_USART1_UART_Init() in main(). Is that assumption wrong?
The old binary from before I touched the clock tree still prints correctly on the same hardware, so the wiring and adapter are fine.
3 Replies
Baud rate, parity, stop bits — I'd normally work through all of those, but you've already ruled them out by confirming the old binary still works on the same hardware. This one's a SystemCoreClockUpdate() omission, and I see it constantly whenever someone writes their own SystemClock_Config().
Your UART hardware is transmitting at roughly 604 kbaud right now, not 115200. That's why no baud rate you try works — the actual rate isn't any standard value.
What went wrong
The HAL calculates the baud rate divisor register (BRR) by calling
HAL_RCC_GetPCLK2Freq(), which returns SystemCoreClock divided by the APB2 prescaler
from the RCC registers. SystemCoreClock is a global variable that starts at 16,000,000
at device reset (HSI default) and is only updated to reflect a new clock configuration when
SystemCoreClockUpdate() is explicitly called.
The CubeMX-generated SystemClock_Config() always ends with a SystemCoreClockUpdate()
call. The forum snippet you pasted almost certainly left it out — it's a common omission in
tutorial code. So your PLL hardware is correctly producing 84 MHz, APB2 is running at
84 MHz, but SystemCoreClock still reads 16,000,000. When MX_USART1_UART_Init() runs,
it asks the HAL for the APB2 frequency, gets 16 MHz, and writes BRR = 139 to
USART1->BRR. The real APB2 clock is 84 MHz, so the peripheral divides that by 139 and
transmits at 84,000,000 ÷ 139 ≈ 604,000 baud.
The fix
At the end of your SystemClock_Config() — after the RCC configuration is complete and
before the function returns — add:
SystemCoreClockUpdate();
That function reads the RCC configuration registers and recalculates SystemCoreClock to
match what the hardware is actually doing. Every subsequent peripheral init that calls
HAL_RCC_GetPCLK1Freq() or HAL_RCC_GetPCLK2Freq() will then get the right base
frequency. After rebuilding, USART1->BRR should read 729 (0x2D9), which gives
84,000,000 ÷ 729 ≈ 115,226 baud — well within the ±2% framing tolerance. Your printf
output will come straight back.
Quick verify
Before you rebuild, confirm the diagnosis in a debug session: add SystemCoreClock to
your watch window and break right before MX_USART1_UART_Init() is called. If it reads
16,000,000 instead of 84,000,000, that's your problem. Add the missing call, rebuild, and
check that SystemCoreClock is now correct before the UART init runs.
The UART explainer covers why even a small baud rate error compounds across a frame and produces garbled output — worth a read if you want to understand the ±2% tolerance. The STM32 clock tree explainer walks through the full prescaler chain from HSE to each peripheral bus if you want to make sure the rest of your clock config is solid.
To make the clock path concrete, here's what your STM32F401 configuration produces:
HSE 25 MHz
│
▼ PLL (M=25, N=336, P=4)
│ VCO input = 25 MHz ÷ 25 = 1 MHz
│ VCO output = 1 MHz × 336 = 336 MHz
│ PLLCLK = 336 MHz ÷ 4 = 84 MHz
▼
SYSCLK = 84 MHz
│
▼ AHB prescaler ÷1
HCLK = 84 MHz (CPU core, DMA, Flash)
│
├──▶ APB2 prescaler ÷1 → PCLK2 = 84 MHz → USART1, USART6, SPI1, ADC1/2/3
│
└──▶ APB1 prescaler ÷2 → PCLK1 = 42 MHz → USART2, SPI2/3, I2C1/2/3, TIM2–5
USART1 (and USART6) are on APB2, which on the F401 can run at full SYSCLK speed. USART2 is on APB1, capped at 42 MHz — that's a silicon limit on this device, not a CubeMX choice.
The key point here: both HAL_RCC_GetPCLK1Freq() and HAL_RCC_GetPCLK2Freq() read the
APB prescaler settings from the RCC_CFGR register accurately, but then multiply by
SystemCoreClock — or more precisely, they divide SystemCoreClock by the AHB prescaler
to get HCLK first, then divide by the APB prescaler. If SystemCoreClock is stale, the
error propagates all the way to the BRR calculation regardless of which UART you're using.
One thing worth noting while you're in there: if you ever need to move to USART2 for longer cable runs (RS-232 or RS-485 link), remember it draws from the 42 MHz APB1 clock. For anything above about 2 Mbaud you need to account for that. At 115200 it makes no practical difference.
Before you even add the fix, read USART1->BRR in a live debug session. It should be 0x2D9 (729) for 115200 baud at an 84 MHz APB2 clock. If it reads 0x8B (139), that's the 16 MHz BRR — confirms the diagnosis without touching code.
After adding SystemCoreClockUpdate() and rebuilding, check that same register again. If it now shows 0x2D9 and your output is still garbled, the problem is elsewhere — wrong GPIO alternate function mapping for PA9, TX/RX crossed somewhere, or a voltage level issue between the MCU and your CP2102.
Side note on OVER8: if you ever need to push baud rates above about 5 Mbaud on a high
clock part, the OverSampling field in the HAL init struct switches from 16x to 8x
sampling, which halves the minimum divisor. BRR calculation changes in that mode. At
115200 on 84 MHz it's irrelevant, but it's in huart1.Init.OverSampling if you ever
need it.
Related Discussions
SPI reads all returning 0xFF — logic analyser shows MISO activity, W25Q32 not responding to commands
Been staring at this one for a day and a half. I'm trying to read the JEDEC ID from a W25Q32JV SPI flash chip on a custom STM32L432 board. T
I2C bus scan finding nothing — NACK on every address despite pull-ups
Working through my first proper I2C project — hooking up a BME280 temp/humidity sensor to an ESP32 devkit. Wired SDA to GPIO21 and SCL to GP
STM32H743 HAL_UART_Receive_DMA fires error callback immediately — TEIF1 set, RxCplt never fires
Upgrading a project from STM32F4 to STM32H743. UART DMA receive worked on the F4 without any issues — standard CubeMX setup, call HAL_UART_R
STM32 GPIO interrupt configured but ISR never fires — what am I missing?
Trying to use a button on PA0 to trigger an interrupt on an STM32F411 Nucleo board. Using HAL, generated the init code with CubeMX. The GPIO