We need to have an environment known as a testbench to run any kind of simulation on the design.
Click here to refresh basic concepts of a simulation
What is the purpose of a testbench ?
A testbench allows us to verify the functionality of a design through simulations. It is a container where the design is placed and driven with different input stimulus.
- Generate different types of input stimulus
- Drive the design inputs with the generated stimulus
- Allow the design to process input and provide an output
- Check the output with expected behavior to find functional defects
- If a functional bug is found, then change the design to fix the bug
- Perform the above steps until there are no more functional defects
Components of a testbench
The example shown in Introduction is not modular, scalable, flexible or even re-usable because of the way DUT is connected, and how signals are driven. Let's take a look at a simple testbench and try to understand about the various components that facilitate data transfer from and to the DUT.
|Generator||Generates different input stimulus to be driven to DUT|
|Interface||Contains design signals that can be driven or monitored|
|Driver||Drives the generated stimulus to the design|
|Monitor||Monitor the design input-output ports to capture design activity|
|Scoreboard||Checks output from the design with expected behavior|
|Environment||Contains all the verification components mentioned above|
|Test||Contains the environment that can be tweaked with different configuration settings|
What is DUT ?
DUT stands for Design Under Test and is the hardware design written in Verilog or VHDL. DUT is a term typically used in post validation of the silicon once the chip is fabricated. In pre validation, it is also called as Design Under Verification, DUV in short.
// All verification components are placed in this top testbench module module tb_top; // Declare variables that need to be connected to the design instance // These variables are assigned some values that in turn gets transferred to // the design as inputs because they are connected with the ports in the design reg clk; wire en; wire wr; wire data; // Instantiate the design module and connect the variables declared above // with the ports in the design design myDsn ( .clk (clk), .en (en), .wr (wr), . ... .rdata); // Develop rest of the testbench and write stimulus that can be driven to the design endmodule
What is an interface ?
If the design contained hundreds of port signals it would be cumbersome to connect, maintain and re-use those signals. Instead, we can place all the design input-output ports into a container which becomes an interface to the DUT. The design can then be driven with values through this interface.
What is a driver ?
The driver is the verification component that does the pin-wiggling of the DUT, through a task defined in the interface. When the driver has to drive some input values to the design, it simply has to call this pre-defined
task in the interface, without actually knowing the timing relation between these signals. The timing information is defined within the
task provided by the interface. This is the level of abstraction required to make testbenches more flexible and scalable. In the future, if the interface changed, then the new driver can call the same task and drive signals in a different way.
How does the driver know what to drive ?
The generator is a verification component that can create valid data transactions and send them to the driver. The driver can then simply drive the data provided to it by the generator through the interface. Data transactions are implemented as class objects shown by the blue squares in the image above. It is the job of the driver to get the data object and translate it into something the DUT can understand.
Why is a monitor required?
Until now, how data is driven to the DUT was discussed. But that's only half way through, because our primary aim is to verify the design. The DUT processes input data and sends the result to the output pins. The monitor picks up the processed data, converts it into a data object and sends it to the scoreboard.
What is the purpose of a scoreboard?The Scoreboard can have a reference model that behaves the same way as the DUT. This model reflects the expected behavior of the DUT. Input sent to the DUT is also sent to this reference model. So if the DUT has a functional problem, then the output from the DUT will not match the output from our reference model. So comparison of outputs from the design and the reference model will tell us if there is a functional defect in the design. This is usually done in the scoreboard.
Why is an environment required ?
It makes the verification more flexible and scalable because more components can be plugged into the same environment for a future project.
What does the test do ?
Exactly. The test will instantiate an object of the environment and configure it the way the test wants to. Remember that we will most probably have thousands of tests and it is not feasbile to make direct changes to the environment for each test. Instead we want certain knobs/parameters in the environment that can be tweaked for each test. That way, the test will have a higher control over stimulus generation and will be more effective.
Here, we have talked about how a simple testbench looks like. In real projects, there'll be many such components plugged in to do various tasks at higher levels of abstraction. If we had to verify a simple digital counter with maximum 50 lines of RTL code, yea, this would suffice. But, when complexity increases, there will be a need to deal with more abstraction.
What are abstraction levels ?
In the Preface, you saw that we toggled the design using individual signals.
#5 resetn <= 0; #20 resetn <= 1;Instead, if you put these two signals in a task and call it "apply_reset" task, you have just created a component that can be re-used and hides the details of what signals and what time intervals it is being asserted. This is a feature we would like to have when developing the testbench - to hide away details - so that the test writer need not bother about the how and instead focus on when and why these tasks should be put to use. A test writer finally uses tasks, configures environment and writes code to test the design.
module tb_top; bit resetn; task apply_reset (); #5 resetn <= 0; #20 resetn <= 1; endtask initial begin apply_reset(); end endmodule