View Course Path

How to write a testbench in Verilog?

Verifying complex digital systems after implementing the hardware is not a wise choice. It is ineffective in terms of time, money, and resources. Hence, it is essential to verify any design before finalizing it. Luckily, in the case of FPGA and Verilog, we can use testbenches for testing Verilog source code.

In this article, we will learn how we can use Verilog to implement a testbench to check for errors or inefficiencies. We’ll first understand all the code elements necessary to implement a testbench in Verilog. Then we will implement these elements in a stepwise fashion to truly understand the method of writing a testbench.

What is the Design Under Test?

A design under test, abbreviated as DUT, is a synthesizable module of the functionality we want to test. In other words, it is the circuit design that we would like to test. We can describe our DUT using one of the three modeling styles in Verilog – Gate-level, Dataflow, or Behavioral.

For example,

module and_gate(c,a,b);

input a,b;
output c;

assign c = a & b;

endmodule

We have described an AND gate using Dataflow modeling. It has two inputs (a,b) and an output (c). We have used continuous assignment to describe the functionality using the logic equation. This AND gate can be our DUT.

Moving on, let’s get to the main question.

What is a Testbench?

A testbench is simply a Verilog module. But it is different from the Verilog code we write for a DUT. Since the DUT’s Verilog code is what we use for planning our hardware, it must be synthesizable. Whereas, a testbench module need not be synthesizable. We just need to simulate it to check the functionality of our DUT.

In HDL synthesis, the HDL code after being checked for syntax or logic errors undergoes the process of being turned into the most optimum circuit design. That is what it means to be synthesizable. Normal HDL code definitely needs to be synthesizable. That’s the whole point of it. However, testbenches, on the other hand, aren’t going to be implemented in any circuit. Their entire purpose is to just check/simulate the HDL code. Hence, they do not need to be synthesizable.

So, to test our DUT, we have to write the testbench code.

What? Why do we have to take the trouble to write another code?

Yes, there are other alternatives. Like we can load our code to the FPGA and then check the hardware pins for each signal. But imagine your project has a large number of signals. And we have to probe all the permutations and combinations of outputs via the external pins. That’s too much work.

With a testbench, we can view all the signals associated with the DUT. No need for physical hardware.

Writing a test bench is a bit trickier than RTL coding. Verifying a system can take up around 60-70% of the design process. In fact, in our post on introduction to VLSI, we mentioned that a Verification Engineer is a separate position that’s pretty common in the semiconductor industry.

But don’t worry. This article will help you to take your first steps in writing testbenches.

How to implement a test bench?

Let’s learn how we can write a testbench. Consider the AND module as the design we want to test. Like any Verilog code, start with the module declaration.

module and_gate_test_bench;

Did you notice something? Yes. We didn’t declare the terminal ports. Why? We will understand as we proceed.

Reg and wire declarations

Usually, we declare the input and output ports. But, in a testbench, we will use two signal types for driving and monitoring signals during the simulation.

The reg datatype will hold the value until a new value is assigned to it. This data type can be assigned a value only in the always or initial block. This is used to apply a stimulus to the inputs of DUT. You can read more about the reg datatype in Verilog here.

The wire datatype is similar to that of a physical connection. It will hold the value that is driven by a port, assign statement, or reg. This data type cannot be used in initial or always blocks. This is used to check the output signals from the DUT.

We can declare these data types for the testbench of the AND module.

//used upper case for signals to avoid confusion
reg A, B;
wire C;//think of this as the output

DUT Instantiation

The purpose of a testbench is to verify whether our DUT module is functioning as we wish. Hence, we have to instantiate our design module to the test module. The format of the instantiation is:

<dut_module> <instance name>(.<dut_signal>(test_module_signal),…)

Here it goes:

and_gate dut(.a(A), .b(B), .c(C));

We have instantiated the DUT module and_gate to the test module. The instance name is your choice. The signals with a dot in front of them are the names for the signals inside the and_gate module, while the wire or reg they connect to in the test bench is next to the signal in parenthesis.

Initial and Always blocks

There are two sequential blocks in Verilog, initial and always. It is in these blocks that we apply the stimulus.

The initial block

The initial block is executed only once. It begins its execution at the start of the simulation at time t = 0. The stimulus is written into the initial block.

Here’s how we write stimulus for and_gate in the initial block:

initial
begin
A = 0; B = 0; // starts execution at t=0
#10 A = 0; B = 1; // execution at t = 10 time units
#10 A = 1; B = 0; // execution at t = 20 time units 
#10 A = 1; B = 1; //execution at t = 30 time units 
end

Starting with the first line between the begin and end, each line executes from top to bottom until a delay is reached. When the delay is reached, the execution of this block waits until the delay time (10-time units) has passed and then picks up execution again.

The always block

Contrary to the initial statement, an always block repeatedly executes, although the execution starts at time t=0. For example, the clock signal is essential for the operation of sequential circuits like flip-flops. It needs to be supplied continuously. Hence, we can write the code for operation of the clock in a testbench as:

module always_block_example;
reg clk;

initial
begin
clk = 0;
end

always
 #10 clk = ~clk;
endmodule

The above statement gets executed after 10 ns starting from t =0. The value of the clk will get inverted after 10 ns from the previous value. Thus generating a clock signal of 20 ns pulse width. Therefore, this statement generates a signal of frequency 50MHz.

Initialization

Let’s see the alwaysblock example again:

module always_block_example;
reg clk;

initial
begin
clk = 0;
end
always
 #10 clk = ~clk; 

Have you noticed the initial block in the above code? Yes, we have initialized the clk value as zero.

Why do we need initialization?

Signals are undefined at the start of the simulation. Depending on whether it is reg or wire, the value of signals will be x or z, respectively. For the above code, if we do not do the initialization part, the clk signal will be x from t=0. After 10ns, it will be inverted to another x.

Event Queue

The event queue is a sort of a to-do list for the simulator. It is a conceptual model that helps us understand how various events function. The events associated with this model are:

  1. Active event
    • Occur at current simulation time
    • Executed in any order
  2. Inactive event
    • processed after active events
  3. Non Blocking assignment(NBA) update event
    • evaluated during the previous simulation time
    • updated after the processing of active and inactive events
  4. Monitor event
    • processed after all the above events have been processed
  5. Future events
    •      Events to occur at future simulation time.

We have discussed the different segments of the event queue. Let’s see what comes under each event queue:

Event Queue in Verilog
Event Queue in Verilog

Let’s see an example to understand better

Refer to the below code:

initial
begin

a = 0;
a <= 1;

$display("\nValue of a is :%b", a);
end

What is your answer? Did you think that the value of a is displayed as 1?

Then, you are wrong. Wondering why?

Let’s refer the code with the event queue diagram

The first statement is a = 0 which is an active event since it is a blocking assignment.

The second statement a <= 1 is nonblocking, hence during the active event, only RHS evaluation is scheduled. There is no change in the value of a.

$display is coming under the active event. Accordingly, the simulated output will be:

Value of a is :0

Timescale and Delay

Delays are specified using #. For example

#20 A=1;B=1;

The statement is executed after 20-time units from the time the previous statement was executed.

Delay unit is specified using timescale, declared as `timescale time_unit base/precision base. The time_unit is the amount of time a delay of #1 represents. The precision base represents how many decimal points of precision to use relative to the time units.

For example :

 `timescale 1 ns / 1 ps 

Here, time values will be read as ns and rounded to the nearest 1 ps.

To understand better, let’s see a Verilog example:

`timescale 1 ns / 1 ps 

module tb;
reg value;
 
initial 
begin
    value <= 0;
 
    #1     $display ("T=%0t At time #1", $realtime);
    value <= 1;
 
    #0.49   $display ("T=%0t At time #0.49", $realtime);
    value <= 0;
 
    #0.50   $display ("T=%0t At time #0.50", $realtime);
    value <= 1;
 
    #0.51   $display ("T=%0t At time #0.51", $realtime);
    value <= 0;
 
    #5 $display ("T=%0t End of simulation", $realtime);
  end
endmodule

The simulated result of above code is

T=1 At time #1
T=1.49 At time #0.49
T=1.99 At time #0.50
T=2.50 At time #0.51
T=7.50 End of simulation

Clocks and Reset

The clock and reset are essential signals in sequential circuits. We can incorporate the clock and reset signal on our test bench.

The Verilog code below shows how we can incorporate clock and reset signals while writing a testbench for D-flip flop.

module dff_test_bench;
reg clk, reset,d;
wire q,qbar;

//DUT instantiation  
...
initial 
begin
clk = 0; // clock in test bench
end

always
 #10 clk = ~clk; 

initial 
begin

rst = 1; // reset signal as stimulus
#10 rst = 0;
#5 d = 1;
#5 d = 0;

Assign Statements

An assign statement drives a wire with input from another wire or reg. Let’s see how an assign statement can be used in Verilog

reg [15:0] data_bus;
wire [7:0] upper_byte;
assign upper_byte = data_bus[15:8];  

Here, a continuous assignment is made where the value of data_bus[15:8] is constantly driven onto the upper_byte using the assign statement.

Simulation

During simulation, the designer should know the status of the current simulation. Hence, a printout of the simulation result is essential, which will inform the designer. The value of all the signals should be displayed as it helps for debugging purposes. Therefore, while writing testbench, we can use two system tasks to print the simulation results. Let’s discuss those tasks:

$display

This is an important system task available in Verilog. It is used for displaying values of variables or strings or expressions. This inserts a newline by default at the end of the string.  Let’s see how we can use a $display to print signals in a test bench:

$display( "time = %g, A = %b, B = %b, C =%b", $time, A,B,C);

The characters mentioned in the quotes will be printed as it is. The letter along with % denotes the string format. We use %b to represent binary data. We can use %d, %h, %o for representing decimal, hexadecimal, and octal, respectively. The %g is used for expressing real numbers. These will be replaced with the values outside the quote in the order mentioned. For example, the above statement will be displayed in the simulation log as:

time = 20, A = 0, B = 1, C = 0

$time is a system task that will return the current time of the simulation.

$monitor

As the name states, it is clear that it will monitor the data or variable for which it is written, and whenever the variable changes, it will print the changed value.

So, how is $monitor different from $display?

$display mainly prints the data or variable as it is at that instant of that time like the printf in C. We have to mention $display for whatever text we have to view in the simulation log.

Whereas, the $monitor task will monitor the data and print if there is any change in the value it is monitoring. Therefore, we just need to mention this task once.

The $monitor has the same layout as the $display.

Difference between in-built simulation and test bench simulation

We can perform two types of simulation:

  1. Simulating the source code(DUT)
  2. Simulating the test bench

Have you used Vivado and ModelSim in-built waveform simulators? With those tools, we compile and simulate the source code. We view the simulation output in a waveform window. How’s this happening? The source code, when compiled, generates a netlist that contains the connection of gates to the described hardware. The designer manually applies the different combinations of inputs to check whether the desired output is derived. When we have too many input combinations, manual testing is difficult.

In a testbench simulation, the input combinations and DUT are already mentioned in the test bench Verilog file. These inputs act as stimuli on the DUT to produce the output.

test_bench module
test_bench simulation

We can apply all input combinations in a testbench using a loop. We have an option to choose from four loops in Verilog. For example, if we have four inputs a, b, c, d the input combination can be written in a testbench as:

initial
begin
for(integer i =0 i <=16, i++)
{a,b,c,d} = i
end

Since it provides results for all input combinations associated with the code, we will be able to identify the bugs easier. Hence, the testbench is preferred for debugging.


Examples (Stepwise implementation of writing a testbench in Verilog)

We are now familiarized with the elements that we use to write a testbench in Verilog. So, let’s explore how we can write the Verilog testbenches of some basic combinational and sequential circuits.

Testbench for AND Gate 

We have already written the Verilog file for an AND gate at the beginning of the article. Let’s see how to write a test bench for that DUT.

Start with declaring the module as for any Verilog file. We can name the module as and_tb

module and_tb;

Then, let’s have the reg and wire declarations on the way. The input from the DUT is declared as reg and wire for the output of the DUT. It is through these data types we can apply the stimulus to the DUT. Using upper case letters for signals in the testbench avoids confusion.

reg A,B;
wire C;

Then comes the part of performing instantiation.

and_gate dut(.a(A), .b(B), .c(C));

We have linked our test bench to the DUT.

Let’s get to applying the stimulus.

initial
begin
#5 A =0; B=0;
#5 A =0; B=1;
#5 A =1; B=0;
#5 A =1; B=1;
end

Don’t we have to view the results? Hence, we use a $monitor in another initial to see what has happened.

initial
begin
$monitor("simtime = %g, A =%b, B =%b, C =%b", $time,A,B,C);
end

So our final testbench code will be:

module and_tb;
reg A,B;
wire C;
and_gate dut(.a(A), .b(B), .c(C));
initial
begin
#5 A =0; B=0;
#5 A =0; B=1;
#5 A =1; B=0;
#5 A =1; B=1;
end
initial
begin
$monitor("simtime = %g, A =%b, B =%b, C =%b", $time,A,B,C);
end
endmodule

Since multiple initial and always blocks are executed concurrently; we can mention the monitor block before the stimulus block. It doesn’t make much of the difference, though.

So you want to see how it will be displayed when simulated. You have to compile the DUT and then the test_bench for an error-free simulation.

Simulation Log

The simulation log will display the printed results of the above test bench. It will look like this:

simtime = 0, A =x, B =x, C =z
simtime = 5, A =0, B =0, C =0
simtime = 10, A =0, B =1, C =0
simtime = 15, A =1, B =1, C =1

Testbench for D-flip flop

For sequential circuits, the clock and reset signals are essential for its functioning.  Hence, we will see how we can incorporate those signals in a testbench.

Let’s test the Verilog code for D-flip flop. Here’s the DUT:

module dff_behave(clk,rst,d,q,qbar);

input clk,rst,d;
output reg q,qbar;

always@(posedge clk)
begin
if(rst == 1)
begin
q <= 0;
qbar <= 1;
end
else
begin
q <= d;
qbar <= ~d;
end
end

endmodule

Let’s start writing a testbench for the above :

As usual start with the module declaration. Naming the module as dff_tb

module dff_tb

Moving on with the reg and wire declaration:

reg D,CLK,RST;
wire Q, QBAR;

Time for DUT instantiation:

dff_behave dut(.clk(CLK), .rst(RST), .d(D), .q(Q), .qbar(QBAR));

As we said, a clock signal is essential for working of the flip flop. So, here’s how we create a clock stimulus for our testbench.

always
#10 CLK = ~CLK;

The above clock will have a 20 ns pulse width. Therefore, we have generated a 50 MHz clock.

Let’s apply the stimulus for our DUT:

initial
begin
RST = 1;
#10 RST = 0;
#10 D = 0;
#10 D = 1
end

Command to monitor our signal:

initial
begin
monitor( simetime = %g, CLK = %b, RST =%b, D = %b, Q =%b, QBAR =%b", $time, CLK,RST,D,Q,QBAR); 
end

Finally, our testbench code is:

module dff_tb
reg CLK = 0;
reg D,RST;
wire Q,QBAR;

dff_behave dut(.clk(CLK), .rst(RST), .d(D), .q(Q), .qbar(QBAR));

always
#10 CLK = ~CLK;

initial
begin
RST = 1;
#10 RST = 0;
#10 D = 0;
#20 D = 1
end

initial
begin
monitor("simetime = %g, CLK = %b, RST =%b, D = %b, Q =%b, QBAR =%b", $time, CLK,RST,D,Q,QBAR); 
end
endmodule

Simulation Log

So, let’s see how the simulation results look like:

simetime = 0, CLK = x, RST =x, D = x, Q =x, QBAR =x
simetime = 10, CLK = 1, RST =1, D = x, Q =0, QBAR =1
simetime = 20, CLK = 0, RST =0, D = x, Q =0, QBAR =1
simetime = 30, CLK = 1, RST =0, D = 0, Q =0, QBAR =1
simetime = 50, CLK = 1, RST =0, D = 1, Q =1, QBAR =0

We hope learning about writing a testbench in Verilog was one fun ride. You might still have some doubts. That’s okay. Having doubts is a good sign. You are learning. If you get stuck, feel free to ask for help in the comments. We’ll be writing the testbench for every circuit we design in this Verilog course. Rest assured, you’ll get the hang of it.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.