Tutorial to create Testbench in Verilog
Created by
Madhava Vemuri, Ph.D.
Madhava Vemuri, Ph.D.
There are different types of functional verification, such as simulation-based, assertion-based, and emulation-based. In this tutorial, we are looking at simple simulation-based verification techniques and utilizing stimulus-driven techniques to check the output of a design under test (DUT).
A design under test (DUT) is a digital circuit that is currently being verified for functional correctness. For this tutorial, the top-level Verilog file is named "top.v" which implements a gate-level implementation. This file encompasses all the ports and functionality of the DUT. The "std_cells.v" contains the implementation of the standard cells used in the creation of the
Here are the contents of the "top.v" Verilog file.
`include "std_cells.v" // compiler directive to include the std_cells verilog file into the path
module top(out1, in1, in2, in3, in4);
// top file is implementing 4 bit AND gate
// using 2 2-bit AND gates
//--------------------------------------------------
// Port information
output out1;
input in1, in2, in3, in4;
//--------------------------------------------------
// Wiring and register information
wire net1, net2;
//-----------------------------------------------
// Logic Implementation
and2 and2_x1(net1, in1, in2);
and2 and2_x2(net2, in3, in4);
and2 and2_x3(out1, net1, net2);
endmodule
Here are the contents of the "std_cells.v" Verilog file.
module and2(out1, in1, in2);
// 2 1-inputs and 1-bit output
// in1, in2 : 1-bit input
// out1 : 1-bit output
//----------------------------------------------
// Port information
output out1;
input in1, in2;
//-----------------------------------------------
// Logic Implementation
assign out1= in1 && in2;
//----------------------------------------------
endmodule // Signifies the end of the module
//////////////////////////////////////////////////
module or2(out1, in1, in2);
// 2 1-inputs and 1-bit output
// in1, in2 : 1-bit input
// out1 : 1-bit output
//----------------------------------------------
// Port information
output out1;
input in1, in2;
//-----------------------------------------------
// Logic Implementation
assign out1= in1 || in2;
//----------------------------------------------
endmodule // Signifies the end of the module
///////////////////////////////////////////////////
module inv1(out1, in1);
// 2 1-inputs and 1-bit output
// in1 : 1-bit input
// out1 : 1-bit output
//----------------------------------------------
// Port information
output out1;
input in1;
//-----------------------------------------------
// Logic Implementation
assign out1= not in1;
//----------------------------------------------
endmodule // Signifies the end of the module
A testbench is a Verilog file "top_tb.v" that creates stimulus, which is applied on the instantiation of the top file (DUT). Here is an example of a simple testbench module file that instantiates the DUT.
///////////////////////////////////////////////////
// Compiler directives
//-------------------------------------------------
module top_tb();
// Testbench module
//--------------------------------------------------
// Wiring and register information
//------------------------
// Instantiate the device under test (DUT)
endmodule // Signifies the end of the module
To include the modules from other files in Verilog compiler directives are used, which are written after ( ` : backtick). Here, the compiler directive " `include " specifies the location of the Verilog file to be included in the current testbench.
The top file is included on the first line in the testbench under the compiler directives
`include "top.v" // Location of the top file
The time unit is set using the " `timescale " directive. You can decide the time scale unit, and precision using the directive. "x" and "y" specify the scale of the magnitude, and can be only 1, 10, or 100. The time unit is specified by the magnitude, which can be one of the following: s, ms, us, ns, ps, or fs, denoting seconds, milliseconds, microseconds, nanoseconds, picoseconds, or femtoseconds, respectively.
`timescale x {magnitude}s/ y {magnitude}s
To apply the stimulus onto the DUT, nets are created using the datatypes reg and wire. Here are "in1" and "out1", which are 2 nets created of type reg and wire, respectively:
The "in1" is created as a reg datatype, since we apply stimulus over time.
The "out1" is created using the wire datatype, since it is used to observe the outputs of the DUT
//--------------------------------------------------
// Wiring and register information
reg [3:0]in1=0; // Here in1 is created a vector
wire out1; // Here out1 is created a scalar
To access one of the wires inside the vector, we use indexing to refer to it. There are n+1 wires in the implementation of the "[n:0]net" vector.
To access the ith wire in the vector we use the following format "net[i]".
A group of wires can also be accessed using the ':' in the vector. To access the vector i to k "net[i:k]"
The DUT is instantiated similarly to the gate-level modelling instantiation. The unit test instance created here is "U1" inside the testbench. Each port is connected to the wires explicitly using the ".port_name()", where port_name is one of the ports of the top file.
//------------------------
// Instantiate the device under test (DUT)
top U1(.out1(out1),.in1(in1[0]),.in2(in1[1]),.in3(in1[2]),.in4(in1[3]));
To make the stimulus, assignment operators such as "=" or "<=" are used within the initial block. You can use an initial block or an always block to apply the stimulus. Any value after # denotes the delay associated with that assignment. Here, each assignment is assigned after 10 time units, since we are using the blocking assignment operator "=". In the case of the non-blocking assignment, all the assignments are run concurrently.
initial begin
#0 in1=0;
#10 in1=1;
#10 in1=2;
#10 in1=3;
#10 in1=4;
#10 in1=5;
end
Here is the case for the always block, where you can avoid assigning at each instant
always begin
#10 in1=in1+1;
end
Here are the contents of the final test bench file. The $stop system task is defined within the initial block, which stops the simulation after #100 time units. $display system task written inside the always blocks continually displays the contents inside it at regular intervals.
///////////////////////////////////////////////////
// Compiler directives
//-------------------------------------------------
`include "top.v"
`timescale 1ns/1ns
//--------------------------------------------------
module top_tb();
//--------------------------------------------------
// Wiring and register information
reg [3:0]in1=0;
wire out1;
//--------------------------------------------------
// Instantiate the device under test (DUT)
initial begin
#200 $stop;
end
top U1(.out1(out1),.in1(in1[0]),.in2(in1[1]),.in3(in1[2]),.in4(in1[3]));
//--------------------------------------------------
always begin
#10 in1 <= in1 + 1'b1;
$display($time, " out1=%b,in1=%b,in2=%b,in3=%b,in4=%b",out1,in1[0],in1[1],in1[2],in1[3]);
end
//--------------------------------------------------
endmodule
Here is the output of the Modelsim transcript. You can verify that the output is 1 when all the inputs are 1, and in all other cases, the output is 0 for the 4-bit AND gate.