Module 10: Capstone Projects · Project 2 — UART Controller

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.

← Project 2, Part 3: The Transmitter and Receiver FSMs Project 3, Part 1: SPI and Its Four Modes →