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.