How does a testbench know the right answer without you working it out by hand for every cycle?
The directed test from lesson 4.2 checks eight edges with expected values written by hand, and that works while the run is short. The strong testbench in this lesson runs 239 checking cycles. Nobody hand-computes 239 answers without mistakes, and a hand-written answer you got wrong teaches the testbench your error. This lesson replaces hand-written answers with a small model of the spec that computes every expected value itself.
The model is one function, spec_next, with four branches that match the four rows of the spec table: reset wins first, then clear, then a counting event that wraps from 15 to 0 by the width of the register, then holding the old value when nothing is asked. Each rising edge advances the model with the same inputs the design receives, so the two should always agree.
Independence is the whole point. The function is written from the spec table in docs/spec.md, never by reading the RTL, which is register-transfer level: hardware described as registers and the logic between them. A model copied from the design code repeats the design's mistakes, and then every check passes on broken code and means nothing. The testbench source says this directly: the function is not a copy of the design's branch structure by accident, and when the spec changes, the model changes first and the RTL follows.
The Python golden model from lesson 3.3, Golden Model and Register Contract, is the same idea in another language. Both models are written from the spec table, and both exist so that two independent readings of one table have to agree before anyone trusts the design.
Each cycle of the testbench has three beats. On the falling edge it drives the new inputs, so every input is stable when the design samples it on the rising edge. Driving and sampling on opposite edges keeps the two programs from tripping over each other.
Before the edge, the testbench checks that the count did not move since the previous check, 1 ns after driving and still before the rising edge. The spec says the count changes only after a rising edge and that reset is synchronous, so any movement between edges is a bug by definition. This is the check that catches an asynchronous reset: it fires the moment reset acts without waiting for the clock.
After the edge, the testbench advances the model with the same inputs and compares 1 ns later, once the new count has settled. A mismatch reports the full input combination, the expected and actual values, and the simulation time. Mismatches are counted rather than stopping at the first one, and the run ends with a fatal error unless the count of errors is zero.
The directed phase is not a tour of the inputs. Each step is aimed at one bug class from lesson 5.1, and the table below maps every step to its target. Read it row by row and match each step to the mutant it kills.
Directed step | Inputs driven | Bug class it exposes |
|---|---|---|
Reset from unknown | reset asserted, count unknown |
|
Hold at 5 after counting | idle with a nonzero count |
|
Reset while the count is 5 | reset asserted, count nonzero | Reset must work from any nonzero value, not only from unknown |
Reset with an event present | reset and valid together |
|
Reset with clear and an event | all three together | The three-way collision must still resolve to reset |
Clear with an event present | clear and valid together |
|
Seventeen counting edges | valid alone, from 0 |
|
The stability check, every edge | any inputs |
|
After the directed steps comes a 200-cycle random phase with a fixed seed, so the whole tail replays identically on every run. It covers the combinations the directed steps never named, and the next lesson explains where that randomness comes from.
The figure below shows the structure to hold in your head. The stimulus driver feeds the same inputs to two boxes side by side: the design and the model. The comparator takes both answers and checks them twice per cycle. Look at the two arrows entering the comparator: one carries the design's count, the other the model's answer, and the two labels inside say which check runs on each.
The finished testbench is about seventy lines, which is a lot to take in at once. You would not write it in one sitting, and you should not read it that way. Build it in four stages, each one runnable on its own, and watch what each stage adds. The listing after this section is all four stages together.
Stage 1: the model. Write the spec table as a function, from the spec and not from the design. It takes the current value and the three inputs and returns what the count must become. Nothing else exists yet.
// snippet: stage 1, the model alone
function automatic logic [3:0] spec_next(input logic [3:0] cur,
input logic r, c, v);
if (!r) return 4'd0; // reset wins
else if (c) return 4'd0; // clear next
else if (v) return cur + 4'd1; // count (wraps by width)
else return cur; // hold
endfunctionStage 2: one cycle. Add a task that drives the inputs on the falling edge, waits for the rising edge and 1 ns, updates the model, and compares. With this single routine you can already run one cycle and see a verdict.
// snippet: stage 2, one cycle, after-the-edge check only
task automatic cycle(input logic r, c, v);
@(negedge clk);
{rst_n, clear_count, event_valid} = {r, c, v};
@(posedge clk);
model = spec_next(model, r, c, v);
#1;
if (count !== model) begin
errors++; $display("ERROR: expected %0d got %0d", model, count);
end
endtaskStage 3: directed cases. Call the task with hand-picked inputs, one bug class at a time: reset from unknown, a count to 5, a hold at a nonzero value, reset while the count is nonzero, reset beating valid, clear beating valid, and enough counts to wrap past 15.
// snippet: stage 3, directed cases
cycle(0,0,0); // reset from unknown
repeat (5) cycle(1,0,1); // count to 5
cycle(1,0,0); // hold a NONZERO value
cycle(0,0,1); // reset beats validStage 4: the extra checks and the random phase. Last, check that the count did not move before the clock edge (which catches a reset that acts without the clock), then let a seeded random generator drive 200 more cycles. The model and the task stay exactly as they were.
// snippet: stage 4, the random phase added at the end
repeat (200) cycle($random(seed) & 1,
$random(seed) & 1,
$random(seed) & 1);Each stage answers one question: what is right (the model), how do I compare one cycle (the task), which cases matter (directed), what did I not think of (random).
`timescale 1ns/1ps
// Counts accepted events, 4 bits, wraps 15 -> 0.
// Priority: reset > clear > count > hold. Reset is synchronous, active low.
module mini_controller (
input logic clk,
input logic rst_n,
input logic event_valid,
input logic clear_count,
output logic [3:0] count
);
always_ff @(posedge clk) begin
if (!rst_n)
count <= 4'd0;
else if (clear_count)
count <= 4'd0;
else if (event_valid)
count <= count + 4'd1;
end
endmodule
`timescale 1ns/1psThis listing is the complete strong test: the design plus the testbench that checks it, including the spec_next model, the three-beat cycle task, the directed steps and the seeded random tail.
# captured terminal output
$ make all 2>&1 | grep -E '^(PASS|iverilog|vvp|verilator|yosys)' | cut -c1-110
verilator --lint-only -Wall --top-module mini_controller rtl/mini_controller.sv
iverilog -g2012 -Wall -s tb_mini_controller -o build/sim.vvp rtl/mini_controller.sv tb/tb_mini_controller.sv
PASS: mini_controller
yosys -Q -T -p 'read_verilog -sv rtl/mini_controller.sv; hierarchy -check -top mini_controller; proc; opt; cheThis output was measured on macOS with Icarus Verilog 13.0, Verilator 5.048 and Yosys 0.69+post. Lint runs first, then the simulator compiles both modules and checks all 239 cycles, then synthesis reads the design. The PASS line means every directed and random check matched; any mismatch would report an ERROR line naming the inputs and the time instead.
The asynchronous-reset mutant survives. Without the between-edges watch, a reset that acts the moment its input falls looks identical at every post-edge check: each reset edge still expects 0 and still sees 0. Every other mutant still fails on values, because only the timing bug hides completely inside correct-looking samples.
Cause a failure yourself with the mutant this testbench was built to catch. The steps below use the reset-last variant, where counting beats reset.
Copy mutants/reset_last.sv over rtl/mini_controller.sv, keeping a copy of the original elsewhere.
Run make sim and watch it fail: an ERROR line names the input combination with reset and an event together, the expected 0, and the simulation time, and make exits with a nonzero status.
Restore the original design file and run make sim again to confirm the run is green before you continue.
Building the model from the RTL. A model copied from the design code shares the design's bugs and passes on broken code. Write the model from the spec table, keep it in a function of its own, and change the spec first when behavior must change.
Checking only after the edge. Post-edge comparisons see only settled values, so a reset that acts between edges is invisible to them. Check stability before the edge too, on every cycle, because timing bugs live in the gaps that sampling never sees.
Stopping at the first mismatch. Here counting errors instead of stopping is a deliberate choice: the pattern across 239 cycles points at the bug class, while a single first error could mean anything. Six wrong counting edges mean the increment; one wrong hold edge means the hold. The run still fails at the end, so nothing is forgiven.
Rule: the testbench earns trust when a model of the spec produces every expected value.
Count up to 15 and then drive reset asserted with valid high, a cycle(0, 0, 1) step at the top count, and expect 0. Reset must beat the event that would wrap the count, so a design whose wrap logic overrode reset at the top count would fail the new step. None of the lesson's mutants does that, which shows the matrix is a starting set: a new directed step can guard a bug nobody has written yet.
A function covers every input combination on every cycle, all 239 of them including the random tail, from four lines that mirror the spec table. A hand-written table would need one correct entry per cycle, which is exactly the tedious and error-prone work the model removes.
The asynchronous-reset mutant. Every post-edge value still matches, because each reset edge expects 0 and sees 0, so only the movement between edges exposes it. Without the stability check that mutant survives the whole run.
A reference model turns the spec table into expected values for every cycle, so nobody hand-writes 239 answers.
Check twice per cycle: stability before the edge catches timing bugs, comparison after the edge catches value bugs.
Aim each directed step at one bug class, then let a seeded random phase cover the combinations you did not name.