Electronics Design AU
CommunicationsSTM32Solved

STM32F401 UART printing garbage after switching to 84 MHz PLL — same 115200 baud in CubeMX and PuTTY

5 min read3 replies
Original Question

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.

From the knowledge baseWhat Is UART (Universal Asynchronous Receiver-Transmitter)?

3 Replies

uart_undertaker
Accepted Answer

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.

clock_tree_chaos

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.

soggy_waffle42

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