View Course Path

Testbenches in VHDL – A complete guide with steps

Whenever we design a circuit or a system, one step that is most important is “testing”. Testing is necessary to verify whether the designed system works as expected or not.

We can, of course, opt to test an IC after fabrication. But that would be, to put it delicately, stupid. If we find some error in an IC after fabrication, we are looking at a great loss because now we have to re-do the entire chip manufacturing process from scratch. Right from designing the circuit to fabrication.

Hence, the chip design methodology has testing after every phase. You can read about the design flow of manufacturing chips and ASICs here. Testbenches are used to test the RTL (Register-transfer logic) that we implement using HDL languages like Verilog and VHDL.

With testbenches, we essentially test our HDL generated circuits virtually using the same development suite. Akin to how in analog systems, we broadly test for gain, frequency, and phase response of the system, in digital VLSI systems, we mainly focus on timing analysis.

To summarize, we first design the circuit using HDL, then we verify it using a testbench. Only after this can we move on to the next step in the chip design workflow.

A lot of people wonder about the difference between RTL and HDL. RTL stands for Register-transfer logic (Don’t confuse it with the logic family Resistor Transistor Logic).

  • It is a way of defining an electronic circuit using registers and the signals between them. HDL is the language to implement RTL to define an electronic circuit.
  • Basically, you use an HDL to define a circuit. Then you synthesize it and your code gets converted into a list of registers and signals passing through them.
  • A loose analogy that we have repeated in this VLSI track is it’s like if you are with a sketch artist and tell them the features of a robber you saw, what you tell them is the HDL, what they create is the RTL design.

What is a testbench?

The testbench is also an HDL code. We write testbenches to inject input sequences to input ports and read values from output ports of the module. The module (or electronic circuit) we are testing is called a DUT or a Device Under Test. Testing of a DUT is very similar to the physical lab testing of digital chips. There we use sequence generators for input and probe the output to a capturing device. And here we do the same thing virtually.

A testbench mainly has three purposes:

  1. To generate input sequences for DUT.
  2. After generation, to inject/apply those sequences to input ports.
  3. To observe output values and comparing them with known values.

What is a DUT(Device under test)?

The circuit under test is called the Device Under Test or DUT. In some books, authors also refer to them as an entity under test. We write a testbench to inject inputs (stimulus) to the Device Under Test and reads its output. After that, we plot the results, where the waveform of all input and outputs are plotted showing their values at all instants of time.

Talking in terms of an HDL program, the HDL code for every functional block, whether it is an adder, multiplexer, or memory, they are all called DUTs during verification.

How does a testbench work?

A testbench is specific for a DUT. It contains a blank entity and its architecture. Now, you may think “A blank entity,” what does it mean?

It really does not mean anything. It is there because as said earlier, a testbench is also a VHDL program. So it must have an entity. Also, the entity describes the input and output of the circuit that we are testing.

Inside the architecture of testbench, we declare a component which is actually our DUT. We also initialize some signals because we might need to read the values we’ve previously assigned. So we use signals for internal calculations and in the end, assign the signal value to the port.

The next step is to generate a stimulus, or you may say sequences for inputs. We have two ways to generate an in-program stimulus.

In-program stimulus generation

There are two ways to generate stimulus inside the testbench:

  1. Using repetitive patterns
  2. Using vectors

Assuming the entity of the multiplexer as

entity 4x1MUX is
port( Input : in std_logic_vector(3 downto 0);
      SelectLines : in std_logic_vector(1 doownto 0);
      Output : out std_logic_vector(3 downto 0);
end entity;

To test it, we will need to apply sixteen (24) input combinations. Let’s try them using both methods of stimulus generation and compare them.

Using repetitive patterns

In the repetitive pattern method of generation, we dedicate one statement to generate only one bit. For our 4×1 Mux example:

SelectLines(0) <= '0';
SelectLines(1) <= '0';
Input(0) <= '1';
Input(1) <= '1';
Input(2) <= '1';
Input(3) <= '1';

The above lines of code only represent one input combination. We have to repeat this fifteen times more for each set of inputs. That would look fine, but just imagine if we have to write this for a 32×1 MUX. That will make the testbench much longer. There surely has to be a better way right? Yes!

Using vectors

Here, we use vector notation to apply a group of bits to a port (a group of pins). And bit assigned to the respective pin according to their position.

SelectLines <= "00";
Input <= "1111";

Both methods accomplish the same task. One is just a shorter and easier way of doing it.

Types of testbench in VHDL

  1. Simple testbench
  2. Testbench with a process
  3. Infinite testbench
  4. Finite testbench

The ‘simple testbench’ and the ‘testbench with a process’ types are more suitable for combinational circuits. We will be writing one example of each type for the same DUT so that you can compare them and understand them better.

We will be creating a testbench for a full adder. You can find the code of full adder below for your reference.

library IEEE;
use IEEE.std_logic_1164.all;

entity adder_ff is
port(
a,b,cin : IN std_logic;
sum,carry : OUT std_logic);
end adder_ff;

architecture behavioral of adder_ff is
begin

sum <= (a xor b) xor cin;
carry <= (a and b) or (b and cin) or (cin and a);
end behavioral;

Let’s write a simple testbench for the above code.

Simple testbench

As the name suggests, it is the simplest form of a testbench that uses the dataflow modeling style.

We start writing the testbench by including the library and using its necessary packages. It is the same as the DUT.

library IEEE;
use IEEE.std_logic_1164.all;

Now, we define a blank entity as a testbench does not define actual hardware

entity adder_ff_simple_tb is
end entity;

As discussed earlier, testbench is also a VHDL program, so it follows all rules and ethics of VHDL programming.

We declare a component(DUT) and signals in its architecture before begin keyword.

architecture dataflow of adder_ff_simple_tb is
component adder_ff is
port(
	a,b,cin : in std_logic;
	sum,carry : out std_logic);
end component;

signal a,b,cin,sum,carry : std_logic;
begin

Now we have to port map the internal signals and ports of the DUT to enable the testbench to inject input to the DUT and read its output.

We map signals to ports using a keyword port map

uut : adder_ff port map(
	a =>a,
	b =>b,
	cin => cin,
	sum =>sum,
	carry => carry);

 

Then we start injecting input values to signal.

Also, we are using the after clause to create delays. After providing one input value, we give a delay (20 nanoseconds here).

Only after a delay, we give the next set of values. We do this to ensure that we give proper time, for output to become stable and observable.

a <= '0', '1' after 80 ns;
b <= '0', '1' after 40 ns, '0' after 80 ns, '1' after 120 ns;
cin <= '0', '1' after 20 ns, '0' after 40 ns, '1' after 60 ns, '0' after 80 ns, '1' after 100 ns,
       '0' after 120 ns, '1' after 140 ns;
end dataflow;

At last, we end our testbench. You can find the complete testbench code below:

library IEEE;
use IEEE.std_logic_1164.all;

entity adder_ff_simple_tb is
end entity;

architecture dataflow of adder_ff_simple_tb is
component adder_ff is
port(
a,b,cin : in std_logic;
sum,carry : out std_logic);
end component;

signal a,b,cin,sum,carry : std_logic;

begin

uut : adder_ff port map(
a =>a,
b =>b,
cin => cin,
sum =>sum,
carry => carry);

a <= '0', '1' after 80 ns;
b <= '0', '1' after 40 ns, '0' after 80 ns, '1' after 120 ns;
cin <= '0', '1' after 20 ns, '0' after 40 ns, '1' after 60 ns, '0' after 80 ns, '1' after 100 ns,
       '0' after 120 ns, '1' after 140 ns;

end dataflow;
Output of simple testbench
The output of simple testbench. Notice how the output values tally with our input values. This testbench shows that our VHDL code for the full adder was accurate.

Testbench with a process

According to its name, we use the process statement to generate and inject stimulus. As we know, statements inside a process execute sequentially.

The structure of the testbench always remains the same, just the architecture changes according to what method we use.

The component and signal declaration will also be the same. So let’s just focus on the inside architecture.

We start a process (‘stim’ here), and inside it, we inject values to the inputs and wait for some time.

After that delay, we verify the output by comparing them with expected values. We do this using the assert statement.

stim : process 
begin

a <= '0';
b <= '0';
cin <= '0';
wait for 10 ns;
assert ((sum = '0') and (carry = '0'))
report "test failed for input combination 000" severity error;

This pattern can be repeated for every possible input combination, and you’ll have a nice testbench. A testbench that can even check if the outputs are as expected or not, if there’s a mismatch an error is reported.

The complete testbench for our full adder is below.

library IEEE;
use IEEE.std_logic_1164.all;

entity adder_ff_process_tb is
end entity;

architecture behavioral of adder_ff_process_tb is
component adder_ff is
port(
a,b,cin : in std_logic;
sum,carry : out std_logic);
end component;

signal a,b,cin,sum,carry : std_logic;

begin

uut : adder_ff port map(
a =>a,
b =>b,
cin => cin,
sum =>sum,
carry => carry);

stim : process 
begin

a <= '0';
b <= '0';
cin <= '0';
wait for 10 ns;
assert ((sum = '0') and (carry = '0'))
report "test failed for input combination 000" severity error;

a <= '0';
b <= '0';
cin <= '1';
wait for 10 ns;
assert ((sum = '1') and (carry = '0'))
report "test failed for input combination 001" severity error;

a <= '0';
b <= '1';
cin <= '0';
wait for 10 ns;
assert ((sum = '1') and (carry = '0'))
report "test failed for input combination 010" severity error;

a <= '0';
b <= '1';
cin <= '1';
wait for 10 ns;
assert ((sum = '0') and (carry = '1'))
report "test failed for input combination 011" severity error;

a <= '1';
b <= '0';
cin <= '0';
wait for 10 ns;
assert ((sum = '1') and (carry = '0'))
report "test failed for input combination 100" severity error;

a <= '1';
b <= '0';
cin <= '1';
wait for 10 ns;
assert ((sum = '0') and (carry = '1'))
report "test failed for input combination 101" severity error;

a <= '1';
b <= '1';
cin <= '0';
wait for 10 ns;
assert ((sum = '0') and (carry = '1'))
report "test failed for input combination 110" severity error;

a <= '1';
b <= '1';
cin <= '1';
wait for 10 ns;
assert ((sum = '1') and (carry = '1'))
report "test failed for input combination 111" severity error;
wait;

end process;
end behavioral;
Output of testbench with process
The output of testbench with process

What would happen in the case of an error? Let’s try to assert incorrect values to cause an error. This is the message that gets displayed.

Error detected in process testbench
Error detected in process testbench

 

Now let’s look at the other types of testbench that are more suitable for sequential circuits.

  1. Infinite testbench
  2. Finite testbench

For sequential circuits, we will be writing testbenches for a JK flip flop. You can find its code below.

library ieee;
use ieee.std_logic_1164.all;

entity JK_FF is
port( J, K, clk, rst : in std_logic;
Q, Qbar : out std_logic
);
end JK_FF;

architecture behavioral of JK_FF is
begin
    process(clk, rst)
    variable qn : std_logic :='0';
    begin
    if(rst = '1')then
        qn := '0';
    elsif(clk'event and clk='1') then
        if(J='0' and K='0')then
            qn := qn;
        elsif(J='0' and K='1')then
            qn := '0';
        elsif(J='1' and K='0')then
            qn := '1';
        elsif(J='1' and K='1')then
            qn := not qn;
        else
            null;
        end if;
    end if;
  Q <= qn;
  Qbar <= not qn;
end process;
end behavioral;

Infinite testbench

We use infinite testbenches to test sequential circuits, mainly due to the reason that they allow using a clock.

We can, of course, limit the number of clock cycles for simulation purposes. In fact, we will do that in the next testbench.

Additionally, the infinite testbench is suitable for when you have to test something that is unpredictable. Something like a random sequence generator. But for now, let’s stick to JK flip flop because it is easy to understand.

The structure of the testbench remains the same from the previous types. The libraries, the usage of packages, creating a blank entity, and declaring components inside the architecture is all done in the same way. Here’s the difference. After declaring the components, we define a constant for storing delay time and some signals.

constant T : time := 20 ns;
signal J, K, clk, rst, Q, Qbar : std_logic :='0';

Then we do port mapping after the begin keyword

begin
uut: JK_FF port map(
J => J,
K => K,
clk => clk,
rst => rst,
Q => Q,
Qbar => Qbar);

After that, we create a process (‘clock’ here) to generate a clock of period T/2.

clock:process 
begin
    clk <= '0';
    wait for T/2;
    clk <= '1';
    wait for T/2;
end process;

Then we create another process that generates and injects stimulus to DUT.

Stimulus: process
begin
J <= '0';
K <= '0';
rst <= '0';
wait for 20 ns;
. . 
All input combinations.
. .

J <= '1';
K <= '1';
rst <= '0';
wait for 20 ns;
end process;

After that, we can end the architecture and the program. You can see the complete code below.

library ieee; 
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity infinite_tb is
end infinite_tb;


architecture arch of infinite_tb is

component JK_FF is
port(J, K, clk, rst : in std_logic;
Q, Qbar : out std_logic
);
end component;

constant T : time := 20 ns;

signal J, K, clk, rst, Q, Qbar : std_logic :='0';

begin

uut: JK_FF port map(
J => J,
K => K,
clk => clk,
rst => rst,
Q => Q,
Qbar => Qbar);

-- continuous clock
process 
begin
clk <= '0';
wait for T/2;
clk <= '1';
wait for T/2;
end process;

Force: process
begin
J <= '0';
K <= '0';
rst <= '0';
wait for 20 ns;

J <= '0';
K <= '1';
rst <= '0';
wait for 20 ns;

J <= '1';
K <= '0';
rst <= '0';
wait for 20 ns;

J <= '1';
K <= '1';
rst <= '0';
wait for 20 ns;
end process;
end arch;
Output of infinte testbench
Output of infinte testbench.

 

Finite testbench

This injects the stimulus only up to a certain number of times which we specify in the program.

We start by including the IEEE’s library and using their necessary packages.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

Then again, we create a blank entity and declare a component inside the architecture.

entity finite_tb is
end finite_tb;

architecture arch of finite_tb is

component JK_FF is
port(J, K, clk, rst : in std_logic;
Q, Qbar : out std_logic
);
end component;

After that, we define some internal signals for data injection. Two constants, one for time and one to keep a record of no. of clocks that makes this testbench finite.

constant T : time := 20 ns;
signal J, K, clk, rst, Q, Qbar : std_logic;
constant num_of_clocks : integer := 20;
signal i : integer := 0;

Then again we do port mapping, after the begin keyword.

begin
uut: JK_FF port map(
J => J,
K => K,
clk => clk,
rst => rst,
Q => Q,
Qbar => Qbar);

After that, we create a process for the clock. As we know a process without a sensitivity list has to be stopped with a wait keyword. And we also want to make it finite so we use a signal that we have declared earlier to keep a record of the number of clocks cycles. When it reaches a certain predefined number, it will terminate the process.

process 
begin
clk <= '0';
wait for T/2;
clk <= '1';
wait for T/2;

if (i = num_of_clocks) then
file_close(output_buf);
wait;
else
i <= i + 1;
end if;
end process;

Now, we create another process to inject inputs. And, it will also stop when ‘i’ will reach the predefined number of the clock cycles.

Force: process
begin
J <= '0';
K <= '0';
rst <= '0';
wait for 20 ns;

J <= '0';
K <= '1';
rst <= '0';
wait for 20 ns;

J <= '1';
K <= '0';
rst <= '0';
wait for 20 ns;

J <= '1';
K <= '1';
rst <= '0';
wait for 20 ns;

if (i = num_of_clocks) then
wait;
end if;
end process;
Output of simple finite testbench.
The output of simple finite testbench.

That sums up all the normal testbenches in VHDL. The comment box below awaits your queries if you have any.

Leave a Reply

Your email address will not be published. Required fields are marked *