Simulation

The best way to test your HDL code is to simulate it. This allows you to test your design without having to flash it to the FPGA, which is usually a time-consuming process. XiBIF supports multiple commands to help you simulate and test your design.

Testbench

XiBIF incorporates the hdltbgen tool to generate a testbench for your design. This tool generates a VHDL testbench that can be used to simulate your design. The testbench can be generated using the following command:

xibif testbench -f <my_vhdl_file.vhd>

This will generate a testbench for the specified VHDL file. The file must be a VHDL file that is present in the hw/src directory of your project.

The tool will generate two testbenches: One that uses VUnit as simulation framework (created in the folder hw/tb/vunit) and one that uses no framework (created in the folder hw/tb).

The generated testbench integrates your design as a component and instantiates it. Additionally, it can include a clock generator and a reset generator if you specify the clock and reset signals using the parameters -c or -rn | -rp. For instance, the command to generate the testbench might look like this:

xibif testbench -f adder.vhd -c clk_in -rn resetn_in

The testbench processes stimulus and response pairs from a .csv file, which you can create using tools like Python scripts. A sample .csv file is also generated in the hw/tb folder.

Simulation

XiBIF supports the simulation of your design using the VUnit framework. VUnit is a testbench framework that allows you to run your testbenches in a simple and efficient way. To run all your testbenches, you can use the following command:

xibif simulation

This will run all the testbenches in the hw/tb/vunit folder. The testbenches are automatically detected and run. The results of the simulation are printed to the console. If you are using Questasim, the coverage of the simulations is also gathered, and an HTML report is generated in the hw/results/coverage folder. The coverage report can be opened in a web browser.

It is also possible to run a specific testbench. To do this, you can use the following command:

xibif simulation -t <test_name>

This will run the specified testbench. The testbench must be present in the hw/tb/vunit folder and must be a VUnit testbench.

To get a list of all available testbenches, you can use the following command:

xibif simulation -l

If you need to debug your testbench, you can add the flag -g to the command. This will open the simulation in the GUI of the simulator.

Adding Xilinx simulation libraries

If you are using Xilinx primitives in your design, you need to add the Xilinx simulation libraries to your simulation. For this, the run.py script in the hw/tb/vunit folder needs to be modified. Add the following lines to the script:

libraries['unisim'] = vu.add_library('unisim')
path_unisim = str(project.vivado_binary.parent.parent.joinpath('data/vhdl/src/unisims'))
unisim_files = glob.glob(path_unisim + '/*.vhd', recursive=True)
unisim_files.extend(glob.glob(path_unisim + '/primitive/*.vhd', recursive=True))
unisim_files = [w.replace('\\', '/') for w in unisim_files]
for designfile in unisim_files:
    libraries['unisim'].add_source_files(designfile)

Note

It is recommended to only add the primitives you need in your design. This will speed up the compilation. The above code adds all primitives in the unisim library. If you only need a few, you can change the glob pattern to only include the files you need. For example, if you only need the FlipFlop primitives, you can use the following line:

unisim_files = glob.glob(path_unisim + '/primitive/FD*.vhd', recursive=True)

Compilation of simulation libraries

In rare cases, you might need to compile the simulation libraries for your simulator. XiBIF supports the compilation of the Xilinx simulation libraries as well as the VUnit libraries for use with questasim. To compile the libraries, you can use the following command:

xibif simulation -cx # for Xilinx libraries
xibif simulation -cv # for VUnit libraries

This will compile the libraries and place them in the hw/results/compile_xyz folder. For questasim, the libraries need then to be defined in the modelsim.ini file.

Note

This compilation is only needed if you do not want to use VUnit as simulation framework or your simulator is not supported by VUnit.

VUnit

VUnit is an open-source unit testing framework for VHDL and SystemVerilog. It is inspired by modern software testing tools (like JUnit, pytest, etc.) and is designed to improve the automation, modularity, and scalability of testbenches in FPGA/ASIC development. The full documentation can be found at VUnit Documentation.

Key Features

  • Automated test discovery and execution

  • Test result reporting (pass/fail, logs)

  • Dependency management (e.g., for libraries and external files)

  • OS-independent command-line runner (Python-based)

How to transform a traditional VHDL testbench to VUnit

Step 1: Add VUnit Libraries

VUnit provides a set of libraries that need to be included in your testbench. These libraries are used to provide the test runner functionality and the checker functions. You can add the libraries by including the following lines at the beginning of your testbench file:

library vunit_lib;
context vunit_lib.vunit_context;

Step 2: Add a generic to your entity.

You need to add a generic to your testbench entity. This generic will be used to pass the test runner configuration to the testbench. The generic should be of type string and should have a default value of runner_cfg_default. Additionally, it is possible to add a generic tb_path to your testbench entity. This generic is set by VUnit to the path of the testbench. The entity declaration should look like this:

entity tb_my_module is
    generic (
        runner_cfg : string := runner_cfg_default;
        tb_path    : string
    );
end entity;

Step 3: Modify your testbench process

You need to modify your testbench process to use VUnit’s test runner. This is done by adding a call to the test_runner_setup procedure at the beginning of your process and a call to test_runner_cleanup at the end. Additionally, you need to wrap your test logic in a conditional statement that checks if the test is being run. This is done using the run function. The run function takes a string argument that specifies the name of the test. You can use this to specify the name of your test.

Before (classic testbench):

process
begin
    -- Your test logic here
    a <= '1';
    wait for 10 ns;
    assert b = '1' report "Test failed" severity error;
    wait;
end process;

After (VUnit testbench):

process
begin
    test_runner_setup(runner, runner_cfg);

    if run("basic_test") then
        -- Your test logic here
    end if;

    test_runner_cleanup(runner);
end process;

Checker functions

VUnit provides a set of checker functions that can be used to check the results of your test. These functions are similar to the assert statements in a classic testbench but provide more information about the test. The documentation for the checker functions can be found at VUnit Check Library.

Boolean Checks

  • check_true(expr, msg="") Assert that the boolean expression expr is true.

  • check_false(expr, msg="") Assert that the boolean expression expr is false.

Equality / Inequality Checks

These are overloaded for types like integer, real, std_logic, std_logic_vector, string, etc.

  • check_equal(actual, expected, msg="") Passes if actual = expected.

  • check_not_equal(actual, expected, msg="") Passes if actual /= expected.

Relational Checks

  • check_less(actual, expected, msg="") Passes if actual < expected.

  • check_less_or_equal(actual, expected, msg="") Passes if actual <= expected.

  • check_greater(actual, expected, msg="") Passes if actual > expected.

  • check_greater_or_equal(actual, expected, msg="") Passes if actual >= expected.

Range Checks

  • check_in_range(actual, low, high, msg="") Passes if low <= actual <= high.

  • check_not_in_range(actual, low, high, msg="") Passes if actual < low or actual > high.

Delta / Tolerance Checks

For real and fixed-point values, allows for numerical tolerances.

  • check_equal(actual, expected, tolerance, msg="") Passes if abs(actual - expected) <= tolerance.

  • check_within(actual, center, tolerance, msg="") Passes if center - tolerance <= actual <= center + tolerance.

  • check_not_within(actual, center, tolerance, msg="") Passes if actual lies outside the tolerance range.

Meta Checks

  • fail(msg="") Unconditionally fails the test with the given message.

  • pass(msg="") Marks a test as explicitly passed with a note (useful in parameterized tests).

  • check_stable(signal) Ensures that a signal has not changed between simulation cycles.

  • check_implication(premise, consequence, msg="") Asserts logical implication: if premise is true, then consequence must also be true.

Logging and Info-Level Messages

Useful for reporting information without affecting pass/fail status.

  • note(msg="") Logs a non-intrusive note.

  • info(msg="") Logs an informational message.

  • warning(msg="") Logs a warning message (does not fail the test).

  • error(msg="") Logs an error message (fails the test).