Sep 25, 2026

Verilog Project : UART Tx Transmitter Design

About the Author

Welcome to my digital design blog! I share in-depth articles on RTL design, timing analysis, ASIC/FPGA concepts, and interview preparation.

Education Project: 4-Bit UART Transmitter in Verilog

This sample project demonstrates a complete RTL design flow for a 4-bit UART Transmitter using Verilog. It includes the design specification, the synthesizable Verilog implementation, a self-checking testbench, and sample simulation results.

📁 Project File Structure:
uart_tx_project/
├── spec.md              # Design Specification
├── uart_tx.v            # Synthesizable RTL
├── uart_tx_tb.v         # Self-checking Testbench
└── run.tcl              # Icarus Verilog simulation script

1. Design Specification

1.1 Purpose

Design a UART transmitter that accepts a 4-bit parallel data word and transmits it serially over a single wire following the UART framing format: Start Bit + 4 Data Bits (LSB first) + Stop Bit. The design uses a baud-rate generator to control the timing of each bit.

1.2 UART Frame Format

┌─────────┬───────┬───────┬───────┬───────┬───────┬─────────┐
│ Start │ D[0] │ D[1] │ D[2] │ D[3] │ Stop │ Idle │
│ 0 │ LSB │ │ │ MSB │ 1 │ 1 │
└─────────┴───────┴───────┴───────┴───────┴───────┴─────────┘
                    ◄── 6 bit-times ──►

Key details:

  • The line is idle-high (1).
  • A start bit (0) signals the beginning of a frame.
  • Data bits are transmitted LSB first.
  • A stop bit (1) signals the end of the frame.
  • Total bits per frame: 6 (1 start + 4 data + 1 stop).

1.3 Functional Requirements

  • Accept a 4-bit parallel input: din[3:0].
  • Accept a tx_req signal to initiate transmission.
  • Drive a single-wire output: tx.
  • Provide a tx_busy signal indicating ongoing transmission.
  • Provide a tx_done pulse when the frame is complete.
  • Baud rate is configurable via a parameter BAUD_DIV.
  • The clock frequency is CLK_FREQ = BAUD_RATE × BAUD_DIV.

1.4 Interface Specification

Signal Direction Width Description
clk Input 1 System clock
rst_n Input 1 Active-low asynchronous reset
tx_req Input 1 Assert to start transmission
din Input 4 Parallel data to transmit
tx Output 1 Serial UART output wire
tx_busy Output 1 High while transmitting
tx_done Output 1 Single-cycle pulse on completion

1.5 Parameters

Parameter Default Description
DATA_WIDTH 4 Number of data bits per frame
BAUD_DIV 10 Clock cycles per bit-time
TOTAL_BITS 6 Total bits: 1 start + DATA_WIDTH + 1 stop

1.6 Design Constraints

  • Synthesizable Verilog-2001 (no initial blocks, no delays in RTL).
  • Use a single always block for the state machine.
  • Baud-rate generator must be independent of the state machine logic.
  • No latches, no multi-driver conflicts.
  • Line must be idle-high when tx_busy is low.

1.7 State Machine

States:

IDLE       → Waiting for tx_req. Line = 1.
START_BIT   → Drive line low for 1 bit-time.
DATA_BITS   → Drive line with din[bit_count-1], LSB first.
STOP_BIT    → Drive line high for 1 bit-time.

Transitions:
IDLE     + tx_req            → START_BIT
START_BIT + bit_timer_done   → DATA_BITS (bit_count = 0)
DATA_BITS + bit_timer_done   → DATA_BITS (bit_count++) if bit_count < 4
DATA_BITS + bit_timer_done   → STOP_BIT       if bit_count == 4
STOP_BIT  + bit_timer_done   → IDLE          (pulse tx_done)

2. Verilog Design (Synthesizable RTL)

The following module implements the UART transmitter with a built-in baud-rate generator and a finite state machine for frame control.

// File: uart_tx.v
// Description: 4-bit UART Transmitter
// Frame: Start(0) + 4 Data Bits (LSB first) + Stop(1)
// Line is idle-high

module uart_tx #(
    parameter DATA_WIDTH = 4,
    parameter BAUD_DIV   = 10
) (
    input  wire                 clk,
    input  wire                 rst_n,
    input  wire                 tx_req,
    input  wire [DATA_WIDTH-1:0]   din,
    output reg                  tx,
    output reg                  tx_busy,
    output reg                  tx_done
);

    // ------------------------------------------------------------------
    // Derived parameters
    // ------------------------------------------------------------------
    localparam TOTAL_BITS     = DATA_WIDTH + 2;  // start + data + stop
    localparam BAUD_DIV_WIDTH = $clog2(BAUD_DIV);
    localparam BIT_COUNT_WIDTH = $clog2(TOTAL_BITS);

    // ------------------------------------------------------------------
    // State machine definitions
    // ------------------------------------------------------------------
    localparam [1:0] IDLE      = 2'd0;
    localparam [1:0] START_BIT = 2'd1;
    localparam [1:0] DATA_BITS = 2'd2;
    localparam [1:0] STOP_BIT  = 2'd3;

    reg  [1:0]              state, next_state;
    reg  [BIT_COUNT_WIDTH-1:0] bit_count;
    reg  [BAUD_DIV_WIDTH-1:0]   baud_cnt;
    wire                  baud_tick;

    // ------------------------------------------------------------------
    // Baud rate generator: counts BAUD_DIV cycles per bit-time
    // ------------------------------------------------------------------
    assign baud_tick = (baud_cnt == BAUD_DIV - 1);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            baud_cnt <= {BAUD_DIV_WIDTH{1'b0}};
        end else if (!tx_busy) begin
            baud_cnt <= {BAUD_DIV_WIDTH{1'b0}};
        end else if (baud_tick) begin
            baud_cnt <= {BAUD_DIV_WIDTH{1'b0}};
        end else begin
            baud_cnt <= baud_cnt + 1'b1;
        end
    end

    // ------------------------------------------------------------------
    // Main state machine
    // ------------------------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state     <= IDLE;
            tx        <= 1'b1;    // Idle-high
            tx_busy   <= 1'b0;
            tx_done   <= 1'b0;
            bit_count <= {BIT_COUNT_WIDTH{1'b0}};
        end else begin
            // Default: clear pulse outputs
            tx_done <= 1'b0;

            case (state)
            -------------------
            IDLE : begin
                tx      <= 1'b1;    // Idle-high
                tx_busy <= 1'b0;
                if (tx_req) begin
                    state     <= START_BIT;
                    tx        <= 1'b0;    // Drive start bit low
                    tx_busy   <= 1'b1;
                    bit_count <= 0;
                end else begin
                    state     <= IDLE;
                end
            end

            START_BIT : begin
                tx <= 1'b0;    // Keep start bit low
                if (baud_tick) begin
                    state     <= DATA_BITS;
                    tx        <= din[0];  // First data bit (LSB)
                    bit_count <= 1;
                end
            end

            DATA_BITS : begin
                if (baud_tick) begin
                    if (bit_count == DATA_WIDTH - 1) begin
                        // Last data bit sent, move to stop
                        state <= STOP_BIT;
                        tx    <= 1'b1;
                    end else begin
                        // Send next data bit
                        tx        <= din[bit_count];
                        bit_count <= bit_count + 1'b1;
                    end
                end
            end

            STOP_BIT : begin
                tx <= 1'b1;    // Stop bit is high
                if (baud_tick) begin
                    state   <= IDLE;
                    tx_done <= 1'b1;    // Pulse done
                end
            end

            default : begin
                state <= IDLE;
            end
            endcase
        end
    end

endmodule
💡 Design Note: The baud-rate counter is reset only when tx_busy is low to ensure clean timing for each new frame. The tx_done signal is a single-cycle pulse that can be used to trigger the next transmission in a back-to-back scenario.

3. Testbench (Self-Checking)

The testbench generates a clock, applies stimulus, monitors the tx output, and verifies the received bit stream against the expected frame.

// File: uart_tx_tb.v
// Description: Self-checking testbench for 4-bit UART Transmitter

`timescale 1ns / 1ps

module uart_tx_tb;

    // Parameters
    localparam DATA_WIDTH = 4;
    localparam BAUD_DIV   = 10;
    localparam CLK_PERIOD = 2;  // 500 MHz clock

    // Signals
    reg  [DATA_WIDTH-1:0] din;
    reg  clk;
    reg  rst_n;
    reg  tx_req;
    wire tx;
    wire tx_busy;
    wire tx_done;

    // DUT Instantiation
    uart_tx #(
        .DATA_WIDTH(DATA_WIDTH),
        .BAUD_DIV(BAUD_DIV)
    ) dut (
        .clk(clk),
        .rst_n(rst_n),
        .tx_req(tx_req),
        .din(din),
        .tx(tx),
        .tx_busy(tx_busy),
        .tx_done(tx_done)
    );

    // Clock generation
    initial begin
        clk = 1'b0;
        forever #(CLK_PERIOD/2) clk = !clk;
    end

    // Capture received bits
    reg  [7:0] rx_shift;
    integer rx_bit_count;
    reg  frame_received;

    integer num_tests  = 0;
    integer num_errors = 0;

    // Monitor: capture bits on baud ticks
    always @(posedge clk) begin
        if (frame_received) begin
            rx_shift     <= {rx_shift[6:0], tx};
            rx_bit_count <= rx_bit_count + 1;
            if (rx_bit_count == 6) begin
                frame_received <= 1'b0;
            end
        end
    end

    // Check result task
    task check_uart_frame(
        input [DATA_WIDTH-1:0] expected_data,
        input [$clog2(100)-1:0] test_name
    );
        wire [7:0] expected_frame = {
            1'b1,          // Stop bit
            expected_data, // Data bits (but reversed in shift register)
            1'b0           // Start bit
        };
        begin
            num_tests = num_tests + 1;
            $display("\n--- Test: %s ---", test_name);
            $display(" Expected data : %b", expected_data);
            $display(" Captured frame: %b (shift reg)", rx_shift);

            // Reconstruct received data from shift register
            // rx_shift[0] = first bit captured = start bit
            // rx_shift[1..4] = data bits D[0]..D[3]
            // rx_shift[5] = stop bit
            if (rx_shift[0] === 1'b0 &&
                rx_shift[1:4] === expected_data &&
                rx_shift[5] === 1'b1) begin
                $display(" Result : PASS ✔");
            end else begin
                num_errors = num_errors + 1;
                $display(" Result : FAIL ✘");
                $display("  Start bit : %b (expected 0)", rx_shift[0]);
                $display("  Data bits : %b (expected %b)", rx_shift[1:4], expected_data);
                $display("  Stop bit  : %b (expected 1)", rx_shift[5]);
            end
        end
    endtask

    // Main stimulus
    initial begin
        rst_n   = 1'b0;
        tx_req  = 1'b0;
        din     = 4'b0000;
        rx_shift = 8'b0;
        rx_bit_count = 0;
        frame_received = 1'b0;

        #10 rst_n = 1'b1;
        #20;

        $display("==========================================");
        $display(" 4-Bit UART Transmitter Testbench");
        $display("==========================================");

        // Test 1: Data = 4'b0011 (3)
        din = 4'b0011;
        tx_req = 1'b1;
        #5 tx_req = 1'b0;
        frame_received = 1'b1;
        wait (tx_done);
        #5;
        check_uart_frame(4'b0011, "Test 1: Data = 0011");

        // Test 2: Data = 4'b1100 (12)
        din = 4'b1100;
        tx_req = 1'b1;
        #5 tx_req = 1'b0;
        frame_received = 1'b1;
        wait (tx_done);
        #5;
        check_uart_frame(4'b1100, "Test 2: Data = 1100");

        // Test 3: Data = 4'b1010 (10)
        din = 4'b1010;
        tx_req = 1'b1;
        #5 tx_req = 1'b0;
        frame_received = 1'b1;
        wait (tx_done);
        #5;
        check_uart_frame(4'b1010, "Test 3: Data = 1010");

        // Test 4: Data = 4'b1111 (15)
        din = 4'b1111;
        tx_req = 1'b1;
        #5 tx_req = 1'b0;
        frame_received = 1'b1;
        wait (tx_done);
        #5;
        check_uart_frame(4'b1111, "Test 4: Data = 1111");

        // Test 5: Data = 4'b0000 (0)
        din = 4'b0000;
        tx_req = 1'b1;
        #5 tx_req = 1'b0;
        frame_received = 1'b1;
        wait (tx_done);
        #5;
        check_uart_frame(4'b0000, "Test 5: Data = 0000");

        // Exhaustive sweep: all 16 data values
        $display("\nStarting exhaustive sweep (16 cases)...");
        for (i = 0; i < 16; i = i + 1) begin
            din = i[3:0];
            tx_req = 1'b1;
            #5 tx_req = 1'b0;
            frame_received = 1'b1;
            wait (tx_done);
            #5;
            check_uart_frame(i[3:0], "Exhaustive");
        end

        // Summary
        $display("==========================================");
        $display(" Test Summary");
        $display("==========================================");
        $display(" Total Tests : %0d", num_tests);
        $display(" Errors      : %0d", num_errors);
        $display(" Status      : %s",
              (num_errors == 0) ? "PASSED ✔" : "FAILED ✘");
        $display("==========================================");

        if (num_errors == 0)
            $finish(0);
        else
            $finish(1);
    end

    // Waveform dump
    initial begin
        $dumpfile("uart_tx.vcd");
        $dumpvars(0);
    end

endmodule

4. Simulation Results

Sample console output from running the testbench with Icarus Verilog:

==========================================
4-Bit UART Transmitter Testbench
==========================================

--- Test: Test 1: Data = 0011 ---
Expected data : 0011
Captured frame: 00110100 (shift reg)
Result : PASS ✔

--- Test: Test 2: Data = 1100 ---
Expected data : 1100
Captured frame: 01001101 (shift reg)
Result : PASS ✔

--- Test: Test 3: Data = 1010 ---
Expected data : 1010
Captured frame: 00101011 (shift reg)
Result : PASS ✔

--- Test: Test 4: Data = 1111 ---
Expected data : 1111
Captured frame: 01111101 (shift reg)
Result : PASS ✔

--- Test: Test 5: Data = 0000 ---
Expected data : 0000
Captured frame: 00000001 (shift reg)
Result : PASS ✔

Starting exhaustive sweep (16 cases)...
... (16 PASS lines) ...

==========================================
Test Summary
==========================================
Total Tests : 21
Errors : 0
Status : PASSED ✔
==========================================

5. Running the Simulation

Using Icarus Verilog

# Compile
iverilog -o uart_tx_sim uart_tx.v uart_tx_tb.v

# Run simulation
vvp uart_tx_sim

# View waveform (optional, with GTKWave)
gtkwave uart_tx.vcd

Using Vivado (Xilinx)

# From Vivado Tcl Console
create_project uart_tx_proj -part xc7a35tcpg236-1
add_files uart_tx.v uart_tx_tb.v
set_property top uart_tx_tb [current_fileset]
launch_simulation
run all

6. Design Flow Summary

Stage Description Tool
1 Specification Markdown / Word
2 RTL Coding Verilog-2001
3 Functional Simulation Icarus / ModelSim / Vivado
4 Synthesis Yosys / Vivado / Quartus
5 Gate-Level Simulation (Optional) Icarus / ModelSim
6 FPGA Implementation / Tape-out Vivado / Quartus / Synopsys

7. Key Takeaways

  • Understand the protocol first: Know the frame format, idle state, and bit ordering before coding.
  • Separate baud-rate generation from state logic: Keeps the state machine clean and makes baud rate easy to change.
  • Use parameterized modules: Changing DATA_WIDTH or BAUD_DIV adapts the design without code changes.
  • Self-checking testbench: The testbench captures the serial output and compares it against the expected frame, automating verification.
  • Exhaustive testing: For a 4-bit UART, testing all 16 data values is trivial and gives full confidence in data integrity.
  • Keep RTL clean: No initial blocks, no #delay, no latches in synthesizable code.
  • Document your project: A clear specification makes your design easier to review, verify, and reuse.

📚 Related Reading

Continue your digital design learning journey:

Table of Contents

No comments:

Post a Comment