serial-bench one bench, five serial buses - read-only archive

serial-bench / UART / msg #6

Two boards agreed at 9600 and argued at 115200

From
Ivo Kallas <ivo@rawsocket.cloud>
Date
Thu, 11 Jun 2026 09:00:00 +0300
Subject
Two boards agreed at 9600 and argued at 115200
Message-ID
<20260611.0900.uartbaud@rawsocket.cloud>
List-Id
serial-bench.rawsocket.cloud
X-Bus
UART
X-Topic
UART, 8-N-1 · 12 MHz against 11.0592 MHz

The link was four wires between a sensor board and a small controller. At 9600 it had been flawless for a year. Somebody raised it to 115200 to shorten a polling loop, and roughly one byte in six came back wrong - but not randomly wrong, and that was the clue.

Both ends claimed 115200 in their configuration. Neither was lying; they simply could not both produce it. One board is clocked from a 12 MHz crystal, the other from 11.0592 MHz, and that second number exists in the world for exactly this reason.

Where the error comes from

A classic UART divides its reference by sixteen for oversampling and then by an integer N. So the achievable rates are f / (16 × N) and nothing in between. Pick the nearest integer and you inherit whatever rounding error that leaves. Here is the whole table I now keep taped inside the lid of my parts box.

Divider error for a 16× oversampling UART, N rounded to nearest integer
Reference96003840057600115200
8 MHz+0.16 %+0.16 %−3.55 %+8.51 %
11.0592 MHz0.00 %0.00 %0.00 %0.00 %
12 MHz+0.16 %−2.34 %+0.16 %−7.00 %
16 MHz+0.16 %+0.16 %+2.12 %−3.55 %

Read the 115200 column and the mystery evaporates. The 11.0592 MHz side is exact, because 11.0592 MHz is 115200 × 96. The 12 MHz side wants N = 6.51, rounds to 7, and ends up transmitting 107 143 bit/s while insisting it is at 115 200. At 9600 the same crystal is off by 0.16 %, which is nothing.

The half-bit budget

A receiver finds the falling start edge, waits half a bit cell, then samples every bit cell after that. In an 8-N-1 frame the stop bit is sampled 9.5 cells after that edge. If the two clocks differ by e, the sampling point has slid by 9.5 × e cells by the time it reaches the stop bit, and once that slide exceeds half a cell the receiver is sampling the neighbouring bit.

So the hard ceiling is 0.5 / 9.5 = 5.26 % for the two clocks combined, and in practice I budget ±2 % per side. Part of the remaining margin is spent on something people forget: the start edge itself is only located to within one of the sixteen oversampling ticks, which is 6.25 % of a bit cell before any clock error at all.

Our pair was 7.00 % apart. Dividing: 0.5 / 0.07 ≈ 7.1 bit cells, so the sampling point should cross into the wrong cell somewhere around bit 7 of the frame. Not at bit 1, not uniformly - around bit 7.

sent sampled stb0b1 b2b3b4 b5b6b7 sp b6 sampled on the cell boundary stop bit sampled inside b7 drift per cell 7.0 % · cumulative 0.71 cell at the stop bit · framing error
Fig. 2 - the drift is not noise, it is a ramp. Early bits are sampled near their centres and the last two are not, which is why the byte value decides whether you notice.

Why the failures looked structured

I sent all 256 byte values in a loop, ten thousand frames, and counted which ones came back intact. Bytes with ones in the top bits mostly survived; bytes with zeros there mostly did not. That follows directly from the picture above. When the stop sample lands inside b7, a b7 of 1 happens to look like a valid stop bit and the byte slips through with only the low bits affected. A b7 of 0 produces a framing error and the receiver reports it honestly.

The worst value was 0x00, which failed on essentially every frame. The best was 0xFF, which never failed in the run. I had been staring at a log full of 0x00 and 0x01 and assuming the sensor was returning zeros.

Measurement, with its limits

I captured the transmitted line with the logic analyser at 24 MS/s, which quantises edges to 41.7 ns. Over 64 frames the mean bit cell on the 12 MHz board came out at 9.33 µs ± 0.04 µs against a nominal 8.68 µs. The ratio is 1.0749, so the measured error is +7.5 % rather than the predicted 7.0 %. I am not going to pretend that discrepancy is resolved: it is larger than the quantisation alone explains, and crystal pulling on an unloaded board is the obvious suspect, but I did not have a calibrated reference on the bench that day to prove it.

What we changed

Not the crystal. The link carries about forty bytes a second, so the step up to 115200 bought nothing real; we settled it at 57600, where 12 MHz is off by +0.16 % and 11.0592 MHz is exact, and the combined error is two orders of magnitude inside the budget. Where the throughput is genuinely needed, the right move is to clock both ends from a reference that divides cleanly, or to use a part whose baud generator has a fractional divider instead of a plain integer one.

The general rule I now apply before anybody raises a baud rate: compute f / (16 × round(f / (16 × baud))) for both ends, add the magnitudes of the two errors, and refuse anything above 4 %. It takes one line in a calculator and saves an afternoon.