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.
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_reqsignal to initiate transmission. - Drive a single-wire output:
tx. - Provide a
tx_busysignal indicating ongoing transmission. - Provide a
tx_donepulse 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
initialblocks, 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_busyis low.
1.7 State Machine
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
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_WIDTHorBAUD_DIVadapts 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
initialblocks, 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:
No comments:
Post a Comment