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.
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.
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;
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”.
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.
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.
Unit | Value |
---|---|
fs | |
ps | 1000 fs |
ns | 1000 ps |
us | 1000 ns |
ms | 1000 us |
sec | 1000 ms |
min | 60 sec |
hr | 60 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
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;
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.
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;
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;
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;
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.
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;
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
);
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;
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;
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;
I think you forgot to make in_b an alias of and_in(1)
Thanks for pointing that out, I have fixed it now.