Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Quick Start: Your First Green Build
  • Part 1: How Chips Get Made6
  • Part 2: Your Workstation5
  • Part 3: Specify and Architect3
  • Part 4: The Design Loop6
    • Compile, Elaborate, Simulate, Synthesize, Lint
    • First Simulation
    • Waveforms with GTKWave
    • Lint with Verilator
    • First Synthesis Check
    • Simulation Code vs Synthesizable Code
  • Part 5: The Verification Ladder6
  • Part 6: Automation and Reproducibility5
  • Part 7: Hand-Off to the Back-End3
  • Part 8: When Things Go Wrong3
  • Part 9: Project and Next Steps3
  • Reference4
Loading lesson

‹Part 4: The Design Loop
  1. Topics
  2. Start Here: RTL and Verification
  3. Part 4: The Design Loop
  4. First Simulation

Part 4 · The Design Loop · Working (W)

First Simulation

Run your first simulation of the mini_controller event counter: compile with Icarus, read the waveform, and learn what a PASS really proves.

On this page

    Note

    Level: Working (W) · Answers: You ran the simulation and it printed PASS. What did that just prove?

    Before this: Compile, elaborate, simulate, synthesize, lint

    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.

    What actually runs during a simulation

    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.

    Eight rising edges in one picture

    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.

    Compile, run, and read every line

    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.

    mini_controller.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    `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

    Piece 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.

    Diagram
    // 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
    endtask

    Piece 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.

    Diagram
    // snippet
    @(negedge clk); rst_n = 1; event_valid = 1; clear_count = 0;   // drive the inputs
    @(posedge clk); #1; check(4'd1, 1);                            // sample, then check

    Now the whole file, the three pieces together. The waveform above comes from it.

    mini_controller.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    `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
    endmodule

    The design is piece 1; everything after module tb is scaffolding and eight pairs like piece 3.

    Diagram
    # 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.

    Predict before you look

    Predict first After edge 4, the event and clear inputs are both high. What is the count?

    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.

    Break it on purpose: count by two

    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.

    Diagram
    // 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:

    Diagram
    # 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: tb

    Same 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.

    Your turn: change the stimulus, predict the failure

    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.

    Predict first With that one change, which edges report an error, and what does the count read on each?

    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:

    Diagram
    # 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: tb

    Restore the original line and rerun to confirm PASS: first simulation returns before you move on.

    Four ways this step fools beginners

    • 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.

    Check yourself What does the -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.

    Check yourself Why does the check wait 1 ns after the rising edge instead of sampling at the edge?

    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.

    Check yourself Why does the testbench count errors instead of stopping at the first mismatch?

    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.

    Interview

    Asked: "Your testbench printed PASS. What did it prove?"

    Weak answer: That the design works and the counter is correct.

    Strong answer: That the design matched the eight cases this testbench checks: reset to 0, three counted events, clear beating a simultaneous event, one more count, and two idle holds. It says nothing about the cases the testbench never drives, such as wrapping from 15 to 0, so PASS is a statement about the test, not a certificate for the design.

    Follow-ups: (1) What one case would you add first to trust it more?

    Key takeaways

    • 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.

    Keep going

    Next: Lesson 4.3, Waveforms with GTKWave. The numbers sit in a file; next you open them as a picture and learn to read time the way the tools draw it.

    Related topics

    • RTL course: Simulation and Testbenches

    Previous

    19 / 47

    Next: Waveforms with GTKWave

    Up next

    Up nextWaveforms with GTKWave

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Lint with Verilator
    • First Synthesis Check
    • Simulation Code vs Synthesizable Code
    clkrst_nevent_validclear_countcount012301
    Eight rising edges of the first simulation. Watch the count row: 0 after reset, 1, 2, 3 while the event input is high, back to 0 where clear beats the event, up to 1 once more, then holding at 1 while idle.