Verilog compilation and simulation with Icarus Verilog
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.
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
teeinstead ofvvp. - 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?
Practice: Verilog compilation and simulation with Icarus Verilog
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.