Digital Design / Module 3: Verilog that means hardware
18. Your first self-checking testbench
Prove the design works rather than eyeballing it.
See it move
The idea
A testbench is the code that exercises your design: it makes a clock, drives inputs and checks outputs. It isn't hardware, so it can use delays, files and printing. A self-checking testbench decides pass or fail by itself, so nobody needs to read a waveform to know the result.
module tb;
reg [3:0] a, b;
wire [3:0] s;
wire cout;
integer errors = 0;
add4 dut(.a(a), .b(b), .cin(1'b0), .s(s), .cout(cout));
task check(input [3:0] x, y);
begin
a = x; b = y;
#1; // let the combinational logic settle
if ({cout, s} !== x + y) begin
$display("FAIL %0d+%0d: got %0d", x, y, {cout, s});
errors = errors + 1;
end
end
endtask
integer i, j;
initial begin
$dumpfile("tb.vcd");
$dumpvars(0, tb);
for (i = 0; i < 16; i = i + 1)
for (j = 0; j < 16; j = j + 1)
check(i, j);
if (errors == 0) $display("PASS");
else $display("%0d errors", errors);
$finish;
end
endmoduleWhat makes it good
- It uses
!==so thatxin the output counts as a failure. - It is exhaustive here, which is possible with only 256 cases. Larger designs use directed plus random stimulus.
- It computes the expected value a different way, which is what makes the check meaningful.
Clocked designs
reg clk = 0;
always #5 clk = ~clk;Change inputs away from the sampling edge, such as on the falling edge, or just after the rising edge. Driving inputs exactly on the active edge invites a race with the design.
Common mistake
A testbench that prints values for you to check by eye. It can't run in a regression, and a tired engineer will miss a wrong number. Try the injected bug in the animation: the checks catch it with no reading at all.
Key ideas
- Generate a clock, drive stimulus, and check results.
- Drive inputs away from the sampling edge to avoid races.
- $dumpvars produces a waveform for debugging.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
!=? How does if treat that result?The fix
if (y !== 1'b1) caught = 1'b1;
Why it failed
x != 1 evaluates to x, and if treats x as false, so the error branch never runs. A broken design passes your test. !== compares all four states literally, and it always gives a clear 0 or 1, so x now counts as a mismatch.
Interview questions
coreWhy be self-checking?
It scales: a regression can pass or fail without someone reading waveforms.
basicWhy does a testbench module usually have no ports, and what is it allowed to do that design RTL is not?
Nothing sits outside the testbench, so there is nothing to connect a port to; it instantiates the design under test inside itself. Because it never becomes hardware, it can use delays, waits, file reads and writes, printing and loops that would not synthesize. Its job is to drive stimulus and deliver a pass or fail verdict, not to describe a circuit.
basicThe output y of your design is x because you forgot a reset. Which of these two checks reports the error, and why?
if (y != 1'b1) errors = errors + 1; // check A if (y !== 1'b1) errors = errors + 1; // check B
Only the second one. With the ordinary operator, x != 1 evaluates to x, and an if treats x as false, so the error line never runs and the broken design passes. The case-inequality operator !== compares all four values literally, so x against 1 is simply different and the check fires. In a testbench, the exact forms === and !== are the safe choice.
coreThis loop tests a correct 4-bit adder built from full-adder instances, yet in simulation it can report many failures. Why, and what is the fix?
for (i = 0; i < 16; i = i + 1)
for (j = 0; j < 16; j = j + 1) begin
a = i; b = j;
if ({cout, s} !== i + j) errors = errors + 1;
endThe inputs are set and the output is compared in the same simulation instant, with no time passing. The adder's continuous assignments are only scheduled, so the comparison can read outputs left over from the previous vector. On a structural 4-bit adder like this, all 256 checks failed even though the adder was correct. Adding a small wait such as #1 after setting the inputs lets the logic settle before the check.
coreWhere should a self-checking testbench get its expected values from, and why not reuse a copy of the design's own RTL?
The expected value must be worked out independently, for example with a plain + on integers for an adder, or with a separate behavioural reference model. If the expected value came from the same circuit or the same code, it would share the same bug and agree with the wrong answer. An independent model is what turns a comparison into evidence.
coreA 4-bit adder passes eight hand-picked sums, but it has a bug in which bit 2 of the sum ignores its carry-in. Why did the directed tests not settle the question, and what would you do instead?
Most hand-picked sums never produce a carry into bit 2, so they cannot expose a bug that only matters when that carry is 1. With 4-bit inputs there are only 256 input pairs, so two nested loops can try every one and leave no gap. For wider blocks where exhaustive testing is impossible, combine random stimulus with directed corner cases such as all ones plus one, which forces a carry through every bit.
coreAn adder has two 16-bit inputs and a carry-in. How many vectors does an exhaustive test need, and how long would it take at one million vectors per second?
There are 16 + 16 + 1 = 33 input bits, so 2^33 = 8,589,934,592 vectors. At 10^6 vectors per second that is about 8,590 seconds, roughly 2.4 hours. That is possible for one block, but each extra input bit doubles it, which is why wider designs move to random and directed testing.
advancedA testbench drives a flop's input with a blocking assignment inside always @(posedge clk). Moving that block to a different place in the file changes when the flop captures the new value. What is happening, and how do you fix it?
always @(posedge clk) if (n == 1) d = 1'b1; // stimulus always @(posedge clk) q <= d; // the flop
The stimulus block and the flop's block both wake on the same rising edge, and the language does not define which runs first. If the stimulus runs first, the flop sees the new value on this edge; if second, it sees it one edge later, so the result depends on file order or simulator. Drive inputs away from the sampling edge, for example on the falling edge or a short delay after the rising edge, or use a non-blocking assignment so the new value is not visible until after all edge-triggered blocks have read their inputs.
advancedThis clock generator is in a testbench, but every flop in the design stays x and nothing ever happens. Why?
reg clk; always #5 clk = ~clk;
A reg starts as x in simulation, and the inverse of x is still x, so the clock never leaves x and never produces a clean rising edge. The fix is to give it a starting value, either reg clk = 0; or clk = 0 in an initial block. It is worth checking the clock and reset first whenever a whole design looks dead in a waveform.