24 / 37 · Lab
Lab: a testbench that reports its own failures
Separate DUT, stimulus, reference model and checks, then test reset, enable and wraparound at race-free observation times.
Lessons are free to read. Enroll to save your learning progress.
Separate implementation from the checker
A testbench supplies stimulus, calculates expected behavior and reports mismatches. It is simulation code, not the circuit to synthesize. This lab uses the modulo_tick module from the parameter lesson. Save that module as modulo_tick.sv and the following complete testbench as tb.sv.
The specification is: synchronous reset wins; disabled edges hold count and clear pulse; enabled edges increment modulo N, with pulse=1 only on wrap. Use an integer reference count rather than reading the DUT’s internal state.
`timescale 1ns/1ps
module tb #(parameter integer N = 5);
localparam integer CW = (N <= 1) ? 1 : $clog2(N);
logic clk = 1'b0;
logic rst = 1'b0;
logic enable = 1'b0;
wire [CW-1:0] count;
wire pulse;
integer expected_count = 0;
logic expected_pulse = 1'b0;
integer checks = 0;
modulo_tick #(.N(N)) dut (
.clk(clk), .rst(rst), .enable(enable),
.count(count), .pulse(pulse)
);
always #5 clk = ~clk;
task automatic step(input logic reset_value, enable_value);
begin
@(negedge clk);
rst = reset_value;
enable = enable_value;
if (reset_value) begin
expected_count = 0;
expected_pulse = 1'b0;
end else begin
expected_pulse = 1'b0;
if (enable_value) begin
expected_count = expected_count + 1;
if (expected_count == N) begin
expected_count = 0;
expected_pulse = 1'b1;
end
end
end
@(posedge clk);
#1;
if (count !== CW'(expected_count) || pulse !== expected_pulse)
$fatal(1, "Mismatch check=%0d count=%0d expected=%0d pulse=%b expected=%b",
checks, count, expected_count, pulse, expected_pulse);
checks = checks + 1;
end
endtask
initial begin
if (N < 1) $fatal(1, "N must be positive");
step(1'b1, 1'b1);
repeat (N-1) step(1'b0, 1'b1);
step(1'b0, 1'b0);
step(1'b0, 1'b1);
step(1'b0, 1'b0);
repeat (2*N+1) step(1'b0, 1'b1);
step(1'b1, 1'b1);
step(1'b0, 1'b0);
$display("PASS N=%0d checks=%0d", N, checks);
$finish;
end
initial begin
#10000;
$fatal(1, "Timeout");
end
endmoduleAvoid a race at the active edge
The task drives inputs at a falling edge, half a clock before capture. It compares after the next rising edge’s nonblocking updates. Here #1 is an educational observation delay with a declared timescale and a ten-nanosecond clock. It is not a physical clock-to-Q, setup or hold analysis. A reusable production verification environment may use clocking blocks or a more formal event-region discipline.
At the first step, reset makes the DUT known. The checker’s initialized reference does not initialize the DUT. !== reliably treats an unexpected X/Z output as a mismatch against known expected values.
Compile and run an explicit top
In a shell with Icarus Verilog installed, run:
iverilog -g2012 -s tb -o sim.vvp modulo_tick.sv tb.sv
vvp sim.vvpUse -P tb.N=1, -P tb.N=2, -P tb.N=5 and -P tb.N=8 in separate compile commands to cover the configuration matrix. Icarus’s official command-line documentation explains language-mode and top-selection flags. A simulator may support a different SystemVerilog subset, so keep its actual version in the run record.
Make the checker fail deliberately
Change the DUT’s terminal comparison so count wraps one edge early, or remove the default pulse clear. At least one scenario must terminate with a nonzero failure before the timeout. Restore the correct source afterward. A passing checker that cannot detect a relevant mutation offers weak evidence.
Record the source hash, N, exact commands, exit statuses, PASS line and mismatch from the deliberate failure. Do not label this local testbench as a DDS run. General SystemVerilog testbench delays and initial blocks are separate from the restricted single-clock product lab interface.