Using a verification framework can dramatically improve the quality of our designs. However, setting up a verification framework from scratch can be quite daunting. Luckily, there are open-source libraries that handle most of the infrastructure required for creating, running and managing our tests, so that we can focus on the parts of our testbench that are unique to our design and add value to our development process.

What is VUnit?

VUnit is a open-source unit-testing framework for digital systems design with VHDL and SystemVerilog. It provides the infrastructure for creating and managing verification environments so we can focus on writing meaningful tests. At its core, VUnit provides a Python-based test runner that manages our test cases, handles compilation dependencies, and produces structured test reports.

In addition to automating the simulation runs and test management, VUnit also provides several libraries to help write our testcases. These libraries include verification components, logging echanisms, check procedures, and randomization. By leveraging these pre-built components we can make the testcase-creation process more efficient, less error-prone, and - quite frankly - more enjoyable.

Prerequisites for Setting up a Verification Environment with VUnit

There are two prerequisites for using VUnit: we must have a supported VHDL simulator and we must have a Python installation (including a package manager and a tool for creating virtual environments).

VUnit supports several simulators, a comprehensive list can be found in the VUnit documentation. For this example we will use GHDL. In Ubuntu 24.04.1 LTS we can install GHDL using the Snap packet manager.

1
sudo snap install ghdl

Once we have our simulator, we can install the VUnit Python module. The recommended way to do this is within a Python virtual environent. If we’re using venv we can do this from the terminal by moving into the folder where we’ll run our simulations and using the commands shown below.

1
2
3
python3 -m venv venv
source venv/bin/activate
pip install vunit_hdl

We are now ready to create our verification environemnt.

Setting up a minimal Verification Environment with VUnit

A VUnit verification environment includes, at the very least, two files: a testbench and a simulation script.

This is what a VUnit testbench in its simplest form looks like, as described in the VUnit User Guide.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
library vunit_lib;
context vunit_lib.vunit_context;

entity tb_example is
  generic (runner_cfg : string);
end entity;

architecture tb of tb_example is
begin
  main : process
  begin
    test_runner_setup(runner, runner_cfg);
    report "Hello world!";
    test_runner_cleanup(runner); -- Simulation ends here
  end process;
end architecture;

We must pair our testbench with a simulation script, which in its minimal form looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from vunit import VUnit

# Create VUnit instance by parsing command line arguments
vu = VUnit.from_argv()

# Add VUnit's builtin HDL utilities
vu.add_vhdl_builtins()

# Create library 'lib'
lib = vu.add_library("lib")

# Add all files ending in .vhd in current working directory to library
lib.add_source_files("*.vhd")

# Run vunit function
vu.main()

We can run the simulation script from the command line.

1
python3 run.py

The terminal output confirms that the test was run succesfully. Line 4 shows the location of the file where all the messages in our testbench are logged.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Compile passed

Starting lib.tb_example.all
Output file: /home/isaac/Projects/blog/004_vunit_verification_environment/vunit_out/test_output/lib.tb_example.all_7b5933c73ddb812488c059080644e9fd58c418d9/output.txt
pass (P=1 S=0 F=0 T=1) lib.tb_example.all (0.3 seconds)

==== Summary ==============================
pass lib.tb_example.all (0.3 seconds)
===========================================
pass 1 of 1
===========================================
Total time was 0.3 seconds
Elapsed time was 0.3 seconds
===========================================
All passed!

Our testcase executed a single statement - printing “Hello World!” to the output file.

1
2
/home/isaac/Projects/blog/004_vunit_verification_environment/vunit_tb.vhd:13:5:@0ms:(report note): Hello world!
simulation stopped @0ms with status 0

Using AXI-Stream Verification Components

In a future post we will use this simulation environment to test an audio processing pipeline using AXI-Stream buses to transfer the audio data between modules. VUnit includes Verification Components with support for several standard interfaces, including AXI-Stream.

To get started, we include the vu.add_verification_components() statement in our simulation script to add support for the Verification Components.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from vunit import VUnit

# Create VUnit instance by parsing command line arguments
vu = VUnit.from_argv()

# Add VUnit's builtin HDL utilities
vu.add_vhdl_builtins()

# Add VUnit Verification Components
vu.add_verification_components()

# Create library 'lib'
lib = vu.add_library("lib")

# Add all files ending in .vhd in current working directory to library
lib.add_source_files("*.vhd")

# Run vunit function
vu.main()

We can now expand our existing testbench by adding one AXI-Stream Master and one AXI-Stream Slave Verification Component. First we add all the signals and constants that we’ll use to configure and connect the verification components.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
architecture tb of tb_example is

  signal    clk             : std_logic := '0';
  constant  CLK_PERIOD      : time      := 10 ns;
  constant  AXIS_DATA_WIDTH : integer   := 24;

  -- AXI-Stream Master Interface Deifnition and Configuration
  constant m_axis : axi_stream_master_t := new_axi_stream_master(
    data_length => AXIS_DATA_WIDTH,
    stall_config => new_stall_config(0.05, 1, 10)
  );

  -- AXI-Stream Slave Interface Deifnition and Configuration
  constant s_axis  : axi_stream_slave_t  := new_axi_stream_slave(
    data_length => AXIS_DATA_WIDTH,
    stall_config => new_stall_config(0.05, 1, 10)
  );

  -- AXI-Stream Signals
  signal axis_valid  : std_logic;
  signal axis_ready  : std_logic;
  signal axis_last   : std_logic; 
  signal axis_data   : std_logic_vector(AXIS_DATA_WIDTH-1 downto 0);

begin

Then we can add the Verification Components themselves.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
begin

  vunit_axism : entity vunit_lib.axi_stream_master
    generic map (
      master => m_axis
    )
    port map (
      aclk   => clk,
      tvalid => axis_valid,
      tready => axis_ready,
      tdata  => axis_data,
      tlast  => axis_last
    );

  vunit_axiss : entity vunit_lib.axi_stream_slave
    generic map (
      slave => s_axis
    )
    port map (
      aclk   => clk,
      tvalid => axis_valid,
      tready => axis_ready,
      tdata  => axis_data,
      tlast  => axis_last
    );

  main : process
  begin
    test_runner_setup(runner, runner_cfg);
    report "Hello world!";
    test_runner_cleanup(runner); -- Simulation ends here
  end process;

end architecture;

We are now ready to generate - and capture - data over the AXI-Stream bus. To do this we must expand our ‘main’ process to start the data transmission and reception in the AXI-Stream Verification Components.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
main : process
  variable axis_tdata_v : std_logic_vector(AXIS_DATA_WIDTH-1 downto 0);
  variable axis_tlast_v : std_logic;
begin
  test_runner_setup(runner, runner_cfg);
  report "Hello world!";
  for i in 0 to 99 loop
    push_axi_stream(net, m_axis, std_logic_vector(to_unsigned(i, AXIS_DATA_WIDTH)) , tlast => axis_tlast);
    pop_axi_stream(net, s_axis, tdata => axis_tdata_v, tlast => axis_tlast_v);
  end loop;
  wait for 1 us;
  test_runner_cleanup(runner); -- Simulation ends here
end process main;

At this point we can simulate our expanded testbench. Because I’m using GHDL as my simulator, I’ll include an argument that creates a VCD waveform in the simulation script.

1
python3 run.py --gtkwave-fmt vcd

Wer can finally see the results of our simulation. For this example I’m using Surfer as a waveform viewer. Surfer is a VSCode extension, which allows us to visualize our waveforms without leaving the code editor.

Simulation with VUnit AXI-Stream Components

Zooming in at the start of the simulation, we can see that the tvalid and tready signals of the AXI-Stream bus are toggled by the Verification Components. For this they use the parameters we set when we declared the m_axis and s_axis constants for the AXI-Stream Master and Slave interfaces, respectively.

Start of Simulation with VUnit AXI-Stream Components

Summary

The goal of this post was to create a minimal simulation environment with VUnit that included driving a monitoring AXI-Stream interfaces.

We started with a minimal VHDL testbench and Python simulation script, which we used to create a bare-bones ‘Helo World!’ simulation. We then expanded the simulation script to include the VUnit verification components in our environment and added the logic required for instantiating, connecting, and using the Verification Components in our testbench. Finally, we ran our simulation and confirmed that our verification environment worked by inspecting the generated waveform.

Cheers,

Isaac