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;

Using VHDL Process Blocks to Model Sequential Logic

In this post, we look at the some of the techniques we can use to model sequential logic circuits in VHDL. We mainly look at the process block which we use to write VHDL code which is executed sequentially. We will look at some of the fundamental features of the process block, including sensitivity lists, variables and assignment scheduling.

In the post on VHDL logical operators and signal assignment, we talked about the concept of combinational logic circuits. We also saw how we can use concurrent statements to model the behavior of these circuits in VHDL.

As concurrent statements execute in parallel, they are not suitable for the modelling of sequential logic circuits. Therefore, the VHDL programming language features a construct known as the process block which we can use to model these circuits.

We can also use process blocks to model combinational logic. However, we often have to write more code in comparison to concurrent statement.

As with the previous blogs, there are a number of exercises at the end of this post. However, you should consider reading the blog on basic testbenches in VHDL before tackling these exercises. This will allow you to simulate your solutions and prove that they are working as desired.

VHDL Processes

We use the VHDL process keyword to create blocks of code which are executed sequentially. This is especially important when we describe sequential circuits as we must describe behavior which occurs in a specific sequence.

The code snippet below shows the general syntax for the VHDL process.

<label>:
process (<sensitivity_list>) is
begin
  -- Functional code
end process <label>;

The statements in the process construct execute one after another. This concept is likely to be quite familiar as it is the way in which conventional programming languages such as C or Java work.

However, we need to be careful when using process blocks as there are some features which are unique to VHDL.

These are connected to the fact that we are describing hardware rather than writing software.

It is important that we have a good understanding of process blocks in order to be an effective VHDL designer.

VHDL Sensitivity List

When we write a process block in VHDL, each line of the code is run in sequence until we get to the end of the block.

If we include a sensitivity list in our process, our VHDL code waits at the end of the block until there is an event on one of the signals in this list.

When a relevant event is detected, the process block resumes executing from the first line of code.

We can see then that the sensitivity list has a big effect on the way our process blocks function in VHDL.

It is also possible to exclude the sensitivity list from our process, in which case the VHDL code inside the process block will run continuously.

This means the program loops back to the start of the block after executing the last line.

However, this is not representative of a circuit, which would remain in a steady state until one of the inputs changes state.

This is the behavior that the sensitivity list attempts to emulate in VHDL.

Simple VHDL Process Example

Let's consider the D type flip flop as an example to show how we use the process block to model sequential circuits in VHDL.

As the output changes state whenever there is a positive clock edge, we must include the clock signal in our sensitivity list. The output (q) is then assigned the value of the input (d) on each rising edge of the clock.

The code snippet below shows the implementation of this component.

d_flip_flop:
process (clock) is
begin
  if rising_edge(clock) then
    q <= d;
  end if;
end process d_flip_flop;

As we have included the clock signal in our sensitivity list, the code above executes every time the clock changes from 1 to 0 or from 0 to 1.

The rest of the time, the code is in a paused state while it waits for a change of state to occur.

However, we only expect the output of the flip flop to change when the clock changes from 0 to 1. To detect when this has occurred, we use the rising_edge macro together with an if statement. By doing this we ensure that the output is only updated on a rising edge.

The VHDL if statement is an example of a sequential statement which we discuss further in a separate post.

We should only ever use the rising edge macro for clock signals as synthesis tools will attempt to utilize clock resources within the FPGA to implement it.

Assignment Scheduling

Although the code in a process is sequentially executed, it is important to understand that signals are not updated in this way.

To demonstrate why this is the case, let’s consider the simple twisted ring counter circuit below.

A simple twisted ring style counter circuit
divider:
process (clock) is
begin
  if rising_edge(clock) then
    q_dff1 <= not q_dff2;
    q_dff2 <= q_dff1;
  end if;
end process divider;

First, let's look at the behavior if the signals did update immediately.

Let's assume that the output of both flip flops is 0 when a clock edge occurs. As a result of line 5 in the code above, the output of DFF1 changes to 1.  We can then see that the next line of code would set the output of DFF2 to 1.

This is clearly not the intended behavior of the circuit we are modelling. Instead we expect DFF2 to remain at 0 and DFF1 to change to 1. 

To overcome this issue, VHDL uses scheduled assignment for signals in a process block. As a result, signal changes don't occur immediately after assignment but are instead scheduled to occur at a future time.

Normally signals in a process block update their value at the end of a simulation cycle, which refers to the time taken for the simulator to execute all of the code for a given time step. However, it is also possible for signals to update their values in a subsequent delta cycle.

To better demonstrate the way scheduled assignment works, let's again consider the simple dual flip flop circuit.

The simulator firstly executes the statement to update DFF1 and schedules the update to the signal value.  

The simulator then runs the second line of code, using the original value of the DFF1 flip flop and schedules the update of DFF2.

As there are only two statements in this design, the simulation cycle is now complete. At this point, all of the scheduled changes are applied and the flip flops have the correct values.

Variables in VHDL

We can also declare variables within our VHDL process blocks.

We use VHDL variables in the same way as we would variables in other programming languages such as C. This means that, unlike VHDL signals, they always update their value immediately after assignment.

We assign variables a value using the := symbol rather than <=.

The code snippet below shows how to declare and assign a variable within a process.

example_process:
process (clk) is
  variable var_ex : std_logic;
begin
  var_ex := '1';
end process;

We often use variables to improve the readability of our VHDL code.

For example, if there is a long boolean equation it can make sense to break the expression down for readability.

We can also use variables to keep the code which models the combinational and sequential circuits together.

Although we mostly use variables to model combinational logic, variables can also be synthesised as flip flops. This occurs if we write code in which we read the variable before we assign a value to it.

Variable vs Signal in VHDL

There are two major differences between variables and signals in VHDL.

Firstly, we can only use a variable within a process block whereas signals can be used in any part of the VHDL design.

Secondly, the value of variables are updated immediately after assignment whereas signals inside process blocks aren't.

We have seen in the previous section how we declare a variable within a VHDL process block.

When we do this, we can only read or write the variable within the process block that it was declared in. As a result of this, we can't access the variable in any of the other processes or concurrent statements in our code.

In contrast, we can read signals in any process or concurrent statement which we write. However, we can still only assign signals at one point in our code. This means we can only write data to a signal within one concurrent statement or process block.

We can actually use a special type of variable, known as a VHDL shared variable, which can be read and written by all the processes in our VHDL code. However, this is a more advanced topic which we discuss in detail in a later post.

As we talked about in the previous section, when we write sequential logic circuits our signals use assignment scheduling to update their values.

This is intended to emulate the behaviour of storage elements such as flip flops.

In contrast to this, variables are assigned immediately after they are assigned a value.

When we write combinatorial logic circuits using concurrent statements, the signal value will update immediately on assignment.

As we can only use variables within processes, there is no equivalent for using variables with concurrent statements.

Combinational Logic in a VHDL Process

Although we most commonly use process blocks to model sequential logic in VHDL, we can also use them to model combinatorial logic.

As this adds boiler plate code to our design, we don't see much code written in this way. Boiler plate code refers to code we must include in a design even though it serves no functional purpose. We have to type more lines of code because of this.

The code snippet below shows how we would model ancombinational AND-OR circuit using the VHDL process block. This is the same circuit which we model using concurrent statements in the post on VHDL logical operators.

combinatorial_example:
process (all) is
begin
  logic_out <= (a and b) or c;
end process combinatorial_example;

We can see that this code is almost identical to the example in the previous. The only difference here is the fact that it's encased within a process statement.

The "all" statement in the sensitivity list performs an important function here.

Our simulator or synthesis tool will determine which signals to include in the sensitivity list when we use this keyword.

The all keyword was introduced in VHDL-2008 and can not be used with earlier standards of VHDL. Before this, we had to manually include the relevant signals in the sensitivity list which often leads to bugs in the code.

As combinational circuits written inside processes are generally more difficult to maintain, we should avoid doing this as far as possible.

However, we can sometimes simplify the modelling of complex combinational logic in VHDL by using a process block.

Exercises

Why do we use process blocks in VHDL to model sequential logic circuits?

They allow us to write code which is executed sequentially

What are sensitivity lists used for in a process block?

They define the list of signals that a process will wait on before resuming the execution of code.

Which operator to we use to assign values to a variable and which operator do we use to assign a value to a signal?

:= for variables and <= for signals 

Write the code for a 4 input NAND gate using a process block

-- Using VHDL-2008 syntax
nand_example:
process (all) is
begin
  nand_out <= a nand b nand c nand d;
end process nand_example;
 
-- Using non VHDL-2008 syntax
nand_example:
process (a, b, c, d) is
begin
  nand_out <= a nand b nand c nand d;
end process nand_example;

Write the code for the circuit shown below.

Circuit diagram showing 3 d type flip flops in a chain. The input to the first flip is an or gate whose inputs are the outputs of the other two flip flops.
ff_example:
process (clock) is
begin
  if rising_edge(clock) then
    q_dff1 <= q_dff2 or q_dff3
    q_dff2 <= q_dff1;
    q_dff3 <= q_dff2;
  end if;
end process ff_example;

Sign up free for exclusive content.

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close
The fpgatutorial.com site logo

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close