Module 6: Verilog for Testbench & Simulation ยท 6.1 Testbench Architecture

Automatic Pass and Fail

The testbench so far prints numbers and you decide whether they are right. That is fine for five cases and impossible for five thousand, and five thousand is a morning’s work on a moderate design. A self-checking testbench compares every output with a known answer at simulation speed and prints one line at the end: everything passed, or this many failed and here they are. You read the verdict, not the table.

The counter

integer error_count;
integer test_count;

Every check increments test_count, compares, and on a mismatch prints the details and increments error_count. The report at the end reads the two:

initial begin
    error_count = 0;
    test_count  = 0;

    // ... stimulus ...

    // each check
    expected   = compute_expected(inputs);
    test_count = test_count + 1;
    if (actual_output !== expected) begin
        $display("FAIL [test %0d]: input=%0d expected=%0d got=%0d", test_count, input_val, expected, actual_output);
        error_count = error_count + 1;
    end

    // 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;
end

test_count matters as much as error_count: a broken stimulus loop that runs no checks would otherwise report a pass with zero failures.

Use !==, not !=. If the output is x, from an uninitialised register say, != gives x, which an if treats as false, and the check passes silently. !== compares all four values and catches it.

All 512

`timescale 1ns / 1ps

module tb_adder_4bit_selfcheck();

    reg  [3:0] a_tb, b_tb;
    reg        cin_tb;
    wire [3:0] sum_tb;
    wire       cout_tb;

    integer   error_count, test_count;
    integer   i, j, k;
    reg [4:0] expected_full;     // {cout, sum}

    adder_4bit dut (.a(a_tb), .b(b_tb), .cin(cin_tb), .sum(sum_tb), .cout(cout_tb));

    initial begin
        $dumpfile("tb_adder_4bit_selfcheck.vcd");
        $dumpvars(0, tb_adder_4bit_selfcheck);

        error_count = 0;
        test_count  = 0;

        for (k = 0; k < 2; k = k + 1) begin              // cin
            for (i = 0; i < 16; i = i + 1) begin         // a
                for (j = 0; j < 16; j = j + 1) begin     // b
                    a_tb   = i[3:0];
                    b_tb   = j[3:0];
                    cin_tb = k[0];
                    #10;

                    expected_full = i + j + k;           // the reference: integer arithmetic
                    test_count    = test_count + 1;

                    if ({cout_tb, sum_tb} !== expected_full) begin
                        $display("FAIL [test %0d]: %0d + %0d + %0d = %0d (expected %0d)",
                                 test_count, i, j, k, {cout_tb, sum_tb}, expected_full);
                        error_count = error_count + 1;
                    end
                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
iverilog -o sim_adder_sc tb_adder_4bit_selfcheck.v adder_4bit.v
vvp sim_adder_sc

For a correct adder the whole output is one line:

PASSED: all 512 tests.

With the carry inverted, say, it is a list of the failing inputs with what came out and what should have, and a count. You know what is wrong without opening a waveform.

A check task

The comparison is tangled with the loop. Move it into a task:

`timescale 1ns / 1ps

module tb_adder_4bit_task();

    reg  [3:0] a_tb, b_tb;
    reg        cin_tb;
    wire [3:0] sum_tb;
    wire       cout_tb;

    integer   error_count, test_count;
    integer   i, j, k;
    reg [4:0] expected_full;

    adder_4bit dut (.a(a_tb), .b(b_tb), .cin(cin_tb), .sum(sum_tb), .cout(cout_tb));

    task check_output;
        input [3:0] a_in, b_in;
        input       cin_in;
        input [4:0] expected;
        begin
            test_count = test_count + 1;
            if ({cout_tb, sum_tb} !== expected) begin
                $display("FAIL [test %0d]: %0d + %0d + cin=%0d", test_count, a_in, b_in, cin_in);
                $display("         got {cout,sum}=%05b expected %05b", {cout_tb, sum_tb}, expected);
                error_count = error_count + 1;
            end
        end
    endtask

    initial begin
        $dumpfile("tb_adder_4bit_task.vcd");
        $dumpvars(0, tb_adder_4bit_task);

        error_count = 0;
        test_count  = 0;

        for (k = 0; k < 2; k = k + 1) begin
            for (i = 0; i < 16; i = i + 1) begin
                for (j = 0; j < 16; j = j + 1) begin
                    a_tb   = i[3:0];
                    b_tb   = j[3:0];
                    cin_tb = k[0];
                    #10;
                    expected_full = i + j + k;
                    check_output(i[3:0], j[3:0], k[0], expected_full);
                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

The loop is three lines of stimulus and a call. A new check, a parity, a flag, goes into the task once.

The verdict

Every testbench ends the same way:

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

For a build system that only looks at exit codes, add

if (error_count != 0)
    $fatal(1, "FAILED with %0d errors.", error_count);

$fatal comes from SystemVerilog, Icarus accepts it, and it exits with a non-zero status that a script can see.

A testbench that prints PASSED can run on every commit, in a build pipeline, on a new engineer’s first day, and be trusted by people who have never seen the waveforms. That is what no amount of GTKWave can give.

The rest of the module builds on this skeleton: the simulation-only tasks in 6.2, clocks and resets in 6.3, better checkers in 6.4, and waveforms in 6.5 for the day a test fails and you need to see why. Section 6.2 starts with the three printing tasks and when each is right.

โ† Sampling the Outputs $display, $monitor and $strobe โ†’