Simulation and testbenches
Why you simulate before you synthesise, how a Verilog or VHDL testbench drives a design, and how to read the waveform it produces.
You will learn
- Explain why simulation finds bugs faster and more cheaply than testing on a board
- Write a Verilog testbench with clock generation, stimulus, $display and $finish
- Write the equivalent VHDL testbench using a stimulus process and assertions
- Run Icarus Verilog and open the resulting VCD file in GTKWave or Surfer
- Read a waveform and relate what you see to the clock edges in the code
In the last lesson you wrote a module and pushed it through synthesis. This lesson is about the step that should come before synthesis every time: simulation. You will write a testbench for a small counter in both Verilog and VHDL, run it with free tools, and learn to read the waveform that comes out. If you only take one habit away from the beginner track, make it this one.
Why simulate at all
A synthesis run for a small design takes a minute or two. Place and route takes longer. Programming the board and poking at it with a finger takes longer still, and when the LED does the wrong thing you have almost no information about why. You can see the outputs and nothing else.
A simulator runs your HDL as a program on your PC. It starts in well under a second for a design this size, and it lets you look at every signal in the design at every point in time. When a bug appears you can trace it back from the wrong output to the exact clock edge where a register took the wrong value. That is a completely different kind of debugging from staring at an LED.
There is a second reason. A testbench is a written record of what you expect the design to do. Six months from now, when you change the module, the same testbench tells you whether you broke it. Boards do not do that.
The one thing simulation cannot tell you is whether the design will meet timing on a real chip. That is the job of the timing report, which lesson 10 covers. Simulation checks function, not speed.
The structure of a testbench
A testbench is itself an HDL module, but one with no ports. It sits above the design under test (DUT), wires itself to the DUT’s ports, and drives them. Because it will never be synthesised, it may use constructs that have no hardware meaning: delays, loops that wait for time to pass, file output and console printing.
Four parts appear in nearly every testbench. A clock generator produces a free-running clock. A stimulus block drives the inputs in a chosen order, waiting for clock edges between steps. A monitor watches outputs and prints or checks them. And somewhere a waveform dump is switched on, so that you can inspect the run afterwards.
The design under test
The DUT is a 4-bit counter with a synchronous reset and an enable, the kind of thing lesson 04 introduced.
// A 4-bit counter with synchronous reset and enable.
module counter4 (
input wire clk,
input wire rst,
input wire en,
output reg [3:0] count
);
always @(posedge clk) begin
if (rst)
count <= 4'd0;
else if (en)
count <= count + 4'd1;
end
endmodule
A Verilog testbench
Here is a complete testbench for it. Read it top to bottom, then the notes below.
`timescale 1ns / 1ps
// Testbench for counter4. Not synthesisable.
module tb_counter4;
reg clk = 1'b0;
reg rst = 1'b1;
reg en = 1'b0;
wire [3:0] count;
// Device under test.
counter4 dut (.clk(clk), .rst(rst), .en(en), .count(count));
// 100 MHz clock: toggle every 5 ns.
always #5 clk = ~clk;
// Stimulus.
initial begin
$timeformat(-9, 0, " ns", 6);
$dumpfile("counter4.vcd");
$dumpvars(0, tb_counter4);
// Hold reset for two clock edges, then release.
repeat (2) @(posedge clk);
rst <= 1'b0;
// Count for five cycles.
en <= 1'b1;
repeat (5) @(posedge clk);
// Pause, then count three more.
en <= 1'b0;
repeat (2) @(posedge clk);
en <= 1'b1;
repeat (3) @(posedge clk);
#1; // let the last edge settle before checking
if (count !== 4'd8)
$display("FAIL: expected 8, got %0d", count);
else
$display("PASS: count = %0d", count);
$finish;
end
// Print every change on the output.
always @(count)
$display("t=%0t count=%0d", $time, count);
endmodule
The timescale directive sets the unit for # delays (1 ns) and the resolution the simulator works to (1 ps). Without it, delays are in whatever unit the tool defaults to, which differs between tools.
The inputs are declared as reg because the testbench assigns them from procedural blocks. The output is a wire because the DUT drives it.
The clock is one line: always #5 clk = ~clk;. The block has no sensitivity list, so it runs forever, inverting clk every 5 ns for a 10 ns period. It only works because clk was given an initial value of 0. An uninitialised reg is x, and ~x is still x, so the clock would never start.
The stimulus lives in an initial block, which runs once from time zero. The key idiom is @(posedge clk), which blocks until the next rising edge. Writing stimulus in terms of clock edges rather than nanosecond delays is a habit worth forming early. If you later change the clock period, the test still works.
Inputs are assigned with the non-blocking operator <= right after an edge. That mirrors what a real upstream register would do: the value changes a moment after the edge, so the DUT samples it on the following edge, not the edge that has already passed. This avoids a race between the testbench and the DUT that catches out nearly everyone once.
$display prints to the console. %0d is a decimal with no padding, %0t is the current time formatted according to $timeformat. The final check uses !== rather than != so that an x on the output counts as a failure rather than being silently treated as unknown-but-equal.
$finish ends the simulation. Without it the clock generator runs forever and the simulator never returns.
Running it with Icarus Verilog
Icarus Verilog is a free, open-source simulator that covers Verilog-2005 and a useful subset of SystemVerilog. Compile the DUT and the testbench together, then run the result:
iverilog -g2012 -o counter4.vvp counter4.v tb_counter4.v
vvp counter4.vvp
The -g2012 flag enables SystemVerilog-2012 syntax, which does no harm for plain Verilog and lets you use logic and always_ff later. You should see something like:
t=5 ns count=0
t=25 ns count=1
t=35 ns count=2
t=45 ns count=3
t=55 ns count=4
t=65 ns count=5
t=95 ns count=6
t=105 ns count=7
t=115 ns count=8
PASS: count = 8
Read the times against the code. The first edge is at 5 ns and reset is high, so the count becomes 0. Reset is released after the edge at 15 ns and en is raised at the same moment, so the first increment lands on the edge at 25 ns. The gap between 65 ns and 95 ns is the two cycles where en was low. If any of these times surprised you, that is the simulator earning its keep.
Viewing the waveform
The run also wrote counter4.vcd, a Value Change Dump. Two free viewers read it.
GTKWave is the long-standing option. Run gtkwave counter4.vcd, expand tb_counter4 in the tree on the left, select the signals and press Append. Surfer is a newer viewer with a cleaner interface that also runs in a browser. Both show the same thing: time along the x-axis, one row per signal, with buses drawn as a band labelled with the value.
When you look at a waveform, look for three things. First, that every registered output changes only on a clock edge, never in between. Second, that the change comes one edge after the input that caused it, because the input is sampled on the edge and the output updates after it. Third, that nothing is red. Viewers draw x (unknown) in red and z (undriven) in a different colour, and a red stripe at the start of a signal that never clears usually means a register with no reset and no initial value.
The same testbench in VHDL
VHDL has no initial block or # delay operator, but the ideas map across directly. First the DUT:
-- A 4-bit counter with synchronous reset and enable.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity counter4 is
port (
clk : in std_logic;
rst : in std_logic;
en : in std_logic;
count : out unsigned(3 downto 0)
);
end entity;
architecture rtl of counter4 is
signal count_r : unsigned(3 downto 0) := (others => '0');
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
count_r <= (others => '0');
elsif en = '1' then
count_r <= count_r + 1;
end if;
end if;
end process;
count <= count_r;
end architecture;
And the testbench:
-- Testbench for counter4. Not synthesisable.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_counter4 is
end entity;
architecture sim of tb_counter4 is
constant clk_period : time := 10 ns; -- 100 MHz
signal clk : std_logic := '0';
signal rst : std_logic := '1';
signal en : std_logic := '0';
signal count : unsigned(3 downto 0);
signal done : boolean := false;
begin
-- Device under test.
dut : entity work.counter4
port map (clk => clk, rst => rst, en => en, count => count);
-- Clock runs until the stimulus process sets done.
clk <= not clk after clk_period / 2 when not done else '0';
stim : process
begin
wait for 2 * clk_period;
rst <= '0';
en <= '1';
wait for 5 * clk_period;
en <= '0';
wait for 2 * clk_period;
en <= '1';
wait for 3 * clk_period;
assert count = 8
report "FAIL: expected 8, got " & integer'image(to_integer(count))
severity error;
report "count = " & integer'image(to_integer(count));
done <= true;
wait;
end process;
monitor : process (count)
begin
report "count=" & integer'image(to_integer(count));
end process;
end architecture;
The clock is a concurrent conditional assignment. It re-evaluates every time clk changes and schedules the inverse half a period later, until done goes true. That last part matters: VHDL has no $finish, and a simulator keeps running while any signal is still scheduled to change. Stopping the clock is how the simulation ends.
The stimulus is a process with no sensitivity list, which runs from time zero. wait for plays the role of #, and the bare wait; at the end suspends the process forever. assert with severity error prints a message and carries on, which is usually what you want in a test. severity failure would stop the run.
The free simulators for VHDL are GHDL and nvc. With nvc the whole flow is one command:
nvc --std=2008 -a counter4.vhd tb_counter4.vhd -e tb_counter4 -r --wave=counter4.fst
The -a step analyses the files, -e elaborates the top level and -r runs it. The --wave option writes an FST file, which GTKWave and Surfer both open.
What a testbench cannot do for you
A testbench only checks what you thought to check. The counter test above never tries rst and en at the same time, never checks the wrap from 15 back to 0, and never asserts that count holds while en is low. Those are the sorts of gaps a real test should close. The verification topic goes on to self-checking testbenches, randomised stimulus and formal tools. For now, the rule is to write at least one check that would fail if the design were wrong, and to confirm that it actually does fail when you break the design on purpose.
Exercise
Extend tb_counter4.v so that it counts all the way to 15, carries on for two more cycles, and checks that count wrapped to 1. Then assert rst while en is still high and check that reset wins. Run it with Icarus Verilog, open the VCD in GTKWave or Surfer, and find the wrap on the waveform. Finally, change the DUT so that the reset is asynchronous (always @(posedge clk or posedge rst)) and see which parts of the waveform move.
If you are working in VHDL, do the same with tb_counter4.vhd and nvc or GHDL.
Summary
- Simulation runs your HDL on a PC and shows every internal signal. It is faster and far more informative than testing on a board, and it checks function, not timing.
- A testbench has no ports. It generates a clock, drives stimulus in step with clock edges, monitors outputs and dumps a waveform.
- In Verilog,
always #5 clk = ~clk;makes a clock,@(posedge clk)waits for an edge,$displayprints and$finishstops. - In VHDL, a conditional clock assignment with a
doneflag, a stimulus process usingwait for, andassertdo the same jobs. - Icarus Verilog and nvc are free and sufficient for the whole beginner track. GTKWave and Surfer read the waveform files they produce.
Further reading
- Icarus Verilog user guide
- nvc, a VHDL simulator
- GTKWave and Surfer waveform viewers
- IEEE 1800-2017, SystemVerilog language reference
- WaveDrom tutorial, for drawing the timing diagrams you expect before you simulate