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.
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.
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.
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.
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.
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
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:
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.
There are two sequential blocks in Verilog, initial and always . It is in these blocks that we apply the stimulus.
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.
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.
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.
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:
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; aWhat 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.
$display is coming under the active event. Accordingly, the simulated output will be:
Value of a is :0Timescale 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 t ime_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.
`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 valueThe 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 simulationClocks 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
- 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 = i endSince 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; endDon’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); endSo 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 endmoduleSince 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 =1Testbench 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 qLet’s start writing a testbench for the above :
As usual start with the module declaration. Naming the module as dff_tb
module dff_tbMoving 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 endCommand 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); endFinally, 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 endmoduleSimulation 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 =0We 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.
About the author
Aiysha is a 2019 BTech graduate in the field of Electronics and Communication from the College of Engineering, Perumon. Her fascination with digital circuit modeling encouraged her to pursue a PG diploma in VLSI and Embedded Hardware Design from NIELIT, Calicut. And this is where she was initiated into the world of Hardware Description and Verilog. She spends her downtime perfecting either her dance moves or her martial arts skills.
Related courses to How to write a testbench in Verilog?
CMOS - IC Design Course
A free course as part of our VLSI track that teaches everything CMOS. Right from the physics of CMOS to designing of logic circuits using the CMOS inverter.
VHDL Course
A free and complete VHDL course for students. Learn everything from scratch including syntax, different modeling styles with examples of basic circuits.
Digital Electronics Course
A free course on digital electronics and digital logic design for engineers. Everything is taught from the basics in an easy to understand manner.