on
Testing the LABS modules
In the previous post in this series I introduced the LABS problem, and explained some of the relevant properties of the binary sequences. I also sketched out my proposed solution and started work on implementing this solution in SystemVerilog.
In this post I will cover some design verification for the modules we have designed thus far, so that we can be confident of our basic building blocks when we start wiring them together.
You can view the other posts in this series here:
Design Verification
Verifying RTL designs is a critical part of the design process. Debugging on FPGAs is a tough challenge, so it is important to have other methods of catching bugs and building up confidence in the code. In this blog post we will first develop a high level model of our problem (in this case using Python code); personally I find these models really useful for a few reasons, which I will explain in more detail in the next section. After creating the high-level model we will create a basic handwritten testbench; I normally use these testbenches for two reasons: to verify the basic functionality of the RTL block under test, and to generate waveforms for inspection. The focus of the next blog post will be on writing automated test benches to significantly increase the coverage of our testing.
Python Models
As I mentioned before, I like to write models of the RTL code in a high-level language like Python. Such models can be used to calculate the expected outputs for a given input, which is useful when running simulations later. In a high level model, I can use higher level concepts and libraries, which makes it easier to prove. For example, when working on a DSP signal processing chain I can use built-in maths functions and floating point numbers; or, if I am working on an implementation of a hashfunction I can use the built-in libraries in Python. And finally, we can use the model to verify the output signals in automated testbenches, which can cover a much wider range of inputs than handwritten testbenches.
The listing below shows the Python models for the three RTL modules we developed in the last post: calc_ck
, square_accumulate
and calc_e
.
The Python function calc_ck_impl
represents the calc_ck
module, and the calc_ck
function is a convenience function which calculates the second input sequence based on the original sequence.
One downside of using Python is that integer types do not have a fixed width, so we have to use some bit-fiddling using masks to accurately represent the effect of signals with fixed widths in hardware.
|
|
We can also use pytest
to write some unit test for our model, to verify the model.
In this case the inputs for the tests are values I worked out by hand to populate the example diagrams in the previous blog post.
|
|
Now we have a good model in Python for the RTL modules we are about to test, and we have written some basic unit tests to validate their behaviour. The next step is to hand-write test benches for each of these modules.
Testbenches
The following testbenches are written by hand, using the same test inputs we used above in the unit tests for the Python models. The value of these testbenches is that we can write them quickly and use them to verify the basic functionality of our RTL code, while also generating waveform files which we can inspect.
In this post we will use a tool called Icarus Verilog (also known as iverilog
) to run our simulations / testbenches.
iverilog
is an open source tool which aims to be able to compile all of the Verilog HDL standard, though as of this time the support for recent additions to the specification is limited.
With iverilog
we can compile our Verilog code into an executable which we can run on our system.
We will also use GTKWave to visualise the waveform files which can be generated using iverilog
.
Correlation Calculation
The first testbench, for the calc_ck
block, is listed below.
The testbench creates an instance of calc_ck
with a seuence width of 8 bits.
It is then tested using four inputs: (0, 0)
, (15, 0)
, (15, 255)
, (0, 255)
.
These are the same inputs we used in the unit test for the Python model, so we know the expected outputs.
We are using the $display
command to print the output signal z
on the commandline, and we are using assert
to test that the output matches the results we expect.
|
|
The testbench can be compiled into an executable using iverilog
as follows, and then it can be executed.
The output from the $display
command matches the expected values based on our Python model.
We can also use gtkwave
to inspect the dumpfile tb_calc_ck.vcd
, which contains the waveforms generated by the testbench.
The figure below shows these waveforms (you can click on the image to expand it) in GTKWave.
In this case the waveform does not show us much that we did not already know, but it can be very useful for more complicated designs.
When working with buses it allows you to view all the signals invovled in a bus transactions in a convenient way, when working on FPGA designs for mixed signal applications, the waveform viewers can plot the digital signal as analog trace and when debugging why a simulation is not giving the expected result, it allows you to view signals buried deep inside the hierachy of the design.
Square Accumulate
The second hand-written testbench is for the square_accumulate
module.
Again we use a series of inputs which we have already verified using the Python model, and we use $display
and assert
to print and check the output from the unit-under-test.
|
|
Compiling and running the testbench results in the expected output, and all the assertions are passed successfully.
GTKWave shows us the waveforms of our unit under test (click on the image to expand it).
Energy Calculation
The final testbench is for the calc_e
module, which uses the calc_ck
and square_accumulate
modules internally.
I have broken down the testbench into a four parts to make it easier to understand.
The first part of the testbench looks very similar to the previous testbenches.
First we instantiate the calc_e
module (named UUT
) with an input sequence width of 4 bits and output energy width of 16 bits.
In this testbench we will be calculating the energy content of all 16 possible sequences of 4 bits, just like in the Python unit tests.
|
|
The next part is to set up the array of expected output values.
Using iverilog
I was not able to initialize the array directly, so I had to assign it item by item instead.
|
|
The next block generates the input sequences, with a new sequence being generated every cycle; at the start of the simulation we wait one clock cycle before generating the first input sequence.
This block also controls the i_valid
flag, which signals that the input to calc_e
is valid.
The final block is similar to the previous testbenches; this block reads the output values from the calc_e
module and compares them to the expected results.
It also checks that the o_valid
flag is set as expected and that the copy of the input sequence (o_seq
) matches the actual input sequence.
At the start of the block we must wait for one cycle to match the previous block, where only started generating valid input data after one clock cycle, and then we must wait for four clock cycles due to the latency of the calc_e
module, which is equal to the width of the input sequence.
|
|
We can compile the testbench (note that we have to pass in the sources files calc_ck.sv
and square_accumulate.sv
too) and run it in the terminal.
Notice that this time we get a warning about a SystemVerilog feature which not yet supported by iverilog
.
Thankfully for us, the workaround it uses is fine for our purposes.
From the output we can see that all the assertions pass, and the output from the $display
commands matches our expected energy values.
|
|
GTKWave shows us the waveforms of our testbench (click on the image to expand it).
In this example I have included some internal signals from the calc_e
module, specifically the inputs and outputs of the calc_ck
and square_accumulates
in all the internal stages.
Conclusion
In this post we have looked at design verification using a combination of high level models written in Python and hand-written testbenches written in SystemVerilog.
The high level models can be used to better understand the problem at hand, and they can be used to verify the results from the RTL simulations.
We have used Icarus Verilog to run our simulations and used the built-in $display
and assert
commands to verify the results of our units-under-test.
We have also used GTKWave to visualise the waveforms which were dumped to a file by iverilog
; these waveforms are very useful when debugging complex designs.
In the next post we will extend these concepts further by combining SystemVerilog and Python to write automated testbenches which can have increased test coverage.