How to Write a Basic Testbench using VHDL

In this post we look at how we use VHDL to write a basic testbench. We start by looking at the architecture of a VHDL test bench. We then look at some key concepts such as the time type and time consuming constructs. Finally, we go through a complete test bench example.

When using VHDL to design digital circuits, we normally also create a testbench to stimulate the code and ensure that the functionality is correct. We can write testbenches using a variety of languages, with VHDL, Verilog and System Verilog being the most popular.

System Verilog is widely adopted in industry and is probably the most common language to use. If you are hoping to design FPGAs professionally, then it will be important to learn this skill at some point.

As it is better to focus on one language as a time, this blog post introduces basic VHDL testbench principles. This allows us to test designs while working through the VHDL tutorials on this site.  

If you are interested in learning more about testbench design using either VHDL or SystemVerilog, then there are several excellent courses paid course available on sites such as udemy.

Architecture of a Basic VHDL Testbench

Testbenches consist of non-synthesizable VHDL code which generate inputs to the design and checks that the outputs are correct.

The diagram below shows the typical architecture of a simple testbench.

Block diagram of a testbench, showing a stimulus block which generates inputs to the design and an output checker which checks for the correct outputs.

The stimulus block generates the inputs to the FPGA design and a separate block checks the outputs. The stimulus and output checker will be in separate files for larger designs. It is also possible to include all of these different elements in a single file. 

The main purpose of this post is to introduce the skills which will allow us to test our solutions to the exercises on this site.

Therefore, we don’t diccuss the output checking block as it adds unnecessary complexity. Instead, we can use a simulation tool which allows for waveforms to be viewed directly.

The freely available software packages from Xilinx (Vivado) and Intel (Quartus) both offer this capability and are recommended as tools for learning VHDL. 

The first step in writing a testbench is creating a VHDL component which acts as the top level of the test.

As we discussed in a previous post, we need to write a VHDL entity architecture pair in order to create a VHDL component.

We use the entity to define the inputs and outputs to our design. However, as the testbench has no inputs or outputs, we create an empty VHDL entity.

The code snippet below shows the syntax for doing this.

entity test_bench is
end entity test_bench;

DUT Instantiation

The architecture of the testbench must contain an instantiation of the design under test (DUT).

We use the same methods for this as we discussed in the post about signal assignment in VHDL. This means that we can instantiate the DUT using either component or direct entity instantiation.

When using component instantiation , we must define the component before using it in the code. We can either do this in a separate VHDL package or before the main code (in the same way as a signal).

The code snippet below shows the syntax we use to declare a component in VHDL. The component and port names must match the names used in the entity of the DUT.

component and_gate is
  port (
    a       : in std_logic;
    b       : in std_logic;
    and_out : out std_logic
  );
end component and_gate;

We connect the component to the main circuit after we have declared it. The code snippet below shows the method for doing this.

add_gate_instance: component and_gate
  port map (
    a       => signal_a,
    b       => signal_b,
    and_out => signal_and_out
  );

Every instantiation of the component must have a unique name, in the VHDL code example above this is “and_gate_instance”.

The names on the left-hand side of the port map correspond to the names of the component ports.

The names on the right-hand side are the signals which connect to the ports.  We must declare these signals before using them and they must also have the same type as the ports which they connect to.

The second technique we can use is direct entity instantiation. When using this method, we don’t have to declare a separate component. The code snippet below shows the syntax for instantiating an AND gate using this method.

and_gate_instance: entity work.and_gate(rtl)
  port map (
    a       => signal_a,
    b       => signal_b,
    and_out => signal_and_out
  );

As with the component instantiation technique, each instantiation must have a unique name.

Likewise, the names of the ports are on the left and the name of the signals are on the right.

There are two additional requirements for this type of instantiation – the library and the architecture must also be specified. In the example above, we use a library called “work” and an architecture called “rtl”.

Time Consuming Statements

One of the key differences between testbench code and design code is that we don’t need to synthesize the testbench.

As a result of this, we can use special constructs which consume time. Infact, this is crucial for creating test stimulus.

There are two main constructs in VHDL which we use to consume time.

The most basic method is the VHDL after statement which we can either use concurrently or within processes.

We can also use the wait statement but this is only valid within processes and is slightly more complex to understand. 

VHDL Time Type

We use a special pre-defined type in VHDL to specify time.

When we define a period using the time type we must give both a number and a time unit.

The table below shows the list of time units which we can use with the VHDL time type.

UnitValue
fs
ps1000 fs
ns1000 ps
us1000 ns
ms1000 us
sec1000 ms
min60 sec
hr60 min

According to the IEEE 1076-2008 VHDL standard, the implementation of the time type is dependent on the simulation tool which is being used.

However, by default the smallest resolution of the VHDL time type is 1fs. As a result of this, we can only use decimal fractions to specify the time if we are not using the fs base unit.

Bearing this in mind, the VHDL code below shows examples of the time type in use.

time_ex <= 100 fs;  -- 100 femtoseconds
time_ex <= 1.1 ns;  -- 1100 picoseconds
time_ex <= 1.1 sec; -- 1100 milliseconds

VHDL After Statement

We use the after keyword in VHDL to assign signals a value at a specified time in the future. We can use the after statement in concurrent statements or within processes.

The VHDL code snippet below shows the syntax we use for the after statement.

<signal> <= <initial_value>, <end_value> after <time>;

The first part of the assignment, which occurs before the comma, is relatively simple to understand. This functions in the same way as normal signal assignments which we have encountered before.

The second part of the code schedules a further update to the signal at a specified point in the future.

The <time> variable is used to specify exactly when the signal changes value.

We use the time type which we discussed above to specify the value of the <time> field.

We often use the VHDL after statement to reset the FPGA at the start of a simulation.

To do this, we firstly drive the initial value of the reset signal so that it is in its active state.

We then schedule a change in state so that the reset signal becomes inactive after some time.

The code snippet below gives an example of this, with the reset being active for 1 us.

reset <= '1', '0' after 1 us;

It is also possible to use the after statement without an initial assignment in VHDL.

We can use this approach to continually schedule changes to the signal state. This is useful for generating clocks, as the signal can be inverted at fixed intervals.

The code snippet below shows a basic method for generating a clock in a VHDL testbench.

clock <= not clock after 10 ns;

VHDL Wait Statement

We use wait statements in VHDL to temporarily suspend the execution of code within a VHDL process block.

However, we can only use this construct within a process which doesn’t have a sensitivity list. This is because the two techniques perform the same function of blocking the execution of the process code.

In the case of the sensitivity list, the execution stops until one of the designated signals changes state.

In the case of the wait statement, the code stops either for a set period of time or until a signal changes state. 

There are three different types of wait statement we can use, all of which are discussed below.

Wait for Statement in VHDL

We use the wait for statement in VHDL to suspend execution of code for a given period of time. The code snippet below shows the syntax for this.

wait for <time>;

This csontruct is simple to understand, as the execution of the code will pause for a given period of time.

The length of time to wait is specified with the <time> variable, which must be specified as a VHDL time type.  

This method is useful in a test bench as we often want to set the input signal and then wait for an output to be generated.

As an example, the VHDL code snippet shows how we use the wait for statement to pause the execution of a process block for 1 ms.

wait for 1 ms;

Wait Until Statement in VHDL

We use the wait until statement in VHDL to suspend the execution of code within a process until a given logical expression evaluates as true.

The code snippet below shows the syntax for this.

wait until <condition> for <time>;

In this case, the execution of the code is paused until the <condition> statement evaluates as true. Normally this means waiting until one or more signals have a certain value.

However, we can also use the rising_edge or falling_edge macros to wait for specific events to occur.

The for statement in this construct is optional but we can use this as a time out function. When it is used, the code will pause until either the <condition> branch is true or the time given by <time> has expired.

To better demonstrate how this statement works, let’s consider a basic example.

In this example, we wait until two signals (sig_a and sig_b) are set to logical 1.

However, if the condition isn’t met after 1 us then we want to resume execution of the process block.

The VHDL code snippet below shows how we use the wait until statement to do this.

wait until (sig_a = '1' and sig_b = '1') for 1 us;

Wait on Statement in VHDL

We use the wait on statement in VHDL to suspend execution of code until a signal changes state.

The code snippet below shows the syntax for this.

wait on <signal_name>;

When we use this construct, we simply wait for any event to occur on the signal given by the <signal_name> field.

We can also use this statement to wait on more than one signal to change state. To do this, we have to provide a list of signals separated by a comma in the <signal_name> field.

As an example, the code snippet below waits for a change of state in either the sig_a or sig_b signals.

wait on sig_a, sig_b;

VHDL Testbench Example

Now that we have discussed the most important topics for testbench design using VHDL, let’s consider a complete example.

For this example, we will use a very simple circuit and build a test bench which generates every possible input combination. The circuit shown below is the one we will use for this example.

This consists of a simple four input AND gate and a d type flip flip.

A circuit diagram showing a two input and gate with the output of the and gate being an input to a D type flip flop

1. Create an Empty Entity and Architecture

The first thing we do in the testbench is declare the entity and architecture. As we saw with the first example, the entity declaration for a testbench is left empty.

The code snippet below shows the declaration of the entity and architecture for this testbench.

entity example_tb is
end entity example_tb;

architecture test of example_tb is

...

end architecture example_tb;

2. Instantiate the DUT

Now that we have a blank test bench to work with, we need to instantiate the design we are going to test.

As component instantiation requires more VHDL code to be written, we use direct entity instantiation for this.

The code snippet below shows the method used for this, assuming that the signal in_1, in_b and out_q are declared previously.

dut: entity work.example_design(rtl)
  port map (
    a => in_a,
    b => in_b,
    q => out_q
  );

3. Generate Clock and Reset

The next thing we do when writing a VHDL testbench is generate a clock and a reset signal. We use the after statement to generate the signal concurrently in both instances.

We generate the clock by scheduling an inversion every 1 ns, giving a clock frequency of 1GHz. This frequency is chosen purely to give a fast simulation time. In reality, 1GHz clock rates in FPGAs are not achievable and the test bench clock frequency should match the frequency of the hardware clock .

The VHDL code snippet below shows how the clock and the reset signals are generated in our testbench.

-- Reset and clock
clock <= not clock after 1 ns;
reset <= '1', '0' after 5ns;

4. Write the Stimulus

The final part of the testbench which we will write is the test stimulus.

In order to test the circuit we need to generate each of the four possible input combinations in turn. We can use a process to generate this stimulus.

To do this we assign the inputs a value and then use a wait statement to allow for propagation through the FPGA.

The code snippet below shows the code for this.

stimulus:
process begin
  -- Wait for the Reset to be released before 
  wait until reset = '0';

  -- Generate each of in turn, waiting 2 clock periods between
  -- each iteration to allow for propagation times
  and_in <= "00";
  wait for 2ns;
  and_in <= "01";
  wait for 2ns;
  and_in <= "10";
  wait for 2ns;
  and_in <= "11";

  -- Testing complete
  wait;
end process stimulus;

Full VHDL Testbench Example Code

The VHDL code below shows the testbench example in its entirety.

In this example we use the alias keyword, which can improve the readability of our code by explicitly naming slices of a VHDL array type. More information about using the alias keyword in VHDL can be found here.

entity example_tb is
end entity example_tb;

architecture test of example_tb is

  signal clock  : std_logic := '0';
  signal reset  : std_logic := '1';

  signal and_in : std_logic_vector(1 down 0) := (others => '0');
  alias in_a is and_in(0);
  alias in_b is and_in(1);
  signal out_q  : std_logic;

begin

  -- Reset and clock
  clock <= not clock after 1 ns;
  reset <= '1', '0' after 5 ns;

  -- Instantiate the design under test
  dut: entity work.example_design(rtl)
    port map (
      a => in_a,
      b => in_b,
      q => out_q
    );

  -- Generate the test stimulus
  stimulus:
  process begin
    -- Wait for the Reset to be released before 
    wait until (reset = '0');

    -- Generate each of in turn, waiting 2 clock periods between
    -- each iteration to allow for propagation times
    and_in <= "00";
    wait for 2 ns;
    and_in <= "01";
    wait for 2 ns;
    and_in <= "10";
    wait for 2 ns;
    and_in <= "11";

    -- Testing complete
    wait;
  end process stimulus;

end architecture example_tb;

Exercises

When using a basic testbench architecture which block generates inputs to the DUT?

The stimulus block is used to generate inputs to the DUT.

Write an empty entity architecture pair for an empty VHDL testbench.

entity testbench_example is
end entity testbench_example;
 
architecture test of testbench_example is
begin
end architecture testbench_example;

Why is direct entity instantiation preferable to component instantiation?

We can write less code as there is no need to declare a component.

Which time consuming construct can be used outside of a process block?

Only the after statement can be used outside of a process.

What must we omit from a process declaration if we want to use a time consuimng construct inside of it?

We must omit the sensitivity list from the declaration.

Write a process which generates stimulus for a 3 input AND gate. There should be a delay of 10 ns between changing the inputs.

stimulus:
process is
begin
  and_in <= "000";
  wait for 10 ns; 
  and_in <= "001";
  wait for 10 ns;
  and_in <= "010";
  wait for 10 ns;
  and_in <= "011";
  wait for 10 ns;
  and_in <= "100";
  wait for 10 ns; 
  and_in <= "101";
  wait for 10 ns;
  and_in <= "110";
  wait for 10 ns;
  and_in <= "111";
  -- Testing complete
  wait;
end process stimulus;