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
endmodule
CLKS_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
endmodule
UART 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.

NameKindPurpose
clk / rst_ninputSystem clock and active-low reset
startinputOne-cycle pulse that begins a transmit
tx_datainputByte to transmit, latched on start
tx / busy / doneoutputSerial line, active flag, end-of-frame pulse
rxinputSerial line into the receiver
rx_data / validoutputRecovered byte and its ready pulse
CLKS_PER_BITparameterSystem clocks per bit period
DATA_BITSparameterBits 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)/2 offset 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 $clog2 size the counters from the parameters.
  • Treating done/valid as 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 done after valid can 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.

Have questions about this topic?
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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD