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.