UART Verilog Code: TX, RX & Loopback Testbench
A UART is the simplest way to move bytes between two chips, or between an FPGA and a PC, and writing the RTL yourself is the fastest route to understanding asynchronous serial links. This tutorial gives you complete, synthesizable UART Verilog code: a baud-rate generator, a transmitter and a receiver that handle start, data and stop bits, all parameterized by clock rate and word width. Every listing below is a full module plus its testbench, and each simulation log is produced by compiling the exact code with Icarus Verilog.
The baud-rate generator
A UART has no shared clock, so both ends must agree on a bit period. Instead of synthesizing a separate baud clock, we count system clocks per bit: CLKS_PER_BIT = f_clk / baud. The module below pulses tick for one cycle at the end of each bit period. The testbench uses a deliberately tiny divisor so the run finishes in a few hundred cycles.
// baud_gen.v -- baud-rate tick generator
// Build & run: iverilog -g2005 -o sim baud_gen.v && vvp sim
module baud_gen #(parameter integer CLKS_PER_BIT = 4) (
input wire clk,
input wire rst_n,
output reg tick // 1-cycle pulse every CLKS_PER_BIT clocks
);
localparam integer W = (CLKS_PER_BIT < 2) ? 1 : $clog2(CLKS_PER_BIT);
reg [W-1:0] count;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 0; tick <= 1'b0;
end else if (count == CLKS_PER_BIT-1) begin
count <= 0; tick <= 1'b1; // one bit period elapsed
end else begin
count <= count + 1'b1; tick <= 1'b0;
end
end
endmodule
module tb_baud_gen;
localparam integer CLKS_PER_BIT = 4;
reg clk=1'b0, rst_n=1'b0;
wire tick;
integer ticks = 0;
baud_gen #(CLKS_PER_BIT) dut (.clk(clk), .rst_n(rst_n), .tick(tick));
always #5 clk = ~clk; // 100 MHz
always @(posedge clk)
if (rst_n && tick) begin
ticks = ticks + 1;
$display("[%0t] tick #%0d (one serial bit period)", $time, ticks);
end
initial begin
$display("CLKS_PER_BIT=%0d -> a tick is expected every %0d clocks", CLKS_PER_BIT, CLKS_PER_BIT);
repeat (2) @(posedge clk); rst_n = 1'b1;
repeat (20) @(posedge clk);
$display("Ticks counted over 20 clocks = %0d", ticks);
$finish;
end
endmoduleCLKS_PER_BIT=4 -> a tick is expected every 4 clocks [55] tick #1 (one serial bit period) [95] tick #2 (one serial bit period) [135] tick #3 (one serial bit period) [175] tick #4 (one serial bit period) [215] tick #5 (one serial bit period) Ticks counted over 20 clocks = 5 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpech9cxqc/baud_gen.v:44: $finish called at 215 (1s)
Transmitter, receiver and a loopback testbench
The transmitter serializes a byte as one start bit, eight data bits (LSB first) and one stop bit. The receiver recovers the byte by finding the start edge and then sampling every bit at its centre. Wiring the transmitter output straight into the receiver input gives a self-checking loopback: send a byte, wait for valid, compare. The testbench drives three patterns and reports pass or fail.
// uart_loopback.v -- parameterized UART TX + RX, wired back-to-back
// Build & run: iverilog -g2005 -o sim uart_loopback.v && vvp sim
module uart_tx #(parameter integer CLKS_PER_BIT = 8, parameter integer DATA_BITS = 8) (
input wire clk,
input wire rst_n,
input wire start, // 1-cycle pulse: latch tx_data and send
input wire [DATA_BITS-1:0] tx_data,
output reg tx, // serial line, idles high
output reg busy,
output reg done // 1-cycle pulse at end of stop bit
);
localparam [1:0] IDLE=2'd0, START=2'd1, DATA=2'd2, STOP=2'd3;
localparam integer CW = (CLKS_PER_BIT < 2) ? 1 : $clog2(CLKS_PER_BIT);
localparam integer BW = (DATA_BITS < 2) ? 1 : $clog2(DATA_BITS);
reg [1:0] state; reg [CW-1:0] cnt; reg [BW-1:0] idx; reg [DATA_BITS-1:0] sh;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state<=IDLE; tx<=1'b1; busy<=1'b0; done<=1'b0;
cnt<=0; idx<=0; sh<=0;
end else begin
done <= 1'b0;
case (state)
IDLE: begin tx<=1'b1; cnt<=0; idx<=0;
if (start) begin sh<=tx_data; busy<=1'b1; state<=START; end end
START: begin tx<=1'b0; // start bit
if (cnt==CLKS_PER_BIT-1) begin cnt<=0; state<=DATA; end
else cnt<=cnt+1'b1; end
DATA: begin tx<=sh[idx]; // 8 data bits, LSB first
if (cnt==CLKS_PER_BIT-1) begin cnt<=0;
if (idx==DATA_BITS-1) begin idx<=0; state<=STOP; end
else idx<=idx+1'b1;
end else cnt<=cnt+1'b1; end
STOP: begin tx<=1'b1; // stop bit
if (cnt==CLKS_PER_BIT-1) begin cnt<=0; busy<=1'b0; done<=1'b1; state<=IDLE; end
else cnt<=cnt+1'b1; end
endcase
end
end
endmodule
module uart_rx #(parameter integer CLKS_PER_BIT = 8, parameter integer DATA_BITS = 8) (
input wire clk,
input wire rst_n,
input wire rx, // async serial input
output reg [DATA_BITS-1:0] rx_data,
output reg valid // 1-cycle pulse when byte is ready
);
localparam [1:0] IDLE=2'd0, START=2'd1, DATA=2'd2, STOP=2'd3;
localparam integer CW = (CLKS_PER_BIT < 2) ? 1 : $clog2(CLKS_PER_BIT);
localparam integer BW = (DATA_BITS < 2) ? 1 : $clog2(DATA_BITS);
reg s1, s2; // 2-flop synchronizer
reg [1:0] state; reg [CW-1:0] cnt; reg [BW-1:0] idx;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin s1<=1'b1; s2<=1'b1; end
else begin s1<=rx; s2<=s1; end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin state<=IDLE; cnt<=0; idx<=0; rx_data<=0; valid<=1'b0; end
else begin
valid <= 1'b0;
case (state)
IDLE: begin cnt<=0; idx<=0; if (s2==1'b0) state<=START; end
START: begin // confirm start at its centre
if (cnt==(CLKS_PER_BIT-1)/2) begin
if (s2==1'b0) begin cnt<=0; state<=DATA; end
else state<=IDLE; // glitch, not a real start
end else cnt<=cnt+1'b1; end
DATA: begin // sample each bit at its centre
if (cnt==CLKS_PER_BIT-1) begin cnt<=0; rx_data[idx]<=s2;
if (idx==DATA_BITS-1) begin idx<=0; state<=STOP; end
else idx<=idx+1'b1;
end else cnt<=cnt+1'b1; end
STOP: begin
if (cnt==CLKS_PER_BIT-1) begin cnt<=0; valid<=1'b1; state<=IDLE; end
else cnt<=cnt+1'b1; end
endcase
end
end
endmodule
module tb_uart_loopback;
localparam integer CLKS_PER_BIT = 8, DATA_BITS = 8;
reg clk=1'b0, rst_n=1'b0, start=1'b0;
reg [DATA_BITS-1:0] tx_data=0;
wire line, busy, done, valid;
wire [DATA_BITS-1:0] rx_data;
integer errors = 0;
uart_tx #(CLKS_PER_BIT, DATA_BITS) TX (.clk(clk),.rst_n(rst_n),.start(start),
.tx_data(tx_data),.tx(line),.busy(busy),.done(done));
uart_rx #(CLKS_PER_BIT, DATA_BITS) RX (.clk(clk),.rst_n(rst_n),.rx(line),
.rx_data(rx_data),.valid(valid));
always #5 clk = ~clk; // 100 MHz
task send_and_check(input [DATA_BITS-1:0] b);
begin
@(posedge clk); tx_data=b; start=1'b1;
@(posedge clk); start=1'b0;
@(posedge valid); @(posedge clk);
if (rx_data===b)
$display("[%0t] sent 0x%02h -> received 0x%02h OK", $time, b, rx_data);
else begin
$display("[%0t] sent 0x%02h -> received 0x%02h MISMATCH", $time, b, rx_data);
errors = errors + 1;
end
repeat (2*CLKS_PER_BIT) @(posedge clk);
end
endtask
initial begin
$display("UART loopback, CLKS_PER_BIT=%0d, %0d data bits", CLKS_PER_BIT, DATA_BITS);
repeat (3) @(posedge clk); rst_n = 1'b1;
send_and_check(8'h55);
send_and_check(8'hA5);
send_and_check(8'h3C);
if (errors==0) $display("LOOPBACK PASS: all bytes matched");
else $display("LOOPBACK FAIL: %0d mismatch(es)", errors);
$finish;
end
endmoduleUART loopback, CLKS_PER_BIT=8, 8 data bits [845] sent 0x55 -> received 0x55 OK [1825] sent 0xa5 -> received 0xa5 OK [2805] sent 0x3c -> received 0x3c OK LOOPBACK PASS: all bytes matched /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpeoy4gi48/uart_loopback.v:123: $finish called at 2965 (1s)
How the Verilog works
The frame
An idle UART line sits high. A frame begins with a start bit (a low), followed by the data bits sent least significant bit first, and closes with a stop bit (a high) that returns the line to idle. This design uses one start bit, eight data bits and one stop bit with no parity; parity is a small addition once the core works. Because there is no shared wire for a clock, timing is the whole game: the receiver must know how long one bit lasts and where its centre is.
Baud timing
The single most important number is CLKS_PER_BIT, the count of system clocks in one bit period. For a 100 MHz clock at 115200 baud it is 100000000 / 115200, about 868; for 50 MHz it is about 434. Round to the nearest integer and keep the resulting baud error under roughly 2–3 percent so the sampling point does not drift out of the bit by the stop bit. In simulation we set the divisor to 4 and 8 purely so a byte completes in a few hundred cycles and the log stays short.
The transmitter
The transmitter is a four-state machine. In IDLE the line is held high; a start pulse latches tx_data into a shift source and raises busy. START drives a low for one full bit period, DATA drives sh[idx] for each of the eight bits (incrementing idx once per period), and STOP drives a high and emits a one-cycle done pulse. The counter cnt measures each bit period exactly, so every bit is held for CLKS_PER_BIT clocks.
The receiver
Incoming rx is asynchronous to the local clock, so it first passes through a two-flop synchronizer (s1, s2) to let any metastable value settle before the logic uses it. IDLE watches for the falling edge of a start bit. START waits to the centre of that bit, (CLKS_PER_BIT-1)/2 clocks in, and re-checks that the line is still low to reject a glitch. DATA then samples s2 at the centre of each bit and shifts it into rx_data[idx]; sampling mid-bit leaves the maximum margin on either side for small frequency mismatch. STOP waits one more period and raises valid for a single cycle.
Parameters and ports
Both CLKS_PER_BIT and DATA_BITS are parameters, and the counter widths are derived with $clog2, so the same RTL retargets any clock, baud rate or word size without edits. Override the values at instantiation.
| Name | Kind | Purpose |
|---|---|---|
clk / rst_n | input | System clock and active-low reset |
start | input | One-cycle pulse that begins a transmit |
tx_data | input | Byte to transmit, latched on start |
tx / busy / done | output | Serial line, active flag, end-of-frame pulse |
rx | input | Serial line into the receiver |
rx_data / valid | output | Recovered byte and its ready pulse |
CLKS_PER_BIT | parameter | System clocks per bit period |
DATA_BITS | parameter | Bits per word (8 here) |
The loopback test
The testbench ties tx to rx through a single wire called line, so whatever the transmitter sends is exactly what the receiver sees. send_and_check pulses start, waits for valid, and compares the recovered byte with the one sent, counting mismatches. The vectors 0x55 (01010101) force every bit to toggle, while 0xA5 and 0x3C are asymmetric so an LSB-first versus MSB-first mistake would show up immediately.
Common mistakes
- Sampling at the bit edge instead of the centre. Edges are where the line is transitioning and timing is least certain. Always sample mid-bit, as the
(CLKS_PER_BIT-1)/2offset does here. - Dropping the synchronizer on
rx. A raw asynchronous input into your sampling flops invites metastability and rare, maddening corruption on silicon. Keep the two-flop chain. - Wrong bit order. UART is LSB first. A loopback where both ends agree can still “pass” while failing against a real PC, so test with asymmetric bytes.
- A non-integer or badly rounded
CLKS_PER_BIT. Accumulated phase error walks the sample point off the bit; keep baud error small and derive the value from the actual clock. - Hard-coded counter widths. A fixed width overflows for large divisors. Let
$clog2size the counters from the parameters. - Treating
done/validas levels. They are one-cycle pulses; latch them or downstream logic may react twice. - Testbench deadlock. The transmitter finishes its stop bit before the receiver finishes sampling, so waiting on
doneaftervalidcan hang. Sequence waits in the order events actually occur.
Quick check
Why does the receiver sample each bit at its centre rather than at the edge?
The edges are where the line changes level and timing is most uncertain. The centre is the point farthest from both edges, so it gives the largest margin against baud-rate mismatch and jitter; a small frequency error still lands the sample on the correct level.
For a 50 MHz clock at 115200 baud, what is CLKS_PER_BIT and how is it found?
CLKS_PER_BIT = f_clk / baud = 50000000 / 115200 which is about 434 (rounded to the nearest integer). The remaining fractional error is small enough that mid-bit sampling still recovers every bit of the frame.
Why does rx need a two-flop synchronizer while tx does not?
rx arrives from another clock domain and can change near a local clock edge, risking metastability; two flops let any unsettled value resolve before use. tx is generated by this clock, so it is already synchronous on the way out and needs no synchronizer.
What levels are the start and stop bits, and in what order are the data bits sent?
The line idles high. The start bit is a logic 0 (low), the stop bit is a logic 1 (high), and the data bits are sent least significant bit first.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses