Vaibhav Shende Vaibhav Shende

UART & USART Communication: A Deep Dive for Embedded Engineers

Complete guide to UART and USART serial communication protocols — frame structure, baud rate, synchronization, hardware wiring, and practical implementation tips.

Embedded Systems

UART & USART Communication: A Deep Dive for Embedded Engineers

Introduction

Serial communication is the backbone of embedded systems. Whether you’re connecting a microcontroller to a GPS module, a Bluetooth radio, or another MCU, chances are you’re using UART or USART. Understanding how these protocols work at the bit level separates engineers who debug confidently from those who spend hours staring at a logic analyzer.

This post covers the full picture: hardware signaling, frame structure, baud rate math, synchronization, and common pitfalls.


UART vs USART: What’s the Difference?

FeatureUARTUSART
Full NameUniversal Asynchronous Receiver/TransmitterUniversal Synchronous/Asynchronous Receiver/Transmitter
ClockInternal (no shared clock line)External clock optional (synchronous mode)
Wires (async)2 (TX, RX)2–3 (TX, RX, optionally CLK)
SpeedSlower (depends on baud)Can be faster in synchronous mode
Typical UseSensor modules, debug consolesSTM32 peripherals, SPI-compatible devices

UART is purely asynchronous — both sides agree on a baud rate beforehand and use their own clocks.
USART adds the option of a shared clock line (synchronous mode), which removes the need for baud-rate matching and enables higher reliability at speed.

In practice, most people use USART peripherals in asynchronous mode (making it functionally identical to UART). You’ll see “USART” on STM32 datasheets and “UART” on Arduino/ESP32 — they behave the same way in typical embedded use.


The Physical Layer

Logic Levels

UART transmits one bit at a time on a single wire. The idle state is HIGH (logic 1). A transmission begins by pulling the line LOW (the start bit).

Idle State:

TX ─────────────────────────────────────

         (line held HIGH when idle)

Voltage Standards

StandardLogic LOWLogic HIGHNotes
TTL (5V)0V5VClassic Arduino
LVTTL (3.3V)0V3.3VESP32, STM32
RS-232+3 to +15V-3 to -15VInverted! PC serial ports

Critical: Never connect a 5V UART TX directly to a 3.3V RX pin. Use a voltage divider or level shifter.

5V MCU ──── TX ──┬───── 1kΩ ─────┬──── RX ── 3.3V MCU
                 │                │
                 └──── 2kΩ ───── GND
                 
         Simple resistor voltage divider
         Output ≈ 3.3V (5V × 2k/(1k+2k))

The UART Frame

This is the heart of the protocol. Every byte transmitted is wrapped in a frame with start, data, optional parity, and stop bits.

                    ┌─────────────────────────────────────────────┐
                    │              UART Data Frame                │
                    └─────────────────────────────────────────────┘

Idle  START    D0    D1    D2    D3    D4    D5    D6    D7   PAR  STOP  Idle
─────┐     ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬───┐     ─────
     │     │  0   │  1   │  1   │  0   │  1   │  0   │  0   │  1   │ P │  1  │
     └─────┘      │      │      │      │      │      │      │      │   └─────┘
                  └──────┴──────┴──────┴──────┴──────┴──────┴──────┘
     
     ←1bit→←──────────────── 8 data bits (LSB first) ─────────────→←P→←1bit→

     Transmitting: 0b10100110 = 0xA6 = 166 decimal

Frame Components

1. Start Bit (always 1 bit)

  • Line pulled LOW for exactly 1 bit period
  • Wakes up the receiver and starts the bit clock
  • The receiver detects the HIGH→LOW transition and begins sampling

2. Data Bits (5, 6, 7, or 8 bits)

  • Transmitted LSB first (least significant bit first) — the opposite of how we write numbers
  • 8N1 (8 data bits, No parity, 1 stop bit) is the most common configuration by far

3. Parity Bit (optional)

  • Even parity: bit is set so total number of 1s in data+parity is even
  • Odd parity: total number of 1s is odd
  • Detects single-bit errors but cannot correct them
  • Most embedded systems skip parity and rely on higher-layer CRCs

4. Stop Bit(s) (1 or 2 bits)

  • Line held HIGH for 1 or 2 bit periods
  • Returns line to idle state
  • 2 stop bits give the receiver more recovery time — useful at high baud rates or with slower processors

Baud Rate: The Math Behind the Speed

Baud rate = number of symbols per second. For UART (which uses binary signaling), 1 baud = 1 bit/second.

Common baud rates: 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600

Bit Period

              1
Bit Period = ─────
             Baud

At 9600 baud:   T = 1/9600  ≈ 104.2 µs per bit
At 115200 baud: T = 1/115200 ≈ 8.68 µs per bit

How the Receiver Samples

The receiver doesn’t know exactly when the start bit arrived — it samples at the midpoint of each bit period to maximize noise tolerance.

Receiver Sampling Strategy (16x oversampling, common in STM32):

                  ←───── 1 bit period (16 clocks) ─────→
                  
                  ┌─────────────────────────────────────┐
Line: ────────────┘                                     └────
                  
Sample ticks:  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16
                                          ↑
                                     SAMPLE HERE
                                     (tick 8 = midpoint)
                  
The start-bit detection resets the sample counter.
Each subsequent bit is sampled at tick 8.

Most UART hardware uses 8x or 16x oversampling internally. The receiver detects the start bit falling edge, waits 0.5 bit periods to land at the center, then samples every full bit period after that.


Full Communication Flow

Here’s what happens when Device A sends the byte 0x41 (‘A’) to Device B:

DEVICE A (Transmitter)                    DEVICE B (Receiver)
─────────────────────                     ────────────────────

1. Load byte 0x41 into                    1. Waiting, monitoring
   TX shift register                         RX line (idle HIGH)

2. Pull TX LOW                            2. Detects HIGH→LOW
   (Start bit)                               transition on RX
        ↓                                         ↓
3. Shift out D0=1 ──── TX ──── RX ──→  3. Sample at midpoint
4. Shift out D1=0                            of each bit period
5. Shift out D2=0                                 ↓
6. Shift out D3=0                         4. Reconstruct byte
7. Shift out D4=0                            from sampled bits
8. Shift out D5=1                                 ↓
9. Shift out D6=1                         5. Check stop bit HIGH
10. Shift out D7=0                            (framing check)
         ↓                                        ↓
11. Pull TX HIGH                          6. Move byte to RX
    (Stop bit, return                        buffer / interrupt
     to idle)                                fires


0x41 = 0b01000001

Wire looks like:
                 S  D0 D1 D2 D3 D4 D5 D6 D7  P
TX ──────┐     ┌──┐     ┌──────────────┐     ┌──────
         │     │  │     │              │     │
         └─────┘  └─────┘              └─────┘
         START  1   0  0  0  0  1  1  0  STOP

Full-Duplex vs Half-Duplex

Full-Duplex (standard): TX and RX are separate wires. Both devices can transmit simultaneously.

    Device A                    Device B
    ┌───────┐                  ┌───────┐
    │    TX ├──────────────────┤ RX    │
    │    RX ├──────────────────┤ TX    │
    │   GND ├──────────────────┤ GND   │
    └───────┘                  └───────┘
    
    ← TX of A connects to RX of B, and vice versa →
    ← Both sides share a common ground →

Half-Duplex: A single wire is shared, and devices take turns. Common with single-wire protocols like LIN bus or 1-Wire.


Synchronous Mode (USART with CLK)

In synchronous mode, a clock signal accompanies the data. The receiver samples on the clock edge instead of counting bit periods. This eliminates baud-rate mismatch errors entirely.

    CLK  ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─
           └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘
           
    TX   ──┬─── D0 ─── D1 ─── D2 ─── D3 ─── D4 ─── D5 ─── D6 ─── D7 ──
           │
           ↑
       Start bit still present in some implementations

   Receiver samples TX on rising (or falling) edge of CLK.
   No need for internal baud rate matching!

Synchronous USART is essentially SPI with a different framing convention. It’s less common in practice than asynchronous mode but useful when connecting to devices that provide their own clock.


Baud Rate Errors and Tolerance

Both transmitter and receiver derive their baud rate from their own crystal/oscillator. If these don’t match exactly, the sampling drifts over the frame.

Baud rate mismatch effect over an 8-bit frame:

Ideal:      |─ bit 0 ─|─ bit 1 ─|─ bit 2 ─|─ bit 3 ─|─ bit 4 ─|─ bit 5 ─|
Drift:      |── bit 0 ──|── bit 1 ──|── bit 2 ──|── bit 3 ──|  ← accumulates

By bit 7, the sample point has shifted significantly.

Rule of thumb: UART tolerates up to ~2–3% total baud rate error (transmitter + receiver combined). Beyond that, framing errors occur.

Total error budget: ±2.5%
Split between TX and RX → each can be off by ±1.25%

With 16x oversampling:
  1 bit period = 16 ticks
  Sample at tick 8 (center)
  Error tolerance ≈ ±4 ticks = ±25% of bit period
  
  But accumulated over 10 bits (start + 8 data + stop):
  Maximum drift to stay within ±0.5 bit = ±5% / 10 ≈ ±0.5%/bit

Common clock sources and their accuracy:

  • Internal RC oscillator (e.g., STM32 HSI): ±1–2% — marginal, test carefully
  • External crystal: ±20–50 ppm — excellent, no issues
  • Ceramic resonator: ±0.5% — usually fine

Hardware Flow Control (RTS/CTS)

When the transmitter sends faster than the receiver can process, the RX buffer overflows. Hardware flow control prevents this.

    Device A                              Device B
    ┌─────────┐                          ┌─────────┐
    │      TX ├──────────────────────────┤ RX      │
    │      RX ├──────────────────────────┤ TX      │
    │     RTS ├──────────────────────────┤ CTS     │  ← A says "ready to send"
    │     CTS ├──────────────────────────┤ RTS     │  ← B says "ready to send"
    │     GND ├──────────────────────────┤ GND     │
    └─────────┘                          └─────────┘

RTS (Request To Send): "I have data and I'm ready to transmit"
CTS (Clear To Send):   "I'm ready to receive — go ahead"

Flow:
  A asserts RTS → B checks its buffer → B asserts CTS → A transmits
  B's buffer fills → B deasserts CTS → A pauses transmission

Most microcontroller-to-sensor connections don’t need RTS/CTS (the MCU processes data fast enough). You need it for:

  • PC serial ports to slow devices
  • High-throughput data logging
  • Buffered UART bridges (e.g., FTDI chips)

Common Errors and How to Diagnose Them

Framing Error

The stop bit was sampled LOW instead of HIGH.

Causes:

  • Baud rate mismatch
  • Noise on the line
  • Wrong number of data bits configured
Expected:   ... D7 ─── STOP(HIGH) ─── Idle(HIGH) ...
Received:   ... D7 ─── STOP(LOW!) ─── ...
                              ↑
                         Framing error!

Overrun Error

A new byte arrived before the previous one was read from the buffer.

Causes:

  • Interrupt latency too high
  • Interrupt not enabled
  • DMA not configured for high-throughput

Parity Error

The received parity bit doesn’t match the computed parity of the data bits.

Causes:

  • Single-bit corruption (noise)
  • Both ends configured with different parity settings

Break Condition

The line is held LOW for longer than one full frame.

Causes:

  • Used intentionally as a “wake up” signal in LIN bus
  • Unintentional: TX line stuck low (short to ground, GPIO misconfiguration)

Practical Configuration Checklist

Before your first UART session, verify:

[ ] Baud rates match on both sides
[ ] Data bits match (usually 8)
[ ] Parity setting matches (usually None)
[ ] Stop bits match (usually 1)
[ ] TX of device A → RX of device B (wires crossed)
[ ] Common ground connected
[ ] Voltage levels compatible (3.3V / 5V check)
[ ] UART peripheral clock enabled in MCU (RCC/clock gating)
[ ] GPIO pins configured for UART alternate function
[ ] TX pin mode: Alternate Function Push-Pull
[ ] RX pin mode: Alternate Function Input (or just Input)
[ ] Interrupts / DMA configured if needed

STM32 USART Example (HAL)

// Initialize USART2 at 115200 baud (8N1)
UART_HandleTypeDef huart2;
 
void MX_USART2_UART_Init(void) {
    huart2.Instance        = USART2;
    huart2.Init.BaudRate   = 115200;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits   = UART_STOPBITS_1;
    huart2.Init.Parity     = UART_PARITY_NONE;
    huart2.Init.Mode       = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
    HAL_UART_Init(&huart2);
}
 
// Transmit a string
const char *msg = "Hello UART\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
 
// Receive with interrupt (non-blocking)
uint8_t rx_byte;
HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
 
// Callback fires when byte received
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        process_byte(rx_byte);
        HAL_UART_Receive_IT(&huart2, &rx_byte, 1); // re-arm
    }
}

Baud Rate Register Calculation (STM32)

          f_CLK
BRR = ─────────────
          Baud

For f_CLK = 72 MHz, Baud = 115200:
BRR = 72,000,000 / 115,200 = 625

With 16x oversampling (OVER8 = 0):
  Mantissa = BRR >> 4 = 39
  Fraction = BRR & 0xF = 1
  → USART_BRR = (39 << 4) | 1 = 0x271

Actual baud = 72,000,000 / (16 × 39.0625) = 115,108 baud
Error = (115200 - 115108) / 115200 = 0.08% ✓

RS-232: UART Over Long Distances

Standard TTL UART works over short distances (< 30 cm) before noise becomes an issue. RS-232 extends this to ~15 meters using higher voltages and a dedicated driver IC (MAX232, MAX3232).

TTL UART                   RS-232
─────────                  ───────
Logic 1 = +3.3V/5V         Logic 1 = -3V to -15V  (inverted!)
Logic 0 = 0V               Logic 0 = +3V to +15V  (inverted!)

MCU TX ──→ MAX232 ──→ RS-232 DB9 Connector ──→ (up to 15m cable) ──→ PC
           converts voltage AND inverts logic

The MAX232 contains internal charge pumps that generate ±12V from a 5V supply — no external voltage rail needed.


RS-485: UART for Industrial Applications

When you need to go even further (up to 1200 meters) or connect multiple devices on a single bus, RS-485 is the standard.

                    ┌────────────────────────────────────────┐
                    │           RS-485 Bus Topology          │
                    └────────────────────────────────────────┘

   Node 1          Node 2          Node 3          Node 4
 ┌───────┐        ┌───────┐        ┌───────┐        ┌───────┐
 │  MCU  │        │  MCU  │        │  MCU  │        │  MCU  │
 │  UART │        │  UART │        │  UART │        │  UART │
 └───┬───┘        └───┬───┘        └───┬───┘        └───┬───┘
     │ MAX485         │ MAX485         │ MAX485         │ MAX485
     ├─── A ──────────┼─── A ──────────┼─── A ──────────┼─── A ───┤120Ω
     └─── B ──────────┴─── B ──────────┴─── B ──────────┴─── B ───┤
                                                                   GND

  Differential signaling: data = V(A) - V(B)
  Noise on both lines cancels out
  All nodes share the bus; only one transmits at a time
  Termination resistor (120Ω) at each end of the bus

RS-485 uses differential signaling — the receiver measures the voltage difference between the A and B lines. Common-mode noise (induced by power lines, motors, etc.) appears equally on both lines and cancels out.


DMA-Based UART: Zero-CPU Overhead

For high-throughput applications, use DMA (Direct Memory Access) instead of interrupts. The DMA controller moves bytes between the UART peripheral and RAM without CPU involvement.

   CPU                    DMA Controller              USART Peripheral
   ───                    ──────────────              ────────────────
   
   1. Configure DMA  →→→  Source: RAM buffer    ←←← USART DR register
      transfer            Dest: USART->DR             ↕
                          Length: 256 bytes       Shift register
                                                       ↕
   2. Trigger transfer         ↓                  TX pin
   
   3. CPU does other    DMA moves bytes ──────────────────────────────→
      work...           byte by byte,
                        triggered by USART
                        "TX empty" signal
   
   4. DMA complete      ↓
      interrupt fires   CPU processes
                        next buffer

This is essential for:

  • Streaming sensor data (IMU at 1 kHz+)
  • UART-to-USB bridges
  • Protocol parsers that handle continuous data

Summary

UART/USART remains one of the most important protocols in embedded engineering despite being over 60 years old. Its simplicity — just two wires, no clock — makes it reliable and easy to debug.

Key takeaways:

  • UART is asynchronous; USART adds optional synchronous mode
  • Frames: start bit → data (LSB first) → optional parity → stop bit(s)
  • Baud rate must match within ~2–3% total error
  • 8N1 (8 data, no parity, 1 stop) is the universal default
  • TX → RX, RX → TX, GND → GND — always cross the data lines
  • Voltage levels matter: 5V and 3.3V are not directly compatible
  • For noise immunity at distance: RS-232 (15m) or RS-485 (1200m)
  • For high throughput: use DMA, not polling or interrupts

The next time a UART connection doesn’t work, start with the checklist: baud rate, wire polarity, voltage levels, clock enable, GPIO alternate function. Nine times out of ten, it’s one of those five.


Tags: Embedded Systems, Serial Communication, UART, USART, STM32, Protocols