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:

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

Worked example: Throughput

Given: 115 200 Bd, 8N1

Find: Maximum bytes per second.

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.

Answer: BRR = 0x8B, error −0.08%

Worked example: Parity

Given: Byte 0x07, even parity

Find: Parity bit.

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

Applications

Key takeaways

Going further