Exploring the Arrow SoCKit Part VI - Simulation in ModelSim
In the last post, we created a unit that computes MD5 checksums. Before we program it onto the FPGA, we want to simulate it and verify that it is operating correctly. To do this, we use the ModelSim circuit simulator.
To run ModelSim simulations, we create testbenches, which are programs written in an HDL that describe events that occur at different times. Here is an example of a testbench written in SystemVerilog.
SystemVerilog is a language based on Verilog with several extensions. We use
it in the testbench mainly because of the assert
statement.
This testbench tests a clock-synchronized AND gate.
In the always
block, we toggle the value of the clock every 10 ns
(to simulate a 50 MHz clock frequency). The #delay
syntax causes a statement
to occur a given number of picoseconds later in the simulation.
In the initial
block, we set the values of a and b, wait two cycles,
and then assert that the output value is correct.
You can add a testbench to your design by going to “Assignments” -> “Settings” ->
“EDA Tool Settings” -> “Simulation”. Click on “Test Benches” -> “New” to add
a new test bench. Make sure to set “Test Bench Name” and “Top Level Module”
to the name of the module (in this case, example_tb
) and to set the
simulation period to a reasonable amount of time (180 ns would be sufficient
for this example). You can then choose the newly created testbench in the
dropdown menu.
Before we run ModelSim, we will need to tell Quartus where to find the ModelSim binaries. The binaries can be found at “modelsim_ase/bin” from the root of your Altera installation. So, for instance, if you told the Altera installer to put everything in “/opt/altera/13.1”, the modelsim binaries will be in “/opt/altera/13.1/modelsim_ase/bin”. You can set the directory in “Tools” -> “Options” -> “EDA Tool Options” -> “ModelSim-Altera”. Once the directory is set, you can run the simulation by clicking the “RTL simulation” button, which is the fifth from the right in our Quartus toolbar screenshot.
The simulation should open up a new window. If this does not happen, there may be something wrong with your ModelSim installation. You can check the Arch Wiki to make sure you have all the dependencies installed.
Once the simulation finishes running, the testbench signals should appear in the main window. You can see the full simulation run by clicking on the filled-in magnifying glass with tool tip “Zoom Full” or by pressing Z on the keyboard. It should look something like the following.
You should also see no assertion failures or errors in the command window at the bottom.
Verifying the MD5 Unit
To verify our MD5 unit, we will use a similar technique as above. We put in some input, run the computation, and then check that the output is correct. With more complicated computations like MD5, we can generate the input and output programmatically.
To get our input, we will just create a random sequence of bytes. On Linux, we can do this using
head -c 42 /dev/urandom > testsequence.bin
We can find the md5sum of this using
md5sum testsequence.bin
However, we can’t just copy and paste the bytes of testsequence.bin into our testbench because it hasn’t been appropriately padded. We can write a C program to pad the input.
The reverse_if_needed
function checks to see if the processor architecture
on which the program is being run is big-endian and, if so, reverses the
order of the bytes in each 32-bit word. This is necessary since we will be
putting the input in a word at a time.
You can see the full padding program on Github. The code is split across the md5.c and padandprint.c files.
Now that we have our input, we can write our testbench.
The testbench resets the md5unit, writes the input to the memory, starts the computation, and checks the digest at the end.
Debugging in Simulation
The testbench should run without any assertion errors, but this is because I spent quite some time debugging and fixing small mistakes. In general, you will get assertion errors the first time you run your testbench. This is okay, since finding errors is the whole point of simulation. Here are a few strategies for using ModelSim to debug your hardware.
Exposing Internal Signals
By default, ModelSim will only show you the signals declared in the top-level testbench module. This is not very helpful in debugging, since the problem will most likely be in a signal internal to the unit you are testing. Fortunately, ModelSim provides a way of showing internal signals in the simulation window through the use of TCL scripts.
The TCL script used in my design to set up the simulation looks like this.
You can tell ModelSim to use the script to set up the simulation by going to the Simulation settings in Quartus and filling in the “Use script to set up simulation” option.
As you can see, add wave
is the basic way of adding a signal to the
viewer. You can add refer to internal signals using slashes.
You can also refer to signals inside generate statements using square brackets.
In this case, the signal name must be wrapped in curly braces to prevent the
square brackets from being interpreted as command substitution.
You can also use the -radix
option to change the radix displayed for a
multi-bit signal in the simulation window. The default is binary, but you can
also choose unsigned
, decimal
, or hexadecimal
.
Checking Intermediate Results
To debug, you will have to trace the data flow backwards or forwards until you find the point at which the signal value diverges from its expected value. Sometimes, it is difficult to know what the intermediate values should be. In this case, it is helpful to write a software simulation of the computation and print out what the expected values of registers are. For instance, in our case, it would be helpful to know the values of A, B, C, and D after each cycle of the computation. Therefore, we write a C function that computes the new register values for each cycle.
Then we print them out as we go along.
You can find the full code in the same folder as “padandprint.c”. It’s called “reference.c”.
By checking the output of the reference program against the signals exposed in your ModelSim view, you can track down the bug in your Verilog description.
Conclusion
So now you know that the md5unit is working correctly. In the next post, we will create a Qsys system containing several copies of the MD5 unit, write software for the HPS to control the FPGA units, and take some measurements on how fast our system can compute checksums.