Project 2, Part 4: The Loopback Test
The last three articles built the baud generators, the transmitter and the receiver. This one assembles them and verifies the result the most natural way a UART can be verified on its own: loopback. Connect the transmitter’s serial output to the receiver’s serial input, send a byte, and confirm the byte that comes out matches the byte that went in.
The complete UART module
module uart_top #(
parameter CLK_FREQ = 50_000_000,
parameter BAUD_RATE = 115200
) (
input logic clk, rst_n,
input logic [7:0] tx_data_in,
input logic tx_start,
output logic tx_busy,
output logic [7:0] rx_data_out,
output logic rx_valid,
output logic rx_framing_error,
output logic serial_line // this project's TX-to-RX loopback point
);
logic baud_tick, tick_16x;
baud_tick_gen #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_tx_baud (.clk(clk), .rst_n(rst_n), .tick(baud_tick));
baud_tick_gen_16x #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_rx_baud (.clk(clk), .rst_n(rst_n), .tick_16x(tick_16x));
uart_tx u_tx (
.clk(clk), .rst_n(rst_n), .baud_tick(baud_tick),
.tx_data_in(tx_data_in), .tx_start(tx_start),
.tx_busy(tx_busy), .tx_line(serial_line)
);
uart_rx u_rx (
.clk(clk), .rst_n(rst_n), .tick_16x(tick_16x),
.rx_line(serial_line), // the loopback: RX watches exactly what TX drives
.rx_data_out(rx_data_out), .rx_valid(rx_valid), .framing_error(rx_framing_error)
);
endmodule
serial_line is the whole test setup in one wire. The transmitter drives it, the receiver watches it, and no external fixture is needed to exercise the complete transmit-then-receive path.
The directed loopback test
module tb_uart_loopback;
logic clk = 0, rst_n;
logic [7:0] tx_data_in;
logic tx_start, tx_busy;
logic [7:0] rx_data_out;
logic rx_valid, rx_framing_error;
logic serial_line;
uart_top #(.CLK_FREQ(50_000_000), .BAUD_RATE(115200)) dut (
.clk(clk), .rst_n(rst_n), .tx_data_in(tx_data_in), .tx_start(tx_start),
.tx_busy(tx_busy), .rx_data_out(rx_data_out), .rx_valid(rx_valid),
.rx_framing_error(rx_framing_error), .serial_line(serial_line)
);
always #10 clk = ~clk; // 50 MHz, matching the DUT's CLK_FREQ parameter
task automatic send_and_check(bit [7:0] byte_to_send);
tx_data_in = byte_to_send;
tx_start = 1'b1;
wait (tx_busy); tx_start = 1'b0; // hold the request until the transmitter has taken it
@(posedge rx_valid); // block until the receiver reports a completed byte
@(posedge clk);
if (rx_data_out == byte_to_send && !rx_framing_error)
$display("PASS: sent %h, received %h", byte_to_send, rx_data_out);
else
$display("FAIL: sent %h, received %h, framing_error=%b",
byte_to_send, rx_data_out, rx_framing_error);
endtask
initial begin
$dumpfile("tb.vcd");
$dumpvars(0, tb_uart_loopback);
rst_n = 0; tx_start = 0; tx_data_in = 0;
repeat (3) @(posedge clk); rst_n = 1;
send_and_check(8'h55); // alternating bits: exercises every bit transition
send_and_check(8'h00); // all zeros: no data-bit transitions at all
send_and_check(8'hFF); // all ones: stresses the idle-versus-data distinction
$finish;
end
endmodule
The three bytes are chosen, not arbitrary, in the directed-testing spirit of Section 6.4. 0x55 (01010101) exercises every bit transition within a byte. 0x00 and 0xFF are the extremes where the data bits never toggle relative to their neighbours, the case most likely to expose a bit-counting error that a constantly toggling pattern would mask. Note that tx_start is held until tx_busy confirms the transmitter has accepted it, because the transmitter only looks at the request on a baud tick, which can be hundreds of clock cycles away.
Extending to randomized coverage
Three bytes prove three cases, which is the structural limit of directed testing that Section 9.2 discussed. A randomized extension reusing the same task costs almost nothing and covers far more of the input space:
class uart_byte_txn;
rand bit [7:0] data;
endclass
initial begin
uart_byte_txn t = new();
// ... after the three directed cases above ...
repeat (50) begin
t.randomize();
send_and_check(t.data);
end
end
Fifty more bytes on top of the three directed ones, for no more authoring effort than the class declaration. Loopback testing is also a real bring-up technique: many microcontrollers have a hardware loopback mode for exactly this purpose, letting an engineer verify a UART’s basic transmit and receive without a second device or a cable.
Practice Exercise
Modify the testbench to misconfigure the receiver’s baud rate, say 5 percent faster than the transmitter’s, and predict before running it whether rx_framing_error will assert, using the drift discussion from Part 1.
Solution
With the receiver’s generator 5 percent fast, each successive sample lands progressively earlier relative to the true bit centre. Over a frame of about 10 bit periods (start, 8 data, stop) a 5 percent per-bit drift compounds to roughly half a bit period by the time the stop bit is sampled, right at the edge of what a receiver can absorb. You would expect rx_framing_error to assert at least intermittently, and on every frame once the drift is large enough that the stop-bit sample lands outside the stop bit’s high period. Running it turns Part 1’s claim about baud rate mismatch into a simulation result rather than something taken on faith.
A loopback test, transmitter output wired to receiver input, is the natural self-contained way to verify a UART’s core correctness, reusing the Module 6 self-checking task pattern with three deliberately chosen bytes. Adding the Section 9.2 randomized transactions gives far more coverage for almost no effort. That closes Project 2. In the next article, Project 3 moves to a different serial protocol, SPI, which solves UART’s clock-mismatch problem by doing the one thing UART avoids: sharing a clock line.