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.
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
| Application | Typical Baud Rate | Notes |
|---|---|---|
| Debug console / JTAG UART | 115200 | Most common for printf-style debugging on FPGAs and microcontrollers |
| GPS modules (NMEA sentences) | 9600 | Default for most GPS receivers; configurable to 115200 |
| Bluetooth serial (HC-05, HC-06) | 9600 / 38400 | Transparent bridge — UART on the MCU side, BT on the RF side |
| WiFi modules (ESP8266, ESP32) | 115200 | AT command interface over UART |
| Cellular modems (SIM800, SIM7600) | 9600–921600 | AT commands + data mode |
| USB-UART bridges (FTDI, CP2102) | Up to 12 Mbaud | Convert USB to 3.3 V TTL UART for host PC communication |
| LIDAR sensors (RPLidar, TFMini) | 115200 | Binary distance data stream |
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
| Config | Data Bits | Parity | Stop Bits | Bits per Frame | Use Case |
|---|---|---|---|---|---|
| 8N1 | 8 | None | 1 | 10 | Standard — used by virtually everything modern |
| 8N2 | 8 | None | 2 | 11 | Older / slower receivers needing extra stop time |
| 8E1 | 8 | Even | 1 | 11 | Industrial / modbus devices |
| 8O1 | 8 | Odd | 1 | 11 | Some legacy modems |
| 7E1 | 7 | Even | 1 | 10 | 7-bit ASCII with parity; older terminal equipment |
| 7O1 | 7 | Odd | 1 | 10 | Variant of 7-bit ASCII with parity |
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 Rate | Bit Period | Byte 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 |
| 115200 | 8.68 µs | 86.8 µs | Most 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:
Example: 50 MHz system clock, 115200 baud:
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 Freq | Baud Rate | CLK_PER_BIT | Error |
|---|---|---|---|
| 50 MHz | 115200 | 434 | 0.08% |
| 50 MHz | 9600 | 5208 | 0.002% |
| 100 MHz | 115200 | 868 | 0.04% |
| 25 MHz | 115200 | 217 | 0.16% |
| 12 MHz | 115200 | 104 | 0.16% |
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
| Property | TTL UART (3.3V) | TTL UART (5V) | RS-232 |
|---|---|---|---|
| Logic 0 voltage | 0 V | 0 V | +3 V to +15 V |
| Logic 1 voltage | 3.3 V | 5 V | −3 V to −15 V |
| Idle line level | 3.3 V (HIGH) | 5 V (HIGH) | −3 V to −15 V (Mark) |
| Polarity | Non-inverted | Non-inverted | Inverted vs TTL |
| Max cable length | ~30 cm (PCB traces) | ~1 m | ~15 m @ 9600 baud |
| Noise immunity | Low | Medium | High (large voltage swing) |
| Connector | Dupont / JST pins | Dupont / JST pins | DB-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
| IC | Supply | Max Baud | Notes |
|---|---|---|---|
| FTDI FT232RL | 3.3V / 5V | 3 Mbaud | Highest compatibility, genuine chip required (counterfeit issue). Driver: VCP or D2XX. |
| CP2102 | 3.3V | 1 Mbaud | Common in ESP8266 devboards. Silicon Labs driver. Very reliable. |
| CH340G / CH340C | 3.3V / 5V | 2 Mbaud | Cheap Chinese alternative. Requires separate driver installation on Windows. Works well at 115200. |
| PL2303 | 3.3V / 5V | 115200 | Older Prolific chip. Driver support issues on modern Windows. |
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
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
| State | TX line | Duration | Next state condition |
|---|---|---|---|
| IDLE | HIGH (1) | Until tx_valid asserted | tx_valid = 1 → START |
| START | LOW (0) | CLK_PER_BIT cycles | baud_cnt reaches CLK_PER_BIT−1 → DATA |
| DATA | tx_shift[bit_idx] | 8 × CLK_PER_BIT cycles | bit_idx = 7 and baud_cnt done → STOP |
| STOP | HIGH (1) | CLK_PER_BIT cycles | baud_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
Receiver State Machine
| State | Condition to exit | Action |
|---|---|---|
| IDLE | rx_sync = 0 (start bit detected) | Reset baud_cnt, go to START |
| START | baud_cnt = CLK_PER_BIT/2 − 1 | If rx_sync still LOW → DATA; else noise → IDLE |
| DATA | baud_cnt = CLK_PER_BIT − 1, 8 times | Sample rx_sync into rx_shift[bit_idx] LSB first |
| STOP | baud_cnt = CLK_PER_BIT − 1 | If rx_sync = 1: latch rx_shift → rx_data, pulse rx_valid; → IDLE |
7. Common UART Bugs and Debug Tips
| Symptom | Root Cause | Fix |
|---|---|---|
| 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:
- 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.
- Measure start bit width — should be 1/baud_rate. For 115200: 8.68 µs. If it's different, both sides need reconfiguring.
- Count data bits — after the start bit, count 8 data bits (LSB first), then the stop bit.
- Check stop bit is HIGH — a stop bit that is LOW indicates a framing error; the receiver will flag this.
- Use protocol decode — most modern oscilloscopes have built-in UART decode. Set the baud rate and it will decode hex bytes automatically.
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
| Feature | Hardware (RTS/CTS) | Software (XON/XOFF) | None |
|---|---|---|---|
| Extra wires | 2 (RTS, CTS) | 0 | 0 |
| Binary data safe | Yes | No | N/A |
| Latency | Very low (hardware) | Higher (must send + process byte) | N/A |
| Complexity | Low (hardware-managed) | Higher (software must handle) | None |
| Typical use | Modems, high-speed links, RS-232 | Simple terminal protocols | Most FPGA / embedded UART designs |