UART, RS-232 & RS-485
The UART is the simplest and most universal serial interface — used for debug consoles, GPS modules, Bluetooth/Wi-Fi modems and industrial Modbus networks. Learn asynchronous framing (start, data, parity, stop), baud-rate generation and error, oversampling, flow control, error flags, DMA/interrupt reception, and the physical layers RS-232 (point-to-point, ±V levels) and RS-485 (differential, multi-drop, long distance).
UART (Universal Asynchronous Receiver/Transmitter) sends bytes one bit at a time over a single wire without a shared clock. Both sides agree on a baud rate in advance; the receiver re-synchronises on every start bit and samples each bit in the middle.
The UART itself produces logic-level signals (0/3.3 V). To travel further or survive industrial noise, a transceiver converts them to RS-232 (±5 to ±15 V, up to ~15 m) or RS-485 (differential pair, up to 1200 m and 32+ devices on one bus).
An analogy
UART is like two people tapping Morse code at an agreed speed with no metronome between them. The 'start' tap tells the listener to start counting; as long as their speeds match within a few percent, every symbol is read correctly.
The UART frame
The line idles high. A frame starts with a low start bit, then 5–9 data bits sent least-significant bit first, an optional parity bit, and one or two high stop bits. Common settings are written like '115200 8N1'. TX of one device connects to RX of the other (crossed), with a common ground.
Baud accuracy and oversampling
The receiver samples each bit (typically 16 times) and takes the middle samples. Because it resynchronises only at the start bit, clock mismatch accumulates over the 10 bits of a frame; by the stop bit the sampling point must still be inside the bit. That is why the combined error of both devices must stay within ~2–3%, and why internal RC oscillators can cause intermittent garbage at high baud rates.
Errors, flow control and buffering
Hardware flags tell you what went wrong:
- Framing error: stop bit read as 0 — baud mismatch or noise
- Parity error: parity bit disagrees with data
- Overrun: new byte arrived before the previous one was read — use interrupts/DMA/FIFO
- Noise flag: inconsistent samples within a bit
- Flow control: RTS/CTS hardware lines or XON/XOFF characters stop the sender when buffers are full
RS-232 and RS-485 physical layers
RS-232 uses inverted bipolar levels (logic 1 = −3…−15 V) via a transceiver like the MAX3232; it is point-to-point with a ~15 m practical limit. RS-485 sends each bit as the voltage difference between A and B (≥ ±200 mV at the receiver), rejecting common-mode noise; it runs half-duplex on one twisted pair with 120 Ω termination at both ends and failsafe biasing resistors. A DE (driver enable) pin switches each node between talking and listening — timing DE correctly is the classic RS-485 firmware bug. Modbus RTU is the most common protocol on top.
Formulas
- Baud divisor (16× oversampling):
USARTDIV = f_PCLK / (16 × baud); BRR = round(f_PCLK / baud) - Actual baud and error:
baud_actual = f_PCLK / BRR; error = (actual − target) / target - Error budget:
Total mismatch (both ends) ≲ ±2 % … ±3.75 % (8N1, depends on sampling) - Throughput:
Bytes/s = baud / bits per frame (8N1: 10 bits → 115 200 Bd = 11 520 B/s) - Bit time:
t_bit = 1 / baud (115 200 Bd → 8.68 µs) - RS-485 termination:
R_T ≈ Z₀ ≈ 120 Ω at each end of the bus
Worked example: Throughput
Given: 115 200 Bd, 8N1
Find: Maximum bytes per second.
- 10 bits per byte
- 115 200 / 10
Answer: 11 520 B/s (≈ 86.8 µs per byte)
Worked example: Divisor
Given: f_PCLK = 16 MHz, 115 200 Bd, 16× oversampling
Find: BRR and error.
- 16 MHz / 115 200 = 138.9 → 139 = 0x8B
- Actual = 16 MHz / 139 = 115 108 Bd
Answer: BRR = 0x8B, error −0.08%
Worked example: Parity
Given: Byte 0x07, even parity
Find: Parity bit.
- 0x07 = 0000 0111 → three 1s
- Even parity makes total even
Answer: Parity bit = 1
Baud-rate divisor, error and parity (host-testable C)
#include <stdio.h>
#include <stdint.h>
static uint32_t uart_brr(uint32_t fclk, uint32_t baud)
{
return (fclk + baud / 2u) / baud; /* rounded fclk/baud (16x OVS) */
}
static uint8_t even_parity(uint8_t x) /* 1 if odd number of ones */
{
x ^= x >> 4; x ^= x >> 2; x ^= x >> 1;
return x & 1u;
}
int main(void)
{
const uint32_t fclk = 16000000u, rates[] = { 9600u, 115200u, 921600u };
for (int i = 0; i < 3; i++) {
uint32_t d = uart_brr(fclk, rates[i]);
double act = (double)fclk / d;
printf("%7lu baud: BRR=0x%04lX (mant %lu, frac %lu) actual %9.1f err %+.2f%%\n",
(unsigned long)rates[i], (unsigned long)d, (unsigned long)(d >> 4),
(unsigned long)(d & 15u), act, 100.0 * (act - rates[i]) / rates[i]);
}
printf("even parity of 0x55 = %u\n", even_parity(0x55));
printf("even parity of 0x07 = %u\n", even_parity(0x07));
return 0;
}
/* Output:
9600 baud: BRR=0x0683 (mant 104, frac 3) actual 9598.1 err -0.02%
115200 baud: BRR=0x008B (mant 8, frac 11) actual 115107.9 err -0.08%
921600 baud: BRR=0x0011 (mant 1, frac 1) actual 941176.5 err +2.12%
even parity of 0x55 = 0
even parity of 0x07 = 1 */At 921 600 Bd a 16 MHz clock gives +2.12% error — too much for reliable links. Pick a UART clock that divides well (e.g. 14.7456 MHz or a higher PCLK) or use 8× oversampling/fractional dividers.
Reception with DMA + idle-line interrupt (STM32 HAL)
static uint8_t rx_dma[256];
void uart_start_rx(void)
{
/* DMA fills rx_dma in circular mode; callback fires on idle line,
half-transfer and transfer-complete */
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rx_dma, sizeof rx_dma);
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *h, uint16_t pos)
{
static uint16_t last;
if (h != &huart2) return;
if (pos != last) {
if (pos > last) process(&rx_dma[last], pos - last);
else { process(&rx_dma[last], sizeof rx_dma - last); process(rx_dma, pos); }
last = pos;
}
}DMA moves bytes with zero CPU load; the idle-line event marks the end of a message of unknown length — ideal for GPS NMEA sentences or Modbus frames.
Common mistakes
- Wrong: Connecting TX to TX. — Right: Cross over: TX → RX, RX → TX, and share ground.
- Wrong: Connecting RS-232 levels directly to an MCU pin. — Right: Use a level transceiver (MAX3232); ±12 V destroys the pin.
- Wrong: Releasing RS-485 DE before the last stop bit leaves. — Right: Wait for the transmission-complete (TC) flag, not TXE.
- Wrong: No termination on a long RS-485 bus. — Right: 120 Ω at both ends; failsafe bias resistors.
Applications
- Debug console: printf/logging over a USB-UART bridge.
- Modules: GPS, cellular, BLE/Wi-Fi modules with AT commands.
- Industrial: Modbus RTU over RS-485 for meters, drives, PLCs.
- Bootloaders: ROM bootloaders flash firmware over UART.
Key takeaways
- Frame: start, LSB-first data, parity, stop.
- Keep total baud error within ~2%.
- Use interrupts/DMA to avoid overruns.
- RS-232 point-to-point; RS-485 differential multi-drop with 120 Ω termination.
Going further
- LIN: Single-wire UART-based automotive sub-bus with a master schedule.
- Auto-baud detection: Measuring the first character to set the rate.
- Galvanic isolation: Isolated RS-485 transceivers for ground-loop immunity.