We’re Officially Live β€” Lifetime Founding Membership Available for a Limited Time
Free preview

UART

Two wires, no clock, and a pre-agreed baud: the UART frame (start/data/parity/stop), how the receiver recovers timing by oversampling, and an interrupt-driven RX ring buffer.

24 min read

In this lesson:


The async workhorse

UART (Universal Asynchronous Receiver/Transmitter) is the go-to for debug logs and simple chip-to-chip serial. It's asynchronous (no clock line, previous lesson): the two ends just agree on a baud rate beforehand and send framed bytes over a single data line each way.

A UART line idles high. To send a byte, the transmitter frames it:

   idle    start   d0  d1  d2  d3  d4  d5  d6  d7   [par]  stop   idle
  β–”β–”β–”β–”β–”β–”β•²______β•±β–”β–”β•²__β•±β–”β–”β–”β–”β•²________________β•±β–”β–”β–”β–”β–”β–”β–”
        β”‚ 0  β”‚ ◀──── 8 data bits, LSB first ────▢ β”‚  β”‚  1  β”‚
        start                                       parity stop

Both ends must be configured identically. The shorthand "8N1" = 8 data bits, No parity, 1 stop bit, the most common setting.


Baud and how the receiver finds the bits

Baud rate is bits per second (e.g., 115200). With no clock line, the receiver can't be told when each bit is, it recovers timing locally:

  1. It oversamples the line (commonly 16Γ— the baud rate).
  2. On the falling start-bit edge, it starts counting.
  3. It samples each bit near its center (most stable point), one bit-time apart.
oversample ticks: β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚  (16 per bit)
data:        β–”β–”β–”β•²________________  start bit
                  β–² sample at center (tick 8), then every 16 ticks

This is why both ends need accurate, matched baud: the receiver free-runs its sampling from the start edge across the whole frame. If the clocks differ by more than ~2-3%, by the last bit the sample point has drifted off the bit and you read garbage (the baud-mismatch lesson quantifies this).


Wiring

UART is full-duplex, separate TX and RX lines. The catch: TX connects to the other side's RX (crossed), and both devices must share a common ground.

   MCU A                 MCU B
   TX  ───────────────▢  RX
   RX  ◀───────────────  TX
   GND ─────────────────  GND   (mandatory common reference)

Connecting TX→TX is the classic first-time mistake, nothing works.


A driver: transmit, and an interrupt-driven RX ring buffer

Transmit by writing the data register when it's empty:

void uart_putc(char c) {
    while (!(USARTx->SR & USART_SR_TXE)) { }  // wait until TX data register empty
    USARTx->DR = (uint8_t)c;                   // writing DR sends the byte
}

Receive with an interrupt feeding a ring buffer (the DSA-module structure) so bytes are captured the instant they arrive and the main loop consumes them at its leisure:

static volatile uint8_t rx_buf[64];
static volatile uint8_t head, tail;        // SPSC ring buffer (ISR=producer, main=consumer)
 
void USARTx_IRQHandler(void) {
    if (USARTx->SR & USART_SR_RXNE) {       // a byte arrived
        uint8_t b = (uint8_t)USARTx->DR;    // reading DR clears RXNE
        uint8_t next = (head + 1) & 63;     // power-of-two wrap
        if (next != tail) {                 // drop if full (or overwrite)
            rx_buf[head] = b;
            head = next;
        }
    }
}
 
bool uart_getc(uint8_t *out) {              // main loop polls this
    if (tail == head) return false;         // empty
    *out = rx_buf[tail];
    tail = (tail + 1) & 63;
    return true;
}

This is the bread-and-butter pattern: the ISR is tiny (grab byte, enqueue), and the main loop processes a frame when it's ready. For high rates, DMA replaces the per-byte interrupt.


Gotchas


TL;DR

This is just the start

Sign up free to track progress on this lesson, get coding problems with the Run button, and unlock the full interview Q&A.

More in Communication Protocols

Serial vs Parallel, Sync vs AsyncSPI
I2C
Choosing UART, SPI, or I2C
CAN Bus
Common Bus Bugs