uart stm32 avr debugging serial

Why Your UART Isn't Working: Baud Rate Errors Explained

· Bob Peters

You wire up a UART, flash your firmware, open a terminal, and get garbage: “~’æñ. Or you get nothing. Or you get the first byte right and then chaos.

Ninety percent of the time it’s the baud rate.

What actually causes UART errors

UART is asynchronous — there is no clock line. Both sides agree on a baud rate in advance and count bit times using their local clocks. If those clocks disagree by more than about 2%, the receiver samples bits in the wrong positions.

The tolerance budget: the receiver samples each bit near its center. With a 2% rate difference, a 10-bit frame (start + 8 data + stop) accumulates 0.2 bits of drift. By the stop bit, the sample point has shifted 20% of a bit period. That’s exactly the I2C-style marginal case — it works sometimes, fails on others, and is nearly impossible to debug without a scope.

The clock tree is the root cause

On STM32, USART1 sits on APB2. USART2 and USART3 sit on APB1. If your system clock is 168 MHz but you have APB1 prescaler = 4, then APB1 runs at 42 MHz — not 168 MHz. Write 168000000 into the BRR formula when the actual USART clock is 42 MHz and you get a baud rate that’s 4× off.

CubeMX shows you the clock tree visually. In code, check your RCC prescaler configuration. HAL_RCC_GetPCLK1Freq() and HAL_RCC_GetPCLK2Freq() return the actual peripheral clock values — use these as the numerator in your BRR calculation, not SystemCoreClock.

// Wrong — may use the wrong peripheral clock
USART2->BRR = SystemCoreClock / 115200;

// Correct
USART2->BRR = HAL_RCC_GetPCLK1Freq() / 115200;

The AVR -1 trap

On ATmega, the UBRR formula is:

UBRR = F_CPU / (16 × baud) - 1

The -1 is because the counter counts down to 0. Forget it and your actual baud rate is slightly off. At 9600 baud and 16 MHz: the correct UBRR is 103, not 104. That’s a 0.96% error — fine. At 115200 baud and 8 MHz: UBRR = 3 gives 8.5% error. That’s why the Arduino Wire library recommends avoiding 115200 on an 8 MHz Pro Mini.

Use the double-speed mode (set U2X0 bit) at high baud rates on slow clocks:

// Double speed mode
UCSR0A |= (1 << U2X0);
UBRR0 = F_CPU / (8UL * 115200) - 1;

nRF52: don’t calculate, look up

The nRF52 UARTE’s BAUDRATE register is not a divider. It’s a lookup table. Write 0x01D7E000 for 115200. Write a calculated value and you’ll get the wrong speed.

The Nordic SDK defines NRF_UARTE_BAUDRATE_115200, NRF_UARTE_BAUDRATE_921600, etc. in nrfx_uarte.h. Use those. If you need an unusual baud rate that isn’t in the list, the nRF52 can’t do it accurately — the UART peripheral has no fractional divider.

How to measure actual baud rate

Connect a logic analyser (even a cheap 8-channel LA at €10 works) and decode UART. Most analysers show you the measured bit period — compare it to your target.

Alternatively, send a known byte like 0x55 (alternating bits: 01010101). On a scope, measure the bit period directly. 0x55 at 115200 gives a bit period of 8.68 µs. If you see 9.5 µs, your actual rate is ~105 kBaud — roughly 9% off.

The 2% rule

The I2C spec defines ±2% tolerance per side, meaning total differential error must stay under ~4%. For UART, the rule of thumb is: keep each side’s baud rate within 2% of nominal to allow headroom for both errors to align worst-case.

Use the UART baud rate calculator to verify your register value before flashing. Check the error percentage — anything above 2% is a known failure risk, and above 0.5% is worth flagging on clock-sensitive designs.