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

The Comparison

The model says what the answer should be; the comparison decides whether the design agreed. Get it wrong and a test passes when it should not, or fails when it should not, and both are worse than no test: one gives false confidence, the other sends you after bugs that are not there.

== and ===

In RTL you compare with ==. In a testbench, use ===. With four-valued logic, == returns x when either operand has an x or z bit, and an x in an if counts as false:

actual   = 8'hxx;      // an uninitialised register
expected = 8'h42;

if (actual != expected)          // x, treated as false
    $error("mismatch");          // never fires; the test passes

=== treats x and z as values: x === x is 1, x === 0 is 0.

if (actual !== expected)         // 1
    $error("mismatch");          // fires

An uninitialised register, a floating output or a reset that did not reach everything all produce x, and a testbench using != passes every one of them. If the testbench says PASSED and the board does not work, the first thing to check is the comparison operators.

A check task

task automatic check_result;
    input [7:0]  test_id;
    input [15:0] expected;
    input [15:0] actual;
    inout integer error_count;
    begin
        if (actual !== expected) begin
            $error("[test %0d] expected=0x%0h actual=0x%0h", test_id, expected, actual);
            error_count = error_count + 1;
        end
    end
endtask

inout lets the task update the caller’s counter. automatic gives each call its own storage, which matters if calls ever overlap in a fork, and is a good habit anyway. The message has the three fields.

Exhaustive, 8 bits

`timescale 1ns / 1ps

module tb_multiplier_exhaustive();
    reg  [7:0]  a, b;
    wire [15:0] product;
    integer     error_count, test_count, 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

    task automatic check_result;
        input [7:0]  t_a, t_b;
        input [15:0] t_expected, t_actual;
        inout integer errs;
        begin
            if (t_actual !== t_expected) begin
                $error("[a=%0d b=%0d] expected=%0d actual=%0d", t_a, t_b, t_expected, t_actual);
                errs = errs + 1;
            end
        end
    endtask

    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;
                check_result(a, b, expected, product, error_count);
            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

Complete and self-contained. It says whether the multiplier is right and, if not, which inputs show it. Why is the waveform’s job.

Structured, 32 bits

Two 32-bit inputs cannot be swept. Choose values that stand for the interesting behaviours: zero, all ones, the sign bit alone, the largest positive, alternating patterns, powers of two, and combine each with itself, with zero, with all ones, and with its complement:

`timescale 1ns / 1ps

module tb_alu_32bit_structured();
    reg  [31:0] a, b;
    reg  [2:0]  op;
    wire [31:0] result;
    integer     error_count, test_count, k;

    alu_32bit dut (.a(a), .b(b), .op(op), .result(result));

    function automatic [31:0] ref_alu;
        input [31:0] fa, fb;
        input [2:0]  fop;
        begin
            case (fop)
                3'b000:  ref_alu = fa + fb;
                3'b001:  ref_alu = fa & fb;
                3'b010:  ref_alu = fa ^ fb;
                default: ref_alu = 32'hx;
            endcase
        end
    endfunction

    task automatic check_op;
        input [31:0] t_a, t_b;
        input [2:0]  t_op;
        inout integer errs;
        reg   [31:0] t_expected;
        begin
            a = t_a; b = t_b; op = t_op;
            #10;
            t_expected = ref_alu(t_a, t_b, t_op);
            test_count = test_count + 1;
            if (result !== t_expected) begin
                $error("[op=%0b a=0x%0h b=0x%0h] expected=0x%0h actual=0x%0h", t_op, t_a, t_b, t_expected, result);
                errs = errs + 1;
            end
        end
    endtask

    reg [31:0] corner [0:11];

    initial begin
        error_count = 0;
        test_count  = 0;

        corner[0]  = 32'h0000_0000;
        corner[1]  = 32'hFFFF_FFFF;
        corner[2]  = 32'h8000_0000;
        corner[3]  = 32'h7FFF_FFFF;
        corner[4]  = 32'h0000_0001;
        corner[5]  = 32'hFFFF_FFFE;
        corner[6]  = 32'h0000_FFFF;
        corner[7]  = 32'hFFFF_0000;
        corner[8]  = 32'h5555_5555;
        corner[9]  = 32'hAAAA_AAAA;
        corner[10] = 32'h0000_0100;
        corner[11] = 32'h0001_0000;

        for (k = 0; k < 12; k = k + 1) begin
            check_op(corner[k], corner[k],      3'b000, error_count);
            check_op(corner[k], 32'h0,          3'b000, error_count);
            check_op(corner[k], 32'hFFFF_FFFF,  3'b000, error_count);

            check_op(corner[k], 32'hFFFF_FFFF,  3'b001, error_count);
            check_op(corner[k], 32'h0,          3'b001, error_count);

            check_op(corner[k], corner[k],      3'b010, error_count);
            check_op(corner[k], ~corner[k],     3'b010, error_count);
        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

Each corner against the identities and the extremes covers far more behaviour per vector than random values would.

The verdict

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

A script can grep the first word; a dashboard can track the count. For a build system that only reads exit codes, $fatal(1, ...) when the count is non-zero guarantees a failing status from Icarus.

With the comparison right, the remaining question is which inputs to choose, which is the last article of this section.

โ† Reference Models Directed Tests โ†’