Protocol 03 · Serial Protocols

UART Protocol
Asynchronous Serial · Baud Rate · TX & RX Verilog

The simplest serial protocol — two wires, no shared clock, and a baud rate agreement. Used in every debug console, GPS module, and Bluetooth adapter you've ever touched.

Start/Stop Bits 8N1 Frame Baud Rate Oversampling RS-232 TTL UART Verilog TX/RX Flow Control
TX 1/baud START D0 D1 D2 D3 D4 D5 D6 D7 PAR STOP IDLE HIGH LOW

UART frame waveform — idle HIGH → START (low) → 8 data bits LSB-first → optional parity → STOP (high) → idle. Amber = data, red = start, green = stop, purple = parity.

1. What is UART?

UART — Universal Asynchronous Receiver-Transmitter — is the oldest and simplest serial communication protocol still in widespread use. It requires just two signal wires:

  • TX — Transmit: data sent from this device
  • RX — Receive: data arriving at this device (connect to the other side's TX)

The "asynchronous" part means there is no shared clock signal. Instead, both sides agree in advance on a baud rate (bits per second). Each byte is wrapped in a framing sequence — a start bit and stop bit — that lets the receiver synchronize itself on every byte.

UART is strictly point-to-point: one transmitter, one receiver, one link. It does not support multiple devices on a single bus the way I2C or SPI do. For multi-device communication you need a higher-level protocol on top of UART, or a different physical layer like RS-485.

Where UART Is Used

ApplicationTypical Baud RateNotes
Debug console / JTAG UART115200Most common for printf-style debugging on FPGAs and microcontrollers
GPS modules (NMEA sentences)9600Default for most GPS receivers; configurable to 115200
Bluetooth serial (HC-05, HC-06)9600 / 38400Transparent bridge — UART on the MCU side, BT on the RF side
WiFi modules (ESP8266, ESP32)115200AT command interface over UART
Cellular modems (SIM800, SIM7600)9600–921600AT commands + data mode
USB-UART bridges (FTDI, CP2102)Up to 12 MbaudConvert USB to 3.3 V TTL UART for host PC communication
LIDAR sensors (RPLidar, TFMini)115200Binary distance data stream
Key takeaway: UART is everywhere. If a chip or module has a "TX/RX" pin or "Serial" interface, it almost certainly speaks UART. It is the universal "last resort" debug port on nearly every SoC and microcontroller ever made.

2. UART Frame Format

Every byte transmitted over UART is wrapped in a frame. The frame tells the receiver when a byte starts, carries the data bits, optionally checks for errors, and signals that the transmission is complete.

Idle State

When no data is being sent, the TX line sits HIGH (logic 1). This is the idle or "mark" state. A receiver uses this to confirm the line is connected and powered — a persistently LOW line means the transmitter is broken, unpowered, or the baud rate is catastrophically wrong.

Start Bit

The transmitter pulls TX LOW for exactly one bit period to signal the beginning of a frame. The receiver detects this falling edge and starts its internal bit counter. Because idle is always HIGH and the start bit is always LOW, this transition is unambiguous.

Data Bits

The actual payload follows the start bit, transmitted LSB first (Least Significant Bit first — D0, then D1, … D7). The number of data bits is configurable: 5, 6, 7, or 8 bits. 8 bits is overwhelmingly the modern standard. 7-bit was common for ASCII-only teletype systems; 5-bit was used in Baudot (telex) encoding.

Parity Bit (Optional)

An optional single bit used for rudimentary error detection. Three choices:

  • None (N) — No parity bit. Most common in modern use.
  • Even (E) — Parity bit makes the total count of 1s in the data + parity even.
  • Odd (O) — Parity bit makes the total count of 1s odd.

Parity only detects odd numbers of bit errors (misses 2-bit errors), so most protocols either omit it entirely or use CRC instead.

Stop Bit(s)

TX returns HIGH for one or two bit periods after the data (and parity) bits. The stop bit(s) guarantee a minimum idle time between frames so the receiver can reset its state machine before the next start bit. One stop bit is standard; two stop bits are used with slower receivers or to reduce framing errors at lower baud rates.

Notation Table — Common UART Configurations

ConfigData BitsParityStop BitsBits per FrameUse Case
8N18None110Standard — used by virtually everything modern
8N28None211Older / slower receivers needing extra stop time
8E18Even111Industrial / modbus devices
8O18Odd111Some legacy modems
7E17Even1107-bit ASCII with parity; older terminal equipment
7O17Odd110Variant of 7-bit ASCII with parity
8N1 at 115200 baud is the de facto default for modern UART. When in doubt, start here — it works with 99% of devices.

3. Baud Rate and Oversampling

Baud rate is the number of symbol transitions per second. For UART, each symbol is one bit, so baud rate equals bits per second (bps). A baud rate of 115200 means 115,200 bits are sent every second, giving a bit period of 1/115200 ≈ 8.68 µs.

Both sides must be configured to exactly the same baud rate. A mismatch causes the receiver to sample bits at the wrong point in time. As frames get longer the accumulated error grows until a stop bit is sampled as LOW, triggering a framing error. The maximum tolerable mismatch is approximately ±2% before errors become frequent.

Standard Baud Rates

Baud RateBit PeriodByte Time (8N1)Typical Use
110 9.09 ms 90.9 ms Teletype terminals (historic)
300 3.33 ms 33.3 ms Acoustic modem (300 bps modem era)
1200 833 µs 8.33 ms Older modems, some metering equipment
9600 104 µs 1.04 ms GPS (NMEA), Bluetooth modules, slow sensors
38400 26.0 µs 260 µs Bluetooth (HC-05 default), some bootloaders
57600 17.4 µs 174 µs Some legacy modem standards
1152008.68 µs86.8 µsMost common — debug console, ESP8266/32, FTDI
230400 4.34 µs 43.4 µs High-speed sensor links, some WiFi modules
921600 1.09 µs 10.9 µs High-speed bootloaders, FTDI FT232H

Oversampling — How the Receiver Finds the Bit Center

The UART receiver does not know exactly when the transmitter's bit transitions occur — it only knows the nominal baud rate. To sample each bit reliably, it uses oversampling: the system clock runs much faster than the baud rate, and the receiver counts clock cycles to estimate the center of each bit period.

The standard oversampling ratio is 16× (some implementations use 8×). The key parameter is:

CLK_PER_BIT = clk_frequency / baud_rate

Example: 50 MHz system clock, 115200 baud:

CLK_PER_BIT = 50,000,000 / 115,200 = 434 clock cycles per bit

Half-period centering trick: When the receiver detects the falling edge of the start bit, it waits CLK_PER_BIT / 2 cycles, which places the first sample exactly in the center of the start bit. It then samples once every CLK_PER_BIT cycles for each data bit. This centering maximizes the timing margin — the sample point is as far from both edges of the bit as possible.

Clock FreqBaud RateCLK_PER_BITError
50 MHz 1152004340.08%
50 MHz 9600 52080.002%
100 MHz 1152008680.04%
25 MHz 1152002170.16%
12 MHz 1152001040.16%
Baud error rule: Keep the error (|(CLK_PER_BIT_actual − CLK_PER_BIT_ideal)| / CLK_PER_BIT_ideal × 100%) below 2%. Beyond that, the accumulated timing error over a 10-bit 8N1 frame causes the stop bit to be missampled, generating framing errors.

4. RS-232 vs TTL UART Levels

The UART framing protocol is independent of the physical voltage levels used. The two most common physical layer standards are TTL UART (what microcontrollers and FPGAs use natively) and RS-232 (the classic PC serial port standard). They are not directly compatible — the voltages are different and the polarity is inverted.

Comparison Table

PropertyTTL UART (3.3V)TTL UART (5V)RS-232
Logic 0 voltage0 V0 V+3 V to +15 V
Logic 1 voltage3.3 V5 V−3 V to −15 V
Idle line level3.3 V (HIGH)5 V (HIGH)−3 V to −15 V (Mark)
PolarityNon-invertedNon-invertedInverted vs TTL
Max cable length~30 cm (PCB traces)~1 m~15 m @ 9600 baud
Noise immunityLowMediumHigh (large voltage swing)
ConnectorDupont / JST pinsDupont / JST pinsDB-9 (DE-9), DB-25

Level Conversion

To connect TTL UART to an RS-232 port, you need a level-shifting IC that converts voltages and inverts polarity:

  • MAX232 — Classic 5V TTL ↔ RS-232. Uses charge pump capacitors to generate ±12V internally. Still widely used in industrial equipment.
  • MAX3232 — Same as MAX232 but operates at 3.3V supply. Pin-compatible replacement for 3.3V systems.
  • SP3232E / ADM3202 — Lower-power alternatives to MAX3232 with similar functionality.

USB-to-UART Bridges

ICSupplyMax BaudNotes
FTDI FT232RL3.3V / 5V3 MbaudHighest compatibility, genuine chip required (counterfeit issue). Driver: VCP or D2XX.
CP21023.3V1 MbaudCommon in ESP8266 devboards. Silicon Labs driver. Very reliable.
CH340G / CH340C3.3V / 5V2 MbaudCheap Chinese alternative. Requires separate driver installation on Windows. Works well at 115200.
PL23033.3V / 5V115200Older Prolific chip. Driver support issues on modern Windows.
Modern workflow: Most engineers use a CP2102 or CH340G USB-UART module to connect their FPGA/MCU UART to a PC terminal (PuTTY, minicom, screen). RS-232 is rarely used in new designs — only for long-cable industrial links or legacy equipment.

5. Verilog UART Transmitter

The transmitter shifts out one bit per bit-period (CLK_PER_BIT clock cycles). It uses a 4-state FSM: IDLE → START → DATA → STOP. The tx_ready signal is HIGH when the transmitter is idle and ready to accept a new byte.

// ─────────────────────────────────────────────────────────────────────
// uart_tx.v  —  Parameterized UART Transmitter
// Parameter: CLK_PER_BIT = clk_freq / baud_rate
//   Example: 50 MHz / 115200 = 434
// Frame format: 8N1 (8 data bits, No parity, 1 stop bit)
// ─────────────────────────────────────────────────────────────────────
module uart_tx #(
  parameter CLK_PER_BIT = 434
)(
  input  wire       clk,       // system clock
  input  wire       rst_n,     // active-low synchronous reset
  input  wire       tx_valid,  // pulse HIGH for 1 cycle to start TX
  input  wire [7:0]  tx_data,   // byte to transmit (hold until tx_ready)
  output reg        tx,        // UART TX line (idle HIGH)
  output reg        tx_ready   // HIGH when transmitter is idle
);

  // ── State encoding ──
  localparam [1:0]
    IDLE  = 2'b00,
    START = 2'b01,
    DATA  = 2'b10,
    STOP  = 2'b11;

  reg [1:0]  state;
  reg [9:0]  baud_cnt;   // counts clock cycles per bit
  reg [2:0]  bit_idx;   // 0 to 7: which data bit is being sent
  reg [7:0]  tx_shift;  // copy of tx_data, shifted out LSB first

  always @(posedge clk) begin
    if (!rst_n) begin
      state    <= IDLE;
      tx       <= 1'b1;   // idle HIGH
      tx_ready <= 1'b1;
      baud_cnt <= 0;
      bit_idx  <= 0;
      tx_shift <= 8'h00;
    end else begin
      case (state)

        IDLE: begin
          tx       <= 1'b1;
          tx_ready <= 1'b1;
          baud_cnt <= 0;
          bit_idx  <= 0;
          if (tx_valid) begin
            tx_shift <= tx_data;   // latch the byte
            tx_ready <= 1'b0;
            state    <= START;
          end
        end

        START: begin
          tx <= 1'b0;             // drive start bit LOW
          if (baud_cnt == CLK_PER_BIT - 1) begin
            baud_cnt <= 0;
            state    <= DATA;
          end else
            baud_cnt <= baud_cnt + 1;
        end

        DATA: begin
          tx <= tx_shift[bit_idx];  // LSB first
          if (baud_cnt == CLK_PER_BIT - 1) begin
            baud_cnt <= 0;
            if (bit_idx == 3'd7) begin
              bit_idx <= 0;
              state   <= STOP;
            end else
              bit_idx <= bit_idx + 1;
          end else
            baud_cnt <= baud_cnt + 1;
        end

        STOP: begin
          tx <= 1'b1;              // stop bit HIGH
          if (baud_cnt == CLK_PER_BIT - 1) begin
            baud_cnt <= 0;
            tx_ready <= 1'b1;
            state    <= IDLE;
          end else
            baud_cnt <= baud_cnt + 1;
        end

        default: state <= IDLE;
      endcase
    end
  end
endmodule
Usage: Assert tx_valid for exactly one clock cycle while tx_ready is HIGH and tx_data holds the byte to send. The module latches tx_data immediately. tx_ready goes LOW for the duration of the frame (~8680 cycles at 115200 baud with 50 MHz clock) and returns HIGH when the stop bit completes.

Transmitter State Machine

StateTX lineDurationNext state condition
IDLEHIGH (1)Until tx_valid assertedtx_valid = 1 → START
STARTLOW (0)CLK_PER_BIT cyclesbaud_cnt reaches CLK_PER_BIT−1 → DATA
DATAtx_shift[bit_idx]8 × CLK_PER_BIT cyclesbit_idx = 7 and baud_cnt done → STOP
STOPHIGH (1)CLK_PER_BIT cyclesbaud_cnt reaches CLK_PER_BIT−1 → IDLE

6. Verilog UART Receiver

The receiver is harder than the transmitter because it must synchronize itself to the transmitter on every byte using only the falling edge of the start bit. The key technique is the half-period wait: after detecting the start bit falling edge, the receiver waits CLK_PER_BIT/2 cycles before sampling — placing the sample point at the center of the start bit and all subsequent data bits.

// ─────────────────────────────────────────────────────────────────────
// uart_rx.v  —  Parameterized UART Receiver with oversampling centering
// Parameter: CLK_PER_BIT = clk_freq / baud_rate  (e.g. 434 for 50MHz/115200)
// Outputs rx_data when a valid 8N1 frame is received.
// rx_valid pulses HIGH for 1 cycle when rx_data is ready.
// ─────────────────────────────────────────────────────────────────────
module uart_rx #(
  parameter CLK_PER_BIT = 434
)(
  input  wire        clk,      // system clock
  input  wire        rst_n,    // active-low synchronous reset
  input  wire        rx,       // UART RX line (idle HIGH)
  output reg  [7:0]  rx_data,  // received byte
  output reg         rx_valid  // pulses HIGH for 1 cycle when byte ready
);

  // ── State encoding ──
  localparam [1:0]
    IDLE  = 2'b00,
    START = 2'b01,
    DATA  = 2'b10,
    STOP  = 2'b11;

  // ── 2-FF synchronizer for the RX input (async to clock domain) ──
  reg        rx_s1, rx_sync;
  always @(posedge clk) begin
    rx_s1   <= rx;
    rx_sync <= rx_s1;
  end

  reg [1:0]  state;
  reg [9:0]  baud_cnt;
  reg [2:0]  bit_idx;
  reg [7:0]  rx_shift;  // shift register, fills LSB first

  always @(posedge clk) begin
    if (!rst_n) begin
      state    <= IDLE;
      baud_cnt <= 0;
      bit_idx  <= 0;
      rx_shift <= 8'h00;
      rx_data  <= 8'h00;
      rx_valid <= 1'b0;
    end else begin
      rx_valid <= 1'b0;  // default: not valid; set only in STOP

      case (state)

        IDLE: begin
          baud_cnt <= 0;
          bit_idx  <= 0;
          // Detect falling edge of start bit
          if (rx_sync == 1'b0) state <= START;
        end

        START: begin
          // Wait CLK_PER_BIT/2 to sample at bit center of START bit
          // Verify it is still LOW (noise rejection)
          if (baud_cnt == (CLK_PER_BIT / 2) - 1) begin
            baud_cnt <= 0;
            if (rx_sync == 1'b0)   // confirmed start bit
              state <= DATA;
            else
              state <= IDLE;           // glitch — abort
          end else
            baud_cnt <= baud_cnt + 1;
        end

        DATA: begin
          // Sample each bit at center: count CLK_PER_BIT cycles between samples
          if (baud_cnt == CLK_PER_BIT - 1) begin
            baud_cnt             <= 0;
            rx_shift[bit_idx]    <= rx_sync;  // capture bit into shift reg
            if (bit_idx == 3'd7) begin
              bit_idx <= 0;
              state   <= STOP;
            end else
              bit_idx <= bit_idx + 1;
          end else
            baud_cnt <= baud_cnt + 1;
        end

        STOP: begin
          // Sample stop bit at its center
          if (baud_cnt == CLK_PER_BIT - 1) begin
            baud_cnt <= 0;
            if (rx_sync == 1'b1) begin  // valid stop bit?
              rx_data  <= rx_shift;        // latch complete byte
              rx_valid <= 1'b1;           // pulse valid for 1 cycle
            end
            // Go back to IDLE whether stop bit is valid or not
            // (framing error: rx_valid stays 0, corrupted byte is discarded)
            state <= IDLE;
          end else
            baud_cnt <= baud_cnt + 1;
        end

        default: state <= IDLE;
      endcase
    end
  end
endmodule
The half-period trick explained: After detecting the falling edge of the start bit, the receiver waits exactly CLK_PER_BIT/2 cycles before its first sample. This places the sample at the center of the start bit. All subsequent data-bit samples are taken CLK_PER_BIT cycles apart — also centered. Sampling at the center gives maximum margin against jitter and baud rate mismatch.
Always include the 2-FF synchronizer on the RX input (rx_s1, rx_sync above). The UART RX line is an asynchronous input — it can transition at any time relative to the system clock. Without synchronization, the first FF can go metastable and corrupt the received data.

Receiver State Machine

StateCondition to exitAction
IDLErx_sync = 0 (start bit detected)Reset baud_cnt, go to START
STARTbaud_cnt = CLK_PER_BIT/2 − 1If rx_sync still LOW → DATA; else noise → IDLE
DATAbaud_cnt = CLK_PER_BIT − 1, 8 timesSample rx_sync into rx_shift[bit_idx] LSB first
STOPbaud_cnt = CLK_PER_BIT − 1If rx_sync = 1: latch rx_shift → rx_data, pulse rx_valid; → IDLE

7. Common UART Bugs and Debug Tips

SymptomRoot CauseFix
Garbage / random characters Baud rate mismatch — the most common UART bug by far Verify both sides are set to identical baud rate. Check CLK_PER_BIT calculation. Measure bit period on oscilloscope.
Missing bytes / data loss RX buffer overflow — receiver not reading fast enough Add a FIFO between UART RX and your consumer logic. Use flow control (RTS/CTS or XON/XOFF) if available.
Framing error flag Stop bit sampled as LOW — caused by baud mismatch or wrong number of stop bits Check baud rate. If stop bits = 2 on TX but receiver expects 1, the second stop bit is sampled as a new start bit.
Inverted / mirror-image data RS-232 ↔ TTL confusion — RS-232 polarity is inverted vs TTL Add MAX232 / MAX3232 level shifter. Or check if your UART hardware has a polarity inversion option.
Receives 0x00 for every byte RX line is floating LOW — idle state is not being driven HIGH Add a pull-up resistor (10kΩ) on the RX line to 3.3V (or 5V). Check TX side is powered and driving HIGH when idle.
First byte after reset is wrong Missing 2-FF synchronizer on RX — metastability at startup Always add a 2-FF synchronizer (two cascaded D flip-flops) on the RX input in your Verilog design.
Works at 9600 but not 115200 CLK_PER_BIT rounding error is proportionally larger at high baud rates Recalculate CLK_PER_BIT. Use a higher system clock frequency to reduce the fractional truncation error.
TX and RX swapped TX of device A connected to TX of device B (should be TX → RX) Remember: TX connects to RX on the other side. Cross the wires.

Oscilloscope Debug

A digital oscilloscope is the fastest way to diagnose UART problems:

  1. Measure idle level — should be HIGH (3.3V or 5V for TTL, ~-12V for RS-232). A LOW idle means TX is broken or unpowered.
  2. Measure start bit width — should be 1/baud_rate. For 115200: 8.68 µs. If it's different, both sides need reconfiguring.
  3. Count data bits — after the start bit, count 8 data bits (LSB first), then the stop bit.
  4. Check stop bit is HIGH — a stop bit that is LOW indicates a framing error; the receiver will flag this.
  5. Use protocol decode — most modern oscilloscopes have built-in UART decode. Set the baud rate and it will decode hex bytes automatically.
The #1 UART debug rule: When something is broken, the very first thing to check is baud rate. Both ends must match to within ±2%. Measure it on a scope rather than assuming the configuration is correct.

8. UART Flow Control

Basic UART has no mechanism to tell the transmitter to stop sending when the receiver's buffer is full. This leads to data loss if the receiver cannot keep up. Two flow control mechanisms solve this:

Hardware Flow Control — RTS / CTS

Two additional wires are added to the link:

  • RTS (Request To Send) — driven by the receiver. Asserted (LOW active) to tell the transmitter "I am ready to receive data."
  • CTS (Clear To Send) — driven by the transmitter. The transmitter only sends when CTS is asserted (i.e., it samples the other side's RTS).

When the receiver's buffer is nearly full, it deasserts RTS. The transmitter sees CTS go inactive and pauses mid-stream (it will always finish the current byte cleanly). When the receiver drains its buffer, it reasserts RTS and the transmitter resumes. Hardware flow control is transparent — the upper-level protocol does not need to be aware of it.

Software Flow Control — XON / XOFF

No extra wires needed. Instead, the receiver sends special ASCII control characters:

  • XOFF (0x13, Ctrl+S) — sent by receiver to tell the transmitter "stop sending."
  • XON (0x11, Ctrl+Q) — sent by receiver to resume data flow.

The problem with XON/XOFF is that these control characters can appear in binary data streams, causing the transmitter to pause unexpectedly. It is only suitable for ASCII text protocols.

Flow Control Comparison

FeatureHardware (RTS/CTS)Software (XON/XOFF)None
Extra wires2 (RTS, CTS)00
Binary data safeYesNoN/A
LatencyVery low (hardware)Higher (must send + process byte)N/A
ComplexityLow (hardware-managed)Higher (software must handle)None
Typical useModems, high-speed links, RS-232Simple terminal protocolsMost FPGA / embedded UART designs
In most FPGA and microcontroller designs, flow control is omitted entirely. Instead, a FIFO buffer deep enough to absorb burst data is placed after the UART RX, and the application is designed to read fast enough. For robust designs receiving continuous data at high baud rates, add a FIFO with a nearly-full flag connected back to RTS. See the Verilog UART+FIFO capstone project for a complete implementation.
← I2C Protocol Serial Protocols Hub →