ModelSim Simulation Tutorial
Introduction
To verify that designs are working, it is useful to simulate them using ModelSim. This document will provide a quick demo of how to perform a simulation in ModelSim and how to include iCE40UP library files in the simulation.
Simple Simulations by Forcing Signals
If you have a simple design and want to quickly test it out to see that it is working, you can use the force function in the ModelSim command window to apply test inputs and measure the outputs.
We will use a full adder circuit as a demonstration.
To code up the full adder, first, open ModelSim and create a new ModelSim project titled ModelSim_Tutorial
and save it in a location where the path name has no spaces. Then create a new file named fulladder.sv
, making sure the file type is SystemVerilog. You should now have a blank file. Add the Verilog code below to create a structural Verilog model of the full adder circuit show above.
// fulladder.sv
// Structural Verilog full adder
// 5/26/22
// Josh Brake
// jbrake@hmc.edu
module fulladder(
input logic A, B, Cin,
output logic S, Cout
);
logic n1, n2, n3;
xor g1(n1, A, B);
xor g2(S, n1, Cin);
and g3(n2, n1, Cin);
and g4(n3, A, B);
or g5(Cout, n2, n3);
endmodule
Next, we will simulate the file. First, compile your file via the Compile menu. Then, start the simulation using “Simulate > Start Simulation…”. You will see the GUI change and open up the Objects and Wave panes. You can always toggle windows on or off as desired using the View toolbar menu.
Next, add the selected symbols by selecting them in the Objects pane and adding them to the Wave pane either by using the right-click menu or dragging them. Next, to run the simulation, we need to initialize the input values which are floating (HiZ) to begin. We can do this by typing the command force <Signal Name> <Signal Value>
in the Transcript pane. Force A
, B
, and Cin
to some desired values of your choosing and then run the simulation for 10 time units by typing run 10
. When you run the simulation, you will see the waves window advance the appropriate number of time points, displaying the values of the signals.
Fill out a truth table for a full adder by evaluating the circuit schematic shown above for all combinations of A, B, and Cin and verify a few values by forcing them to get the hang of it.
Testbench Simulation
After testing about only two different inputs by manually forcing values you are probably already frustrated with the tediousness of running a simulation this way. Luckily, there is a much more efficient way to run simulations using testbenches.
A testbench is a separate module which generates simulated inputs and enables the generated output signals to be traced versus time. You’ve likely seen testbenches before (e.g., in E85) but you may have never written one from scratch by yourself before. While this is a bit daunting at first, the basic architecture of a testbench is straightforward. Before you proceed, you should review the HDL testbench section in Digital Design and Computer Architecture (Section 4.9 in the ARM edition) which covers several different styles of testbenches.
Note that testbenches include Verilog statements that are non-synthesizable (in other words, they cannot be mapped onto hardware). One example is the initial block, which runs only once at the start of a simulation. In this sense, testbenches use SystemVerilog a bit like a scripting language to enable designs to be easily tested and verified.
In this example we will create a testbench with automatic checking against a test vector file. This is the most powerful type of testbench and will be a good foundation for all the testbenches you will write for this class.
A testbench is composed of the following elements:
- Create and initialize the signals for the inputs, outputs, and internal signals of your modules.
- Instantiate the device under test (DUT)
- Generate a simulated clock signal. Even if your module is purely combinational, you will need this clock signal to control when the input test vectors are applied and the outputs checked.
- Load the test vectors and initialize the signals for the simulation (e.g., pulsing the reset line)
- Apply the inputs and check the resulting outputs using the test vectors. One simple way to do this is to apply the test vector on the rising edge of the clock and then check the outputs against the expect outputs on the negative edge of the clock.
Every HDL module that you write should have a testbench to accompany it. While this will cost you some time up front, it will save you loads of time and potential head injuries from banging your head against the wall wondering why your circuit is not working as you expect.
Creating the Testbench
Next, we will create the testbench. Create a new SystemVerilog file “fulladder_tb.sv” and add it to your project. Then, type the following code into the testbench.
`timescale 1ns/1ns
`default_nettype none
`define N_TV 8
module fulladder_tb();
// Set up test signals
logic clk, reset;
logic a, b, cin, s, cout, s_expected, cout_expected;
logic [31:0] vectornum, errors;
logic [10:0] testvectors[10000:0]; // Vectors of format s[3:0]_seg[6:0]
// Instantiate the device under test
fulladder dut(.A(a), .B(b), .Cin(cin), .S(s), .Cout(cout));
// Generate clock signal with a period of 10 timesteps.
always
begin
clk = 1; #5;
clk = 0; #5;
end
// At the start of the simulation:
// - Load the testvectors
// - Pulse the reset line (if applicable)
initial
begin
$readmemb("fulladder_testvectors.tv", testvectors, 0, `N_TV - 1);
vectornum = 0; errors = 0;
reset = 1; #27; reset = 0;
end
// Apply test vector on the rising edge of clk
always @(posedge clk)
begin
#1; {a, b, cin, s_expected, cout_expected} = testvectors[vectornum];
end
initial
begin
// Create dumpfile for signals
$dumpfile("fulladder_tb.vcd");
$dumpvars(0, fulladder_tb);
end
// Check results on the falling edge of clk
always @(negedge clk)
begin
if (~reset) // skip during reset
begin
if (cout != cout_expected || s != s_expected)
begin
$display("Error: inputs: a=%b, b=%b, cin=%b", a, b, cin);
$display(" outputs: s=%b (%b expected), cout=%b (%b expected)", s, s_expected, cout, cout_expected);
errors = errors + 1;
end
vectornum = vectornum + 1;
if (testvectors[vectornum] === 11'bx)
begin
$display("%d tests completed with %d errors.", vectornum, errors);
$finish;
end
end
end
endmodule
Note: Functions preceded by a dollar sign (e.g., $readmemb
, $display
, $finish
, etc.) are system functions.
The Testvector (.tv) File
The test vector file is a plaintext file with a single test vector on each line. Typically these are organized in the format <inputs>_<expected_outputs> (underscores are ignored but help to provide visual organization) but the specific format is up to you. You just need to make sure to assign the bits properly in your testbench module.
// fulladder_testvectors.tv
// Josh Brake
// jbrake@hmc.edu
// 5/26/22
//
// 5-bit vectors in binary.
// underscore is ignored
// // indicates comment
// a b cin _ s cout
000_00
001_10
010_10
011_01
100_10
101_01
110_01
111_11
Libraries
In many instances we want to leverage the iCE40 Technology Library. However, when we go to simulate these designs, we need to make sure to include the appropriate libraries so that ModelSim can find the correct design units when it goes to do the simulation.
When you install Lattice Radiant with ModelSim Lattice FPGA Edition, it comes with a set of installed libraries. For our purposes, we are most interested in the iCE40UP library and will need to include this in our simulation.
To do this, start the simulation by clicking Simulate > Start Simulation. Then navigate to the Libraries tab and add iCE40UP to the Search Libraries window using the “Add…” button.
Then navigate to the “Design” tab, select the top
module and then click OK. Your simulation should now start and open up the Wave window.
Note that you can also achieve the steps above by running vsim work.top -L iCE40UP
in the command window.