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.

SimulatorCommand
Synopsys VCSvcs -sverilog counter.sv counter_tb.sv -o simv && ./simv
Cadence Xceliumxrun -sv counter.sv counter_tb.sv
Siemens Questavlog -sv counter.sv counter_tb.sv && vsim -c counter_tb -do "run -all; quit"
Verilatorverilator --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.