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.
Contents
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.
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 always
block 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:
- Active event
- Occur at current simulation time
- Executed in any order
- Inactive event
- processed after active events
- Non Blocking assignment(NBA) update event
- evaluated during the previous simulation time
- updated after the processing of active and inactive events
- Monitor event
- processed after all the above events have been processed
- 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:
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:
- Simulating the source code(DUT)
- 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.
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.