Verilog compilation and simulation with Icarus Verilog

Learning objectives

Compile a Verilog design and testbench, stop after compilation failure, run the simulation, capture logs, check the simulator exit status, generate a VCD waveform, and reproduce the run with a Bash script.

1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.

Explanation

Understand the flow

A command-line simulation has distinct stages:

RTL + testbench + file list
            |
            v
        iverilog
            |
            v
    simulation executable
            |
            v
           vvp
            |
            +----> simulation log
            +----> VCD waveform

iverilog compiles and elaborates the selected top module. vvp runs the resulting simulation executable. GTKWave can open a generated VCD file when a graphical desktop is available.

Check the tools before starting:

command -v iverilog
command -v vvp
command -v gtkwave
iverilog -V
vvp -V

If iverilog or vvp is missing, record the lookup result and use the lab environment supplied by the trainer. Do not download an arbitrary binary.

Files used by the example

Run the example from the project root created in the previous lesson:

counter-project/
|-- rtl/
|   `-- counter.v
|-- tb/
|   `-- tb_counter.v
|-- filelists/
|   `-- sim.f
|-- scripts/
|   `-- run_sim.sh
|-- sim/
|-- logs/
`-- waves/

Design under test

Save this as rtl/counter.v:

module counter (
    input  wire       clk,
    input  wire       reset_n,
    output reg  [3:0] count
);
    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            count <= 4'd0;
        end else begin
            count <= count + 4'd1;
        end
    end
endmodule

Self-checking testbench

Save this as tb/tb_counter.v:

`timescale 1ns/1ps

module tb_counter;
    reg        clk;
    reg        reset_n;
    wire [3:0] count;

    counter dut (
        .clk     (clk),
        .reset_n (reset_n),
        .count   (count)
    );

    always #5 clk = ~clk;

    initial begin
        $dumpfile("waves/counter.vcd");
        $dumpvars(0, tb_counter);

        clk     = 1'b0;
        reset_n = 1'b0;

        #12;
        reset_n = 1'b1;

        repeat (5) @(posedge clk);
        #1;

        if (count !== 4'd5) begin
            $display("TEST FAIL expected=5 actual=%0d", count);
            $finish;
        end

        $display("TEST PASS count=%0d", count);
        $finish;
    end
endmodule

The testbench drives reset, waits for five active clock edges, and checks the result. It prints an explicit PASS or FAIL marker. A normal Verilog $finish can still leave the simulator with status zero after a functional failure, so the run script must check both the process status and the result marker.

Simulation file list

Save this as filelists/sim.f:

rtl/counter.v
tb/tb_counter.v

The design appears before the testbench for a clear, deterministic order. More complex flows should place packages and shared definitions before files that use them.

Commands you will use

Command Purpose
iverilog -g2005 -Wall -s tb_counter Compile Verilog-2005, enable common warnings, and select the testbench top.
-o sim/counter_sim Write the compiled simulation executable to sim/.
-f filelists/sim.f Read source paths from the ordered file list.
vvp sim/counter_sim Run the compiled simulation.
tee logs/simulation.log Show simulation output while saving it.
${PIPESTATUS[0]} Read the status returned by vvp, rather than the status from tee.
gtkwave waves/counter.vcd Open the waveform when a graphical session is available.

Worked example

Compile and inspect the result

cd "$HOME/asic-linux-practice/counter-project"
mkdir -p sim logs waves

iverilog -g2005 -Wall \
    -s tb_counter \
    -o sim/counter_sim \
    -f filelists/sim.f \
    > logs/compile.log 2>&1

compile_status=$?
printf 'compile-status=%s\n' "$compile_status"
cat logs/compile.log

Expected result: compile-status=0 and sim/counter_sim exists. The compile log may be empty when there are no warnings or errors.

Do not run vvp when compilation failed. Read logs/compile.log, correct the source or file list, and compile again.

Run the simulation and preserve its status

vvp sim/counter_sim | tee logs/simulation.log
simulation_status=${PIPESTATUS[0]}
printf 'simulation-status=%s\n' "$simulation_status"
test -f waves/counter.vcd
printf 'waveform-status=%s\n' "$?"

Expected output includes:

VCD info: dumpfile waves/counter.vcd opened for output.
TEST PASS count=5
simulation-status=0
waveform-status=0

PIPESTATUS must be read immediately after the pipeline. Running another command first replaces it.

Automate the complete run

Save this as scripts/run_sim.sh:

#!/usr/bin/env bash
set -u
set -o pipefail

script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || exit 2
project_root=$(cd -- "$script_dir/.." && pwd) || exit 2
cd "$project_root" || exit 2

mkdir -p sim logs waves
rm -f -- sim/counter_sim waves/counter.vcd

printf 'Compiling counter example...\n'
if ! iverilog -g2005 -Wall \
    -s tb_counter \
    -o sim/counter_sim \
    -f filelists/sim.f \
    > logs/compile.log 2>&1; then
    printf 'COMPILE FAIL: see logs/compile.log\n' >&2
    exit 10
fi

printf 'COMPILE PASS\n'
vvp sim/counter_sim | tee logs/simulation.log
simulation_status=${PIPESTATUS[0]}

if (( simulation_status != 0 )); then
    printf 'SIMULATION FAIL: status=%s\n' "$simulation_status" >&2
    exit 20
fi

if ! grep -q '^TEST PASS ' logs/simulation.log || grep -q '^TEST FAIL ' logs/simulation.log; then
    printf 'SIMULATION FAIL: self-check did not pass\n' >&2
    exit 22
fi

if [[ ! -s waves/counter.vcd ]]; then
    printf 'SIMULATION FAIL: waveform was not created\n' >&2
    exit 21
fi

printf 'SIMULATION PASS\n'

Run it from any directory:

chmod u+x scripts/run_sim.sh
./scripts/run_sim.sh
printf 'run-status=%s\n' "$?"

Expected final output includes COMPILE PASS, TEST PASS count=5, SIMULATION PASS, and run-status=0.

Inspect the waveform

Check the file before opening a GUI:

ls -lh waves/counter.vcd
head -n 12 waves/counter.vcd

On a supported graphical workstation:

gtkwave waves/counter.vcd

Add clk, reset_n, and count to the waveform view. Confirm that reset clears count and each later rising clock edge increments it. On a headless server, keep the VCD and logs for later review instead of forcing GUI forwarding.

Diagnose common failures

Symptom First check
iverilog: command not found command -v iverilog and the approved tool setup.
Source file not found pwd, cat filelists/sim.f, and ls -l for each listed path.
Unknown module File-list contents, compile order, spelling, and -s top name.
Syntax error First compiler diagnostic and the reported source line.
Compile status is nonzero Stop; do not run an old simulation executable.
PASS text appears but status is nonzero Treat the run as failed and inspect the complete log.
VCD is missing $dumpfile, $dumpvars, working directory, and simulation completion.
GTKWave cannot open a display Use a graphical workstation or review the saved artifact later.

Common mistakes

  • Running commands outside the documented project root.
  • Continuing with an old executable after compilation failed.
  • Checking the status of tee instead of vvp.
  • Checking only the process status and ignoring the self-check result.
  • Looking only for PASS text and ignoring the process exit status.
  • Mixing generated executables, logs, and waveforms with source files.
  • Omitting the selected top module when multiple top-level modules exist.
  • Committing generated simulation output to Git.
  • Assuming a waveform proves that the testbench checked the correct behavior.

Check yourself

Why must compilation failure stop the flow? After vvp ... | tee ..., why is ${PIPESTATUS[0]} used instead of $?? What evidence shows that the simulation both ran successfully and checked the expected behavior?

Engineering practice

Practice: Verilog compilation and simulation with Icarus Verilog

Task→Run / reason→Check result→Explain why

Run the complete counter example and retain the compile log, simulation log, exit statuses, and VCD file. Then deliberately change the expected count from 4'd5 to 4'd6. Confirm that the testbench prints FAIL and the script returns a nonzero status. Restore the correct expectation, rerun, and record the final PASS evidence.

Practice work is non-certified and is not stored by ASIC Campus.