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).
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.