Module 1: Getting Oriented
Your First Simulation
The smallest thing that actually runs
Here is a complete, working simulation: a design, a testbench that drives it, and a check that fails loudly if the design misbehaves. Read it once through, then we will pull it apart.
// counter.sv - the design
module counter #(
parameter int WIDTH = 8
)(
input logic clk,
input logic rst_n, // active-low reset
input logic enable,
output logic [WIDTH-1:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= '0; // '0 means "zero, whatever the width is"
else if (enable)
count <= count + 1'b1;
end
endmodule
// counter_tb.sv - the testbench
module counter_tb;
localparam int WIDTH = 8;
logic clk = 0;
logic rst_n;
logic enable;
logic [WIDTH-1:0] count;
// Instantiate the design. .name(name) connects testbench signals to ports.
counter #(.WIDTH(WIDTH)) dut (
.clk (clk),
.rst_n (rst_n),
.enable (enable),
.count (count)
);
// A free-running clock: flip every 5 time units, so a 10-unit period.
always #5 clk = ~clk;
int errors = 0;
task automatic check(input logic [WIDTH-1:0] expected, input string what);
if (count !== expected) begin
$error("%s: expected %0d, got %0d", what, expected, count);
errors++;
end
endtask
initial begin
$display("[%0t] starting", $time);
// Reset
rst_n = 0;
enable = 0;
@(posedge clk);
@(posedge clk);
#1;
check(0, "count should be 0 while in reset");
// Release reset, then count for 5 cycles
rst_n = 1;
enable = 1;
repeat (5) @(posedge clk);
#1; // settle past the clock edge before reading
check(5, "count after 5 enabled cycles");
// Hold enable low - the count must not move
enable = 0;
repeat (3) @(posedge clk);
#1;
check(5, "count should hold while disabled");
if (errors == 0)
$display("[%0t] PASS", $time);
else
$display("[%0t] FAIL - %0d error(s)", $time, errors);
$finish;
end
endmodule
Running it
Every simulator wants the same three things: the files, a top module to start from, and a command to go. Only the spelling differs.
| Simulator | Command |
|---|---|
| Synopsys VCS | vcs -sverilog counter.sv counter_tb.sv -o simv && ./simv |
| Cadence Xcelium | xrun -sv counter.sv counter_tb.sv |
| Siemens Questa | vlog -sv counter.sv counter_tb.sv && vsim -c counter_tb -do "run -all; quit" |
| Verilator | verilator --binary --timing counter.sv counter_tb.sv --top-module counter_tb && ./obj_dir/Vcounter_tb |
What you should see:
[0] starting
[96] PASS
Now go and break it on purpose. Change count <= count + 1'b1
to count <= count + 2 and run it again. You should get a real
failure with a line number, which is the entire point of a self-checking
testbench: the simulation tells you it is wrong, rather than leaving you to
notice in a waveform.
The parts that are doing the work
always_ff @(posedge clk or negedge rst_n) —
this is a flip-flop. It describes a piece of hardware that updates on the
rising clock edge, with an asynchronous active-low reset. The
_ff suffix is you telling the tool your intent, and the tool will
complain if what you wrote is not actually sequential logic.
<= versus = — the arrow is a
non-blocking assignment. Inside clocked logic, always use it. All the
non-blocking assignments in a time step compute their right-hand sides first
and update afterwards, which is exactly how a row of flip-flops behaves on a
clock edge. Using = in clocked logic is the classic beginner bug:
it works in simple cases and then produces shift registers that lose a stage,
or races that behave differently in simulation and in silicon.
always #5 clk = ~clk; — the clock generator.
It is a testbench construct, not hardware; no synthesis tool will build you a
clock from that line. Note the initialiser logic clk = 0;: without
it, clk starts as X, and ~X is X, so your clock never
starts and the simulation sits there doing nothing forever. That specific
mistake catches almost everyone once.
!== rather than != — the
three-character version is case inequality. It compares X and Z
literally, so an X in count makes the check fail. Plain
!= returns X when either side has an X, and if (X) is
false, so the check would silently pass. In a checker you almost always want
the three-character form.
The #1 before reading count —
at @(posedge clk) the testbench wakes at the same instant the
flip-flop updates, and reading count right there is a race. The
#1 steps clear of the edge. This is a blunt fix and it is fine at
this stage; the proper answer is clocking blocks, which the advanced course
covers.
$display, $error, $finish,
$time — system tasks, all beginning with a dollar sign.
$error is worth preferring over $display for failures
because simulators count it, so your regression script can tell pass from fail
without grepping output.