serial-bench / UART / msg #6
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.
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.
| Reference | 9600 | 38400 | 57600 | 115200 |
|---|---|---|---|---|
| 8 MHz | +0.16 % | +0.16 % | −3.55 % | +8.51 % |
| 11.0592 MHz | 0.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.
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.
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.
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.
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.