You ran the simulation and it printed PASS. What did that just prove?
This lesson runs your first simulation of mini_controller, the 4-bit event counter you will carry through the whole course, and reads every line of what comes back.
A simulation runs two programs at once. The design under test (DUT) is the hardware you are building, written as code. The testbench is the scaffolding around it: it creates the clock, drives the inputs, and checks the outputs. The simulator advances simulated time and runs both together, edge by edge.
Register-transfer level (RTL) is hardware described as registers and the logic between them: what each register holds after every clock edge. Your DUT is mini_controller, a 4-bit event counter. Its rule is a priority list: reset first, then clear, then count, then hold. Reset is synchronous and active low, so it clears the count at a rising edge where the reset input sits low, not the moment the input changes.
Timing keeps the two programs from tripping over each other. The testbench changes inputs on the falling edge, so every input is stable when the design samples it on the rising edge. The check runs 1 ns after the rising edge, once the new count has settled. That order, drive then sample then check, is the heartbeat of every simulation in this course.
The figure below shows the whole run: the clock, the reset and control inputs, and the count. Read the count row from left to right: each step is one rising edge, and the story is reset, three counts, a clear that wins, one more count, then a hold.
How to read it, slot by slot:
Slot 0: the reset input is low at the edge, so the count stays 0.
Slots 1 to 3: the event input is high and clear is low, so the count rises 1, 2, 3.
Slot 4: the event and clear inputs are both high, and clear has the higher priority, so the count returns to 0.
Slot 5: the event input is high alone, so the count rises to 1.
Slots 6 and 7: the event input is low, so the count holds at 1.
If the testbench syntax is new (initial, @(posedge clk), #1, $display, = versus <=), the Syntax Primer explains each form with a short program.
The file holds two modules. Read them in pieces first, then as one file. Piece 1 is the design under test, the counter itself. It is one clocked block that follows the priority list: reset, then clear, then count, and otherwise it holds.
`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
endmodulePiece 2 is the testbench scaffolding: a clock that flips every 5 ns, a request to record every signal, and a small routine that compares the count with the value we expect.
// snippet
always #5 clk = ~clk; // the clock: period 10 ns
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end // record signals
task automatic check(input logic [3:0] want, input int idx);
if (count !== want) begin // !== treats an unknown x as a mismatch
errors++;
$display("ERROR edge %0d: expected count=%0d got %0d", idx, want, count);
end
endtaskPiece 3 is one stimulus-and-check pair, repeated eight times with different inputs: wait for the falling edge, set the inputs, wait for the rising edge, wait 1 ns, and check.
// snippet
@(negedge clk); rst_n = 1; event_valid = 1; clear_count = 0; // drive the inputs
@(posedge clk); #1; check(4'd1, 1); // sample, then checkNow the whole file, the three pieces together. The waveform above comes from it.
`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
// Self-checking testbench for the first simulation: watch count grow, clear,
// then hold over eight rising edges. Inputs change on the falling edge; count
// is checked 1 ns after each rising edge. Not synthesizable.
module tb;
logic clk = 0;
logic rst_n = 0; // edge 0 stimulus: hold reset (matches the first check)
logic event_valid = 0;
logic clear_count = 0;
logic [3:0] count;
mini_controller dut (.clk, .rst_n, .event_valid, .clear_count, .count);
always #5 clk = ~clk;
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
int errors = 0;
// Check count 1 ns after the rising edge. Mismatches are reported but do
// not stop the run, so one failure cannot hide the rest of the sequence.
task automatic check(input logic [3:0] want, input int idx);
if (count !== want) begin
errors++;
$display("ERROR edge %0d: expected count=%0d got %0d", idx, want, count);
end
endtask
initial begin
@(posedge clk); #1; check(4'd0, 0); // edge 0: reset -> 0
@(negedge clk); rst_n = 1; event_valid = 1; clear_count = 0;
@(posedge clk); #1; check(4'd1, 1); // edge 1: valid -> 1
@(negedge clk); rst_n = 1; event_valid = 1; clear_count = 0;
@(posedge clk); #1; check(4'd2, 2); // edge 2: valid -> 2
@(negedge clk); rst_n = 1; event_valid = 1; clear_count = 0;
@(posedge clk); #1; check(4'd3, 3); // edge 3: valid -> 3
@(negedge clk); rst_n = 1; event_valid = 1; clear_count = 1;
@(posedge clk); #1; check(4'd0, 4); // edge 4: clear wins -> 0
@(negedge clk); rst_n = 1; event_valid = 1; clear_count = 0;
@(posedge clk); #1; check(4'd1, 5); // edge 5: valid -> 1
@(negedge clk); rst_n = 1; event_valid = 0; clear_count = 0;
@(posedge clk); #1; check(4'd1, 6); // edge 6: idle -> holds 1
@(negedge clk); rst_n = 1; event_valid = 0; clear_count = 0;
@(posedge clk); #1; check(4'd1, 7); // edge 7: idle -> holds 1
if (errors == 0) $display("PASS: first simulation");
else $fatal(1, "FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe design is piece 1; everything after module tb is scaffolding and eight pairs like piece 3.
# captured terminal output
$ iverilog -g2012 -Wall -s tb -o build/sim.vvp counter_wave.sv
$ vvp build/sim.vvp
VCD info: dumpfile wave.vcd opened for output.
PASS: first simulation
counter_wave.sv:74: $finish called at 76000 (1ps)This output came from Icarus Verilog 13.0 on macOS. Other versions word the last line slightly differently.
Each line earns its place:
Line 1 compiles. -s tb names the testbench as the top module; the file holds two modules, so without it the wrong one could end up on top. -g2012 selects the SystemVerilog language version, -Wall turns on all warnings, and -o names the compiled output.
Line 2 runs. vvp plays the compiled simulation: the clock ticks, the testbench drives its eight edges, and each check compares the count against its expected value.
Line 3 opens the recording. The testbench asks for a value change dump (VCD), the file of signal changes, with its dump calls; the simulator answers by opening wave.vcd, the file the waveform viewer draws.
Line 4 is the verdict. PASS means all eight checks matched, so reset, counting, clear priority, and holding all behaved. Any mismatch would report ERROR lines naming the edge instead.
Line 5 stamps the time. The run ends at 76,000 ps, which is 76 ns: the eighth rising edge lands at 75 ns on a 10 ns clock starting at 5 ns, and the final check runs 1 ns later.
0. Clear sits above counting in the priority list, so the simultaneous event is ignored and the count returns to 0. Whenever two inputs collide, the priority list decides, not the order in which you think about them.
Change one line in the design so each event adds two instead of one. Everything else stays the same, including the expected counts in the checks.
// snippet
count <= count + 4'd1;
// becomes:
count <= count + 4'd2;The run now fails on every edge that counts. Edges 6 and 7 fail too, though they do not count: the design holds its count there, but it holds the wrong value left over from edge 5. One bug can show up on later edges as a carried-over error:
# captured terminal output
VCD info: dumpfile wave.vcd opened for output.
ERROR edge 1: expected count=1 got 2
ERROR edge 2: expected count=2 got 4
ERROR edge 3: expected count=3 got 6
ERROR edge 5: expected count=1 got 2
ERROR edge 6: expected count=1 got 2
ERROR edge 7: expected count=1 got 2
FATAL: cw2.sv:73: FAIL: 6 mismatches
Time: 76000 Scope: tbSame tool and platform as above: Icarus Verilog 13.0 on macOS. The testbench reports each mismatch and keeps going, so one failure cannot hide the rest of the pattern. The pattern points at the bug: every counting edge is wrong, while the reset, clear, and hold edges still match, which is exactly what adding two does. The run ends with a failure status instead of PASS. The name cw2.sv in the last lines is your edited copy of the file.
Edit the stimulus, not the design. The file is counter_wave.sv in the lesson-files/ folder of start-here-examples.zip. Find the stimulus line just above the check for edge 6 and change event_valid = 0 to event_valid = 1. Leave every expected value alone.
Edges 6 and 7. Edge 6 now counts, so the count is 2 where the check expects 1. Edge 7 is idle and holds, but it holds the 2, so it fails too. The edge-6 error is the cause and the edge-7 error is carried over from it.
Run it and compare with your prediction:
# captured terminal output
$ iverilog -g2012 -Wall -s tb -o build/sim.vvp counter_wave.sv
$ vvp build/sim.vvp
VCD info: dumpfile wave.vcd opened for output.
ERROR edge 6: expected count=1 got 2
ERROR edge 7: expected count=1 got 2
FATAL: counter_wave.sv:73: FAIL: 2 mismatches
Time: 76000 Scope: tbRestore the original line and rerun to confirm PASS: first simulation returns before you move on.
Forgetting `-s tb`. The file holds two modules, and without the flag the design itself can end up on top, so the run tests nothing and still looks quiet. Name the testbench on every compile.
Reading a silent compile as a pass. A compile with no output means the code is only syntactically acceptable; that says nothing about behavior. The PASS line is the verdict, not the absence of warnings.
Checking too few cases. A testbench that never drives clear together with an event, or never idles, passes a design with broken priority or a counter that cannot hold. Tests That Can Fail shows three bugs this very test misses, and the rungs that catch them.
Expecting a waveform without asking for one. wave.vcd exists only because the testbench calls its dump routines. A testbench without dump calls simulates fine and records nothing, so keep those two lines in every testbench you write.
Rule: A pass proves only the cases the test checks.
-s tb flag do on the compile line?It names tb, the testbench, as the top module of the simulation. The file contains two modules, so without the flag the compiler elaborates the wrong one on top: the design would sit there with its inputs undriven, and the run would check nothing while looking quiet.
The design updates the count at the edge with a nonblocking assignment, and sampling in the same instant races that update. Waiting 1 ns lets the new value settle, so the check compares against the decided value instead of a coin toss between old and new.
A stop at the first mismatch shows one symptom; the full count shows the pattern. In the count-by-two run, four errors on the counting edges, followed by two on the idle edges that only hold the wrong carried-over value, point straight at the increment, while a single first error could mean anything. The counter still fails the run at the end, so nothing is forgiven.
Simulation runs the design and its testbench together in simulated time; only the design describes hardware.
Drive inputs on the falling edge, sample on the rising edge, and check after the new value settles.
A pass proves only the cases the test checks, so a test that cannot fail proves nothing.