Simulation flow
This page shows how to run simulations using tsfpga and VUnit.
As far as simulations go tsfpga can be seen as a layer on top of VUnit. tsfpga helps manage the inputs to the simulation project: source files, test benches, test configurations, register code generation, IP cores, simlib, etc. All features of VUnit are available as they are, and all simulators are supported (ghdl as well as commercial).
Minimal simulate.py example
If the source code is roughly organized along the folder structure, running simulations using tsfpga and VUnit is as simple as:
from pathlib import Path
from tsfpga.module import get_modules
from vunit import VUnit
vunit_proj = VUnit.from_argv()
for module in get_modules(modules_folder=Path("path/to/my/modules")):
vunit_library = vunit_proj.add_library(module.library_name)
for hdl_file in module.get_simulation_files():
vunit_library.add_source_file(hdl_file.path)
module.setup_vunit(vunit_proj=vunit_proj)
vunit_proj.main()
The call to get_modules()
creates module objects
from the directory
structure of the folders listed in the argument.
The library name is deduced from the name of each module folder.
Source files, packages and testbenches are collected from a few standard locations within the
module folder.
Note
If you use a different folder structure within the modules than what is currently supported by tsfpga, feel free to create issue or a pull request..
The module.get_simulation_files()
call returns a list of
files (HdlFile
objects) that are to be included in the simulation project.
This includes source files and packages as well as test files.
If you use register code generation, the call will generate a new
VHDL package so that you are always simulating with an up-to-date register definition.
Actually even this example is not truly minimal.
The call to module.setup_vunit()
does nothing in default setup,
but is used to set up local configuration of test cases later.
Realistic example
If you want to dive into a more realistic example have a look at tsfpga/examples/simulate.py in the repo. Or continue reading this document for an explanation of the mechanisms.
This file handles things like
Only a subset of sources available when using a non-commercial simulator
Compile Vivado simlib and Vivado IP cores
Adding
hdl-modules
as modules that shall be compiled, but who’s tests shall not be run.
Local configuration of test cases
Running test cases in a few different configurations via generics is a common design pattern.
This can be achieved in tsfpga by creating a file module_<name>.py
in the root of the
module folder.
Say for example that we want to set some generics for a FIFO testbench, located in a module called
fifo
, which is located under modules
.
We would create the file modules/fifo/module_fifo.py
, and fill it with something like this.
from tsfpga.module import BaseModule
class Module(BaseModule):
def setup_vunit(self, vunit_proj, **kwargs):
tb = vunit_proj.library(self.library_name).test_bench("tb_fifo")
for width in [8, 24]:
for depth in [16, 1024]:
name = f"width_{width}.depth_{depth}"
tb.add_config(name=name, generics=dict(width=width, depth=depth))
This will result in the tests
fifo.tb_fifo.width_8.depth_16.all
fifo.tb_fifo.width_8.depth_1024.all
fifo.tb_fifo.width_24.depth_16.all
fifo.tb_fifo.width_24.depth_1024.all
So what happens here is that we created a class Module
that inherits from BaseModule
.
In this class we override the setup_vunit()
method, which does nothing in the super class, to
set up our simulation configurations.
The get_modules()
call used in our simulate.py
will recognize that this module has a
Python file to set up it’s own class.
When creating module objects the function will then use the user-specified class for this module.
Later in simulate.py
when setup_vunit()
is run, the code we specified above will be run.
Note
Note that the class must be called exactly Module
.
There is also a kwargs
argument available in the setup_vunit()
signature which can be used
to send arbitrary parameters from simulate.py
to the module.
This can be used for example to point out the location of test data.
Or maybe select some test mode with a parameter to our simulate.py
.
This is pure Python so we can get as fancy as we want to.
Vivado simulation libraries
Compiled Vivado simulation libraries (unisim, xpm, etc.) are often needed in the simulation project.
The VivadoSimlib
class provides an easy interface for handling simlib.
There are different implementations depending on the simulator currently in use.
The implementation for commercial simulators will compile simlib by calling Vivado with a TCL script
containing a compile_simlib ...
call.
For GHDL the implementation contains hard coded ghdl compile calls of the needed files.
The compilation with GHDL is very fast (5 seconds), but for commercial simulators it is very
slow (10 minutes).
All implementations are interface compatible with the VivadoSimlibCommon
class.
They will only do a recompile when necessary (new Vivado or simulator version, etc.).
Adding simlib to a simulation project using this class is achieved by simply doing:
from tsfpga.vivado.simlib import VivadoSimlib
...
vivado_simlib = VivadoSimlib.init(output_path=temp_dir, vunit_proj=vunit_proj)
vivado_simlib.compile_if_needed()
vivado_simlib.add_to_vunit_project()
Versioning of simlib artifacts
Compiling simlib takes quite a while for the commercial simulators. It might not be convenient to recompile on each workstation and in each CI run. Instead storing compiled simlib in, e.g., Artifactory or on a network drive is a good idea.
In simulate.py
we can query compile_is_needed
and artifact_name
to see if simlib will be compiled and
with what version tag.
If compile is needed, i.e. compiled simlib does not exist, they could instead be fetched from a
server somewhere.
The from_archive
and
to_archive
methods are useful for this.
Simulating with Vivado IP cores
The VivadoIpCores
class handles the IP cores that shall be included in a
simulation project.
From the list of modules it will create a Vivado project with all the IP cores.
This project shall then be used to generate the simulation models for the IP cores, which shall then
be added to the simulation project.
Note
The folder structure must be followed for this to work.
Adding IP cores to a simulation project can be done like this:
from tsfpga.vivado.ip_cores import VivadoIpCores
from vunit.vivado.vivado import create_compile_order_file, add_from_compile_order_file
...
vivado_ip_cores = VivadoIpCores(
modules=modules, output_path=temp_dir, part_name="xc7z020clg400-1"
)
vivado_project_created = vivado_ip_cores.create_vivado_project_if_needed()
if vivado_project_created:
# If the IP core Vivado project has been (re)created we need to create
# a new compile order file
create_compile_order_file(
project_file=vivado_ip_cores.vivado_project_file,
compile_order_file=vivado_ip_cores.compile_order_file
)
add_from_compile_order_file(
vunit_obj=vunit_proj, compile_order_file=vivado_ip_cores.compile_order_file
)
Note that we use functions from VUnit to handle parts of this.
The create_compile_order_file()
function will run a TCL script on the project that generates
simulation models and saves a compile order to file.
The add_from_compile_order_file()
function will then add the files in said compile order to the
VUnit project.
Simulating a subset based on git history
When the number of tests available in a project starts to grow, it becomes interesting to simulate only what has changed. This saves a lot of time, both in CI as well as when developing on your desktop.
There is a tool in tsfpga called GitSimulationSubset
which helps find a minimal subset of
testbenches that shall be compiled and run based on the git history.
A testbench shall be compiled and executed if
the testbench itself has changed, or if
any of the VHDL files the testbench depends on have changed.
Whether or not a file has changed is determined based on git information, by comparing the local
branch and working tree with a reference branch.
The reference would be origin/main
most of the time.
The subset of tests returned by the class can then be used as the test_pattern
argument when
setting up your VUnit project.
This tools is used in tsfpga CI to make sure that for pull requests only the minimal set of tests
is run.
This saves an immense amount of CI time, especially for commits that do not alter any VHDL code.
For nightly main
runs the full set of tests shall still be run.
See the class documentation
for more information, and
tsfpga/examples/simulate.py
in the repo for a usage example.