Module 6: Verilog for Testbench & Simulation ยท 6.4 Writing Self-Checking Tests

Reference Models

The best self-checking testbenches contain no hard-coded expected values. They contain two implementations of the same function: the RTL under test, and a simple behavioural one that is trusted more. Drive both with the same inputs and compare. Where they disagree the RTL is wrong, and you know which input showed it.

What makes a good one

A reference model has one job, the right answer for any input. It need not synthesise, be efficient, or resemble the RTL. The designer built a Wallace-tree multiplier to meet timing; the model is a * b on integers. One is complicated and might have a bug in a carry-save stage; the other is a line that is right by inspection. The model can use everything the RTL cannot: 64-bit integers, real arithmetic, loops of any length, anything the simulator runs.

The structure is two paths from the same stimulus into one comparator (Figure 1).

Reference model beside the design
Figure 1: The same inputs into the design and the model; the checker compares the outputs

A multiplier

function automatic [15:0] ref_multiply;
    input [7:0] fa, fb;
    begin
        ref_multiply = fa * fb;
    end
endfunction
`timescale 1ns / 1ps

module tb_multiplier_8bit();
    reg  [7:0]  a, b;
    wire [15:0] product;
    integer     error_count, test_count;
    integer     i, j;
    reg  [15:0] expected;

    multiplier_8bit dut (.a(a), .b(b), .product(product));

    function automatic [15:0] ref_multiply;
        input [7:0] fa, fb;
        begin
            ref_multiply = fa * fb;
        end
    endfunction

    initial begin
        error_count = 0;
        test_count  = 0;

        for (i = 0; i < 256; i = i + 1) begin
            for (j = 0; j < 256; j = j + 1) begin
                a = i[7:0];
                b = j[7:0];
                #10;
                expected   = ref_multiply(a, b);
                test_count = test_count + 1;

                assert (product === expected)
                else begin
                    $error("[test %0d] a=%0d b=%0d expected=%0d actual=%0d", test_count, i, j, expected, product);
                    error_count = error_count + 1;
                end
            end
        end

        if (error_count == 0)
            $display("PASSED: all %0d tests.", test_count);
        else
            $display("FAILED: %0d of %0d tests.", error_count, test_count);
        $finish;
    end
endmodule

Sixty-five thousand vectors and not one expected value typed by hand.

Writing the model is also a design step. To write it you have to read the specification closely enough to express it in code, and many RTL bugs are misreadings of the specification. A model written from the spec, ideally by someone other than the RTL author, catches both.

A CRC

Not every function is one operator. A CRC-8 over a message, polynomial $x^8 + x^2 + x + 1$, might be a lookup table in the RTL. The model is the textbook bit-by-bit loop:

function automatic [7:0] ref_crc8_byte;
    input [7:0] current_crc;
    input [7:0] data;
    integer     k;
    reg  [7:0]  crc;
    begin
        crc = current_crc ^ data;
        for (k = 0; k < 8; k = k + 1) begin
            if (crc[7])
                crc = (crc << 1) ^ 8'h07;
            else
                crc = crc << 1;
        end
        ref_crc8_byte = crc;
    end
endfunction
`timescale 1ns / 1ps

module tb_crc8();
    reg        clk, rst_n;
    reg  [7:0] data_byte;
    reg        data_valid;
    wire [7:0] crc_out;

    integer error_count, test_count, byte_idx;
    reg [7:0] test_message [0:7];
    reg [7:0] ref_result;

    crc8_engine dut (.clk(clk), .rst_n(rst_n), .data_byte(data_byte), .data_valid(data_valid), .crc_out(crc_out));

    initial clk = 0;
    always #5 clk = ~clk;

    function automatic [7:0] ref_crc8_byte;
        input [7:0] current_crc, data;
        integer     k;
        reg  [7:0]  crc;
        begin
            crc = current_crc ^ data;
            for (k = 0; k < 8; k = k + 1)
                crc = crc[7] ? ((crc << 1) ^ 8'h07) : (crc << 1);
            ref_crc8_byte = crc;
        end
    endfunction

    initial begin
        error_count = 0;
        test_count  = 0;

        rst_n = 0; data_byte = 8'h00; data_valid = 0;
        repeat (4) @(posedge clk);
        #2; rst_n = 1;

        test_message[0] = 8'h31; test_message[1] = 8'h32; test_message[2] = 8'h33; test_message[3] = 8'h34;
        test_message[4] = 8'h35; test_message[5] = 8'h36; test_message[6] = 8'h37; test_message[7] = 8'h38;

        // the model, over the whole message
        ref_result = 8'h00;
        for (byte_idx = 0; byte_idx < 8; byte_idx = byte_idx + 1)
            ref_result = ref_crc8_byte(ref_result, test_message[byte_idx]);

        // the design, a byte per clock
        for (byte_idx = 0; byte_idx < 8; byte_idx = byte_idx + 1) begin
            @(posedge clk); #2;
            data_byte  = test_message[byte_idx];
            data_valid = 1;
        end
        @(posedge clk); #2;
        data_valid = 0;
        @(posedge clk); #2;

        test_count = test_count + 1;
        assert (crc_out === ref_result)
        else begin
            $error("CRC mismatch: expected=0x%0h actual=0x%0h", ref_result, crc_out);
            error_count = error_count + 1;
        end

        if (error_count == 0)
            $display("PASSED: all %0d tests.", test_count);
        else
            $display("FAILED: %0d of %0d tests.", error_count, test_count);
        $finish;
    end
endmodule

The model does not care how the RTL does it. It computes what the answer must be.

A stateful model

A counter, an accumulator or a FIFO depends on its history, so the model keeps state of its own and a task advances it one cycle:

`timescale 1ns / 1ps

module tb_saturating_counter();
    reg        clk, rst_n, inc, dec;
    wire [3:0] count;

    integer error_count, test_count, i;
    integer ref_count;                     // the model's state

    sat_counter_4bit dut (.clk(clk), .rst_n(rst_n), .inc(inc), .dec(dec), .count(count));

    task ref_tick;
        input t_inc, t_dec;
        begin
            if (t_inc && !t_dec && ref_count < 15)
                ref_count = ref_count + 1;
            else if (t_dec && !t_inc && ref_count > 0)
                ref_count = ref_count - 1;
        end
    endtask

    initial clk = 0;
    always #5 clk = ~clk;

    initial begin
        error_count = 0;
        test_count  = 0;
        ref_count   = 0;

        rst_n = 0; inc = 0; dec = 0;
        repeat (3) @(posedge clk);
        #2; rst_n = 1;

        for (i = 0; i < 20; i = i + 1) begin       // up, into saturation
            @(posedge clk); #2;
            inc = 1; dec = 0;
            ref_tick(1, 0);
            @(posedge clk); #2;
            test_count = test_count + 1;
            assert (count === ref_count[3:0])
            else begin
                $error("[inc %0d] expected=%0d actual=%0d", i, ref_count, count);
                error_count = error_count + 1;
            end
        end

        inc = 0;
        for (i = 0; i < 20; i = i + 1) begin       // and down
            @(posedge clk); #2;
            dec = 1;
            ref_tick(0, 1);
            @(posedge clk); #2;
            test_count = test_count + 1;
            assert (count === ref_count[3:0])
            else begin
                $error("[dec %0d] expected=%0d actual=%0d", i, ref_count, count);
                error_count = error_count + 1;
            end
        end

        if (error_count == 0)
            $display("PASSED: all %0d tests.", test_count);
        else
            $display("FAILED: %0d of %0d tests.", error_count, test_count);
        $finish;
    end
endmodule

In commercial flows the model is often C or Python, linked through the DPI; the structure is the same. One caution: a model is a translation of the specification, and if the RTL author and the model author misread the spec the same way, both agree and the test passes. Independent authorship helps, and so does writing the model straight after reading the spec and before the RTL.

The next article is the other half: the comparison itself, and the operator that decides whether an x is caught.

โ† $error, $fatal and Assertions The Comparison โ†’