Embedded Systems · Embedded Buses
UART Framing, Baud Rate Error, and the Overrun That Loses Data
How start-bit synchronisation survives clock mismatch, how much baud error a ten-bit frame tolerates, and why the receive FIFO overruns before the interrupt handler runs.
A synchronous bus shares a clock wire, so both ends agree on when a bit is on the line. A UART has no clock wire at all: it carries two data lines, one in each direction, and both ends independently run a divider from their own oscillator. Everything fragile about serial ports follows from that: the frame must carry its own synchronisation, the tolerable rate mismatch is a few percent, and there is no way to ask the sender to slow down once a byte has been lost.
The start bit is the only synchronisation
An idle line is high. A frame begins when the transmitter pulls the line low for one bit period. The receiver, which has been watching for exactly that transition, treats the falling edge as time zero, waits one and a half bit periods, and samples the centre of bit 0, the first data bit. It then samples every subsequent bit period from that point until the stop bit.
Sampling in the centre is the whole trick. If the receiver’s bit period is slightly longer or shorter than the transmitter’s, the sample point drifts within the bit. Centring gives the sample point half a bit period of slack in each direction before it falls off the edge of the bit it intends to read. The drift accumulates across the frame, because the receiver never resynchronises until the next start bit, so the frame length determines how much mismatch can be tolerated.
A ten-bit frame — one start, eight data, one stop — has its last sample taken nine and a half bit periods after the start edge. To stay inside the stop bit, the accumulated drift at that point must be less than half a bit period, so the total mismatch between the two bit periods must be below roughly 5 percent. A practical budget is smaller than that bound, around 2 to 3 percent, because the receiver also needs margin for its own sampling jitter and for noise, and because a frame with a parity bit is eleven bits rather than ten. Two oscillators that are each within 2 percent of nominal can differ by 4 percent in the worst case, which is why crystal-controlled clocks are used rather than RC oscillators whenever the baud rate matters.
Divisor arithmetic and the error it leaves behind
The receiver’s bit period comes from dividing a peripheral clock by an integer. With a 16-times oversampling clock, the divisor is the peripheral clock frequency divided by sixteen times the baud rate. That division almost never lands on an integer, so the achieved rate differs from the requested one by the rounding error, and the error grows as the divisor gets smaller, which is to say at higher baud rates.
A common arrangement is a peripheral clock of 8 MHz and a target of 115200 bit/s. The exact divisor is 8000000 / (16 × 115200), about 4.34. Rounding to 4 gives a rate of 125000, an error near 8.5 percent, which will not work. Rounding to 4 with a fractional divider, or choosing a different peripheral clock, is required. With a 16 MHz clock the divisor is about 8.68, and rounding to 9 gives 111111 bit/s, an error of about 3.6 percent against the target, which is marginal for a ten-bit frame and poor for an eleven-bit one. Selecting a peripheral clock that is a clean multiple of the desired rate is not an optimisation; it is a correctness requirement.
/* divisor with 16x oversampling; the fractional part matters at high rates */
static uint32_t uart_divisor(uint32_t pclk, uint32_t baud)
{
uint32_t div = pclk / (16u * baud); /* integer part */
uint32_t frac = ((pclk % (16u * baud)) * 16u) / (16u * baud);
return (div << 4) | (frac & 0x0Fu); /* register format: DIV in high bits */
}
static uint32_t uart_actual_baud(uint32_t pclk, uint32_t divreg)
{
uint32_t div = divreg >> 4;
uint32_t frac = divreg & 0x0Fu;
/* value 16*div + frac, so the effective divisor is that over 16 */
return pclk / (16u * div + frac);
}
The residual error should be computed at configuration time and checked against the tolerance the frame length allows, rather than assumed to be small. A one percent error at 9600 bit/s and a one percent error at 921600 bit/s are the same relative error and equally harmless, but the divisor arithmetic that produces them is much harder to get right at the higher rate.
Parity, stop bits, and what they can detect
The optional parity bit makes the total number of ones in the data plus parity even or odd, depending on the convention chosen. It detects any odd number of flipped bits and misses any even number, including the common case of two adjacent bits corrupted by a burst. It is a weak check, adequate for detecting a misconfigured or marginal line and inadequate as an integrity guarantee.
Parity does buy one useful thing: the receiver can report a parity error per byte, so a host can distinguish a corrupt byte from a byte that never arrived. Combined with the framing error flag, which is raised when the stop bit is not high at the expected sample point, and the overrun flag, a UART gives three coarse diagnostics for free.
The number of stop bits is a legacy parameter. One stop bit is normal; two were used on mechanical teletypes that needed time to settle. A receiver generally only checks that the line is high at the stop sample point and ignores the rest of the stop period, so a sender configured for two stop bits and a receiver configured for one usually interoperates, while the reverse also works. What does not interoperate is a mismatch in the data width or the parity convention: the receiver then samples a bit that is not the one intended, and every byte in the stream is wrong in a consistent way.
Why the overrun happens
A UART receiver has a shift register that assembles bits at the line rate and a holding register or FIFO that the processor reads. The shift register finishes a byte every ten or eleven bit periods. At 115200 bit/s that is about 87 microseconds. If the byte in the holding register has not been read by the time the next byte finishes shifting, the new byte either overwrites it or is dropped, and the overrun flag is set.
The interrupt path has to complete within that 87 microseconds, and the number that matters is not the interrupt latency alone but the worst-case time from the byte arriving to the byte being read. That includes the interrupt being masked by a higher-priority section, the handler being delayed by a longer critical section elsewhere, and any other interrupt at the same or higher priority. A system that spends 200 microseconds in a critical section will overrun at 115200 bit/s regardless of how fast its interrupt handler is.
Two standard responses exist. The first is a FIFO deep enough to absorb the worst-case latency: a sixteen-byte FIFO buys fifteen extra byte times, which at 115200 bit/s is about 1.3 milliseconds, and the FIFO can be configured to interrupt at a threshold rather than per byte, which reduces interrupt load at the cost of more latency per service. The second is DMA, which moves bytes out of the FIFO without processor involvement, at which point the processor only needs to keep up with blocks rather than bytes. Both convert a hard real-time deadline into a softer one.
The failure mode when neither is present is characteristic. Bytes are lost in bursts, always at the moment the processor is busiest, and the loss is silent unless the overrun flag is checked. Because there is no sequence number in the stream, the receiver cannot tell that a byte is missing; a protocol above the UART has to supply framing, length, and a checksum to detect it. This is the same division of labour that the shift register behind SPI illustrates from the other direction: the bus moves bits, and integrity belongs to a layer above it.
Flow control and the case against it
Hardware flow control uses two extra wires, conventionally named for request-to-send and clear-to-send. The receiver lowers its clear-to-send line when its buffer is nearly full, and a sender that honours the line stops within a byte or two. The protection is real but coarse: the line is a single bit with no granularity, it can only be asserted after the buffer has begun to fill, and a sender that ignores it, or a cable that does not carry it, leaves the problem exactly where it was. Many small devices do not implement it at all.
Software flow control uses in-band characters, conventionally XON and XOFF. It costs no wires and works over any channel that preserves byte order, but it consumes two code points out of the 256 available, so binary payloads that may contain those values cannot be sent without escaping, and the escape itself must be escaped. A flow-control character can also be lost, and then the sender keeps sending into a full buffer with no recovery.
Neither mechanism fixes the underlying asymmetry. A UART has no shared clock, so there is no way to slow the bit rate mid-frame; there is only the option to stop between bytes, and only if the sender agrees to listen.
What a link budget has to include
The electrical layer is separate from all of this. A logic-level UART between two chips on one board is a single-ended signal referenced to a shared ground, and its reach is limited by rise time and noise margin rather than by the protocol. RS-232 inverts the levels and uses a wider swing, roughly plus and minus several volts, which buys noise margin and cable length at the cost of a transceiver at each end and a maximum slew rate that caps the useful rate. RS-485 goes further and makes the pair differential, which is the subject of differential signalling and bus termination.
The protocol layer stays identical across all three, which is why a serial port configuration is described in terms of baud rate, data bits, parity, and stop bits and not in terms of voltages. Getting those four parameters right, and getting the divisor arithmetic right for the first of them, is most of what makes a serial link work on the first attempt.
References
- RS-232-C Interface Between Data Terminal Equipment and Data Communication Equipment — Electronic Industries Association, 1969
- PC16550D Universal Asynchronous Receiver/Transmitter with FIFOs — Texas Instruments datasheet
- Linux kernel serial driver documentation — Linux kernel project