You are on page 1of 45

Lecture 18

Complete SV TestBench
with Hierarchy
Memory Read

1. Explain transaction, scoreboard, mail box, generator, driver, monitor,


environment, test and test bench

2. How do you develop the complete SV testbench for a given design?

 Look at the main inputs and outputs


 Define transaction and interface
 Design generator and driver
 Design monitor and scoreboard
 Integrate them in environment using mail boxes
 Instantiate the environment from program test
 Instantiate and Connect module DUT and program test using an interface in
testbench top
What do see?
Interfaces
 An interface contains wires and can contain synthesizable methods to perform operations
such as sending and receiving transactions. An interface is RTL, just like your design.

 Real hardware is static. If your chip is fabricated with 3M gates, it can’t suddenly have 4M
gates just because you need a little boost. In simulation, the RTL design is specified at
compile time and can’t grow or shrink. Likewise, your simulation has a fixed number of
interfaces, specified at compile time, and can’t change at run time.

 SystemVerilog classes and objects are dynamic. If you need to send 1000 transactions, just
construct 1000 objects. Need 2000? Just construct more. If your UVM testbench decides
it needs 2 drivers instead of the default of 1, it just creates another driver (or agent) at
runtime.

 Since objects are dynamic, they can’t contain interfaces. But an object can contain pointers.
For example, an agent has a pointer (handle/class variable) to the objects for the driver,
monitor, etc. How can you make a pointer to the RTL interface?

 A virtual interface is a just pointer to an interface.


module test(colors_ifc c_ifc);
Driver d;
initial begin
d = new(c_ifc); // Pass interface
d.send(42);
end
Endmodule

class Driver;
virtual colors_ifc v_ifc; // Pointer to RTL instance

function new(input virtual colors_ifc c_ifc);


v_ifc = c_ifc;
The driver class saves the pointer
endfunction in a virtual interface, so it can use
it later to drive signals
task send(input logic [31:0] data);
v_ifc.r = data[0];

endtask
endclass
SystemVerilog TestBench Example — Memory

Courtesy: www.verificationguide.com
Verification Process

The steps involved in the verification process are,

 Creation of Verification plan


 Testbench Architecture
 Writing TestBench

Before writing/creating the verification plan, need to know about design, so will go through
the design specification.

* In this example Design/DUT is Memory Model.


I/O Specification
Design Specification

Operations:

Write Operation:

address, wr_en, and wdata should be driven at the same clock cycle.

Read Operation:

address and rd_en should be driven on the same clock cycle, Design will respond with the data in the
next clock cycle.

Design Features,

The Memory model is capable of storing 8bits of data per address location
Reset values of each address memory location is ‘hFF
Memory DUT
module memory
#(
parameter ADDR_WIDTH = 2, //Memory
parameter DATA_WIDTH = 8 reg [DATA_WIDTH-1:0] mem [2**ADDR_WIDTH];
)
( //Reset
input clk, always @(posedge reset)
input reset, for(int i=0;i<2**ADDR_WIDTH;i++) mem[i]=8'hFF;

//control signals // Write data to Memory


input [ADDR_WIDTH-1:0] addr, always @(posedge clk)
input wr_en, if (wr_en) mem[addr] <= wdata;
input rd_en,
// Read data from memory
//data signals always @(posedge clk)
input [DATA_WIDTH-1:0] wdata, if (rd_en) rdata <= mem[addr];
output reg [DATA_WIDTH-1:0] rdata
); endmodule
Verification Plan
1. Write and Read to a particular memory location:

Perform write to any memory location, read from the same memory location, read data should be the same
as written data

2. Write and Read to all memory locations:

Perform write and read to all the memory locations (as the address is 2bit width the possible address are
2‘b00, 2’b01, 2’b10, and 2’b11)

3. Default memory value check:

Check default memory values. (before writing any locations, do read operation we should get default values
as ‘hFF)

4. Reset in Middle of Write/Read Operation:

Assert reset in between write/read operation and check for default values. (after writing to few locations
assert the reset and perform read operation, we should get default memory location value ‘hFF)
TestBench Without Monitor, Agent, and Scoreboard
Transaction Class
Fields required to generate the stimulus are declared in the transaction class. Transaction class can
also be used as a placeholder for the activity monitored by the monitor on DUT signals. So, the first
step is to declare the Fields‘ in the transaction class

class transaction;

//declaring the transaction items


rand bit [1:0] addr;
rand bit wr_en;
rand bit rd_en;
rand bit [7:0] wdata;
bit [7:0] rdata;
bit [1:0] cnt;

//constaint, to generate any one among write and read


constraint wr_rd_c { wr_en != rd_en; };

endclass
Generator Class
Generator class is responsible for,

 Generating the stimulus by randomizing the transaction class


 Sending the randomized class to driver

class generator;

//declaring transaction class


transaction trans;

//main task, generates(create and randomizes) the packets and puts into mailbox
task main();
trans = new();
if( !trans.randomize() ) $fatal("Gen:: trans randomization failed");
gen2driv.put(trans);
endtask

endclass
Generator Class
class generator;

//declaring transaction class task main();


transaction trans;
repeat(repeat_count) begin
//declaring mailbox trans = new();
mailbox gen2driv; if( !trans.randomize() )
$fatal("Gen:: trans randomization failed");
//Specify number of items to generate gen2driv.put(trans);
int repeat_count; end
-> ended;
//event endtask
event ended; endclass

//constructor
function new(mailbox gen2driv,event ended);
//getting the mailbox handle from env
this.gen2driv = gen2driv;
this.ended = ended;
endfunction
Interface
Will group the signals, specifies the direction (Modport)

Synchronize the signals(Clocking Block).

//driver clocking block //monitor clocking block

clocking driver_cb @(posedge clk); clocking monitor_cb @(posedge clk);


default input #1 output #1; default input #1 output #1;
output addr; input addr;
output wr_en; input wr_en;
output rd_en; input rd_en;
output wdata; input wdata;
input rdata; input rdata;

endclocking endclocking
Complete Interface

interface mem_intf(input logic clk,reset);


//monitor clocking block
//declaring the signals clocking monitor_cb @(posedge clk);
logic [1:0] addr; default input #1 output #1;
logic wr_en; input addr;
logic rd_en; input wr_en;
logic [7:0] wdata; input rd_en;
logic [7:0] rdata; input wdata;
input rdata;
//driver clocking block endclocking
clocking driver_cb @(posedge clk);
default input #1 output #1; //driver modport
output addr; modport DRIVER (clocking driver_cb,input clk,reset);
output wr_en;
output rd_en; //monitor modport
output wdata; modport MONITOR (clocking monitor_cb,input clk,reset);
input rdata;
endclocking endinterface
Driver Class
Generator class is responsible for,

 Receiving the stimulus generated from the generator


 Driving to DUT by assigning transaction class values to interface signals.

//creating virtual interface handle


virtual mem_intf mem_vif;

//creating mailbox handle


mailbox gen2driv;

//constructor
function new(virtual mem_intf mem_vif,mailbox gen2driv);
//getting the interface
this.mem_vif = mem_vif;
//getting the mailbox handle from environment
this.gen2driv = gen2driv;
endfunction
Reset Task
For simplicity, define is used to access interface signals.

`define DRIV_IF mem_vif.DRIVER.driver_cb

//Reset task, Reset the Interface signals to default/initial values


task reset;
wait(mem_vif.reset);
$display("--------- [DRIVER] Reset Started ---------");
`DRIV_IF.wr_en <= 0;
`DRIV_IF.rd_en <= 0;
`DRIV_IF.addr <= 0;
`DRIV_IF.wdata <= 0;
wait(!mem_vif.reset);
$display("--------- [DRIVER] Reset Ended ---------");
endtask
Drive Task
//drive the transaction items to interface signals
task drive;
forever begin
transaction trans;
`DRIV_IF.wr_en <= 0;
`DRIV_IF.rd_en <= 0;

gen2driv.get(trans);
$display("--------- [DRIVER-TRANSFER: %0d] ---------",no_transactions);

@(posedge mem_vif.DRIVER.clk);
`DRIV_IF.addr <= trans.addr;

if(trans.wr_en) begin
`DRIV_IF.wr_en <= trans.wr_en;
`DRIV_IF.wdata <= trans.wdata;
$display("\tADDR = %0h \tWDATA = %0h",trans.addr,trans.wdata);
@(posedge mem_vif.DRIVER.clk);
end
Drive Task (contd..)
if(trans.rd_en) begin
`DRIV_IF.rd_en <= trans.rd_en;
@(posedge mem_vif.DRIVER.clk);
`DRIV_IF.rd_en <= 0;
@(posedge mem_vif.DRIVER.clk);
trans.rdata = `DRIV_IF.rdata;
$display("\tADDR = %0h \tRDATA = %0h",trans.addr,`DRIV_IF.rdata);
end
$display("-----------------------------------------");
no_transactions++;
end
endtask

//used to count the number of transactions


int no_transactions;
Adding a local variable to track the
number of packets driven, and increment
//drive the transaction items to interface signals
the variable in drive task (This will be
task drive;
useful to end the test-case/Simulation. i.e
------
compare the generated pkt’s and driven
------
pkt’s, if both are same then end the
no_transactions++;
simulation)
endtask
Complete Driver Code

class driver;

//used to count the number of transactions task reset;


int no_transactions; ---
---
//creating virtual interface handle endtask
virtual mem_intf mem_vif;
task drive;
//creating mailbox handle
------
mailbox gen2driv;
------
no_transactions++;
//constructor
endtask
function new(virtual mem_intf mem_vif,mailbox gen2driv);
//getting the interface
this.mem_vif = mem_vif; endclass
//getting the mailbox handle from environment
this.gen2driv = gen2driv;
endfunction
TestBench Without Monitor, Agent, and Scoreboard
Environment
Environment class is responsible for,

 Container class contains Mailbox, Generator and Driver.


 Creates the mailbox, generator and driver shares the mailbox handle across the Generator and Driver.

//generator and driver instance


generator gen;
driver driv;

//mailbox handle's
mailbox gen2driv;

//event for synchronization between generator and test


event gen_ended;

//virtual interface
virtual mem_intf mem_vif;
Environment Constructor
Create Mailbox, Generator, Driver and pass the interface handle through the new() method.

//constructor
function new(virtual mem_intf mem_vif);
//get the interface from test
this.mem_vif = mem_vif;

//creating the mailbox (Same handle will be shared across generator and driver)
gen2driv = new();

//creating generator and driver


gen = new(gen2driv,gen_ended);
driv = new(mem_vif,gen2driv);
endfunction
Tasks in the Environment
Generator and Driver activity can be divided and controlled in three methods.

pre_test() – Method to call Initialization. i.e, reset method.


test() – Method to call Stimulus Generation and Stimulus Driving.
post_test() – Method to wait the completion of generation and driving.method.
Add a run task to call the above methods,call $finish after post_test() to end the simulation.

task post_test();
task pre_test(); wait(gen_ended.triggered);
driv.reset(); wait(gen.repeat_count == driv.no_transactions);
endtask endtask

task test();
fork task run;
gen.main(); pre_test();
driv.main(); test();
join_any post_test();
endtask $finish;
endtask
`include "transaction.sv"
Complete Environment Code
`include "generator.sv"
`include "driver.sv"
class environment; //creating the mailbox
//(Same handle will be shared
//generator and driver instance //across generator and driver)
generator gen; gen2driv = new();
driver driv; task post_test();
//creating generator and driver wait(gen_ended.triggered);
//mailbox handle's gen = new(gen2driv,gen_ended); wait(gen.repeat_count ==
mailbox gen2driv; driv = new(mem_vif,gen2driv); driv.no_transactions);
endfunction endtask
//event for synchronization
//between generator and test task pre_test(); //run task
event gen_ended; driv.reset(); task run;
endtask pre_test();
//virtual interface test();
virtual mem_intf mem_vif; task test(); post_test();
fork $finish;
//constructor gen.main(); endtask
function new(virtual mem_intf mem_vif); driv.main();
//get the interface from test join_any endclass
this.mem_vif = mem_vif; endtask
Test
Test is written with program construct and is responsible for,

 Creating the environment.


 Configuring the testbench i.e, setting the type and number of transactions to be generated.
 Initiating the stimulus driving..

`include "environment.sv"
program test(mem_intf intf);

//declaring environment instance


environment env;

initial begin
//creating environment
env = new(intf);

//setting the repeat count of generator as 10, means to generate 10 packets


env.gen.repeat_count = 10;

//calling run of env, it interns calls generator and driver main tasks.
env.run();
end
endprogram
`include "interface.sv"
TestBench Top
`include "random_test.sv"

module tbench_top; //Testcase instance, interface handle is passed to test


test t1(intf);
//clock and reset signal declaration
bit clk; //DUT instance
bit reset; memory DUT (
.clk(intf.clk),
//clock generation .reset(intf.reset),
always #5 clk = ~clk; .addr(intf.addr),
.wr_en(intf.wr_en),
//reset Generation .rd_en(intf.rd_en),
initial begin .wdata(intf.wdata),
reset = 1; .rdata(intf.rdata)
#5 reset =0; );
end
//enabling the wave dump
//creatinng instance of interface, inorder to connect DUT initial begin
and testcase $dumpfile("dump.vcd"); $dumpvars;
mem_intf intf(clk,reset); end
endmodule
`include "environment.sv"
program test(mem_intf intf); //declaring environment instance
environment env;
class my_trans extends transaction; my_trans my_tr;

bit [1:0] count; initial begin


//creating environment
function void pre_randomize(); env = new(intf);
wr_en.rand_mode(0);
rd_en.rand_mode(0); my_tr = new();
addr.rand_mode(0);
//setting the repeat count of generator as 4, means to generate 4 packets
if(cnt %2 == 0) begin env.gen.repeat_count = 10;
wr_en = 1;
rd_en = 0; env.gen.trans = my_tr;
addr = count;
end //calling run of env, it interns calls generator and driver main tasks.
else begin env.run();
wr_en = 0; end
rd_en = 1; endprogram
addr = count;
count++;
end
cnt++;
endfunction

endclass
TestBench With Monitor, and Scoreboard
Monitor
Monitor class is responsible for,

 Samples the interface signals and converts the signal level activity to the transaction level

 Send the sampled transaction to Scoreboard via Mailbox

//creating virtual interface handle


virtual mem_intf mem_vif;

//creating mailbox handle


mailbox mon2scb;

//constructor
function new(virtual intf vif,mailbox mon2scb);
//getting the interface
this.vif = vif;
//getting the mailbox handles from environment
this.mon2scb = mon2scb;
endfunction
Sampling logic

task main;
forever begin
transaction trans;
trans = new();

@(posedge mem_vif.MONITOR.clk);
wait(`MON_IF.rd_en || `MON_IF.wr_en);
trans.addr = `MON_IF.addr;
trans.wr_en = `MON_IF.wr_en;
trans.wdata = `MON_IF.wdata;
if(`MON_IF.rd_en) begin
trans.rd_en = `MON_IF.rd_en;
@(posedge mem_vif.MONITOR.clk);
@(posedge mem_vif.MONITOR.clk);
trans.rdata = `MON_IF.rdata;
end
mon2scb.put(trans);
end
endtask
Complete Monitor Code task main;
forever begin
transaction trans;
trans = new();
`define MON_IF mem_vif.MONITOR.monitor_cb
class monitor;
@(posedge mem_vif.MONITOR.clk);
wait(`MON_IF.rd_en || `MON_IF.wr_en);
//creating virtual interface handle
trans.addr = `MON_IF.addr;
virtual mem_intf mem_vif;
trans.wr_en = `MON_IF.wr_en;
trans.wdata = `MON_IF.wdata;
//creating mailbox handle
if(`MON_IF.rd_en) begin
mailbox mon2scb;
trans.rd_en = `MON_IF.rd_en;
@(posedge mem_vif.MONITOR.clk);
//constructor
@(posedge mem_vif.MONITOR.clk);
function new(virtual mem_intf mem_vif,mailbox
trans.rdata = `MON_IF.rdata;
mon2scb);
end
//getting the interface
mon2scb.put(trans);
this.mem_vif = mem_vif;
end
//getting the mailbox handles from environment
endtask
this.mon2scb = mon2scb;
endfunction
endclass
Scoreboard
Scoreboard receives the sampled packet from monitor

 If the transaction type is “read”, compares the read data with the local memory data

 If the transaction type is “write”, local memory will be written with the wdata

class scoreboard;

//creating mailbox handle


mailbox mon2scb;

//used to count the number of transactions


int no_transactions;

//constructor
function new(mailbox mon2scb);
//getting the mailbox handles from environment
this.mon2scb = mon2scb;
endfunction
Store wdata and compare rdata with stored data,
task main;
transaction trans;
forever begin
mon2scb.get(trans);
if(trans.rd_en) begin
if(mem[trans.addr] != trans.rdata)
$error("[SCB-FAIL] Addr = %0h,\n \t Data :: Expected = %0h Actual =
%0h",trans.addr,mem[trans.addr],trans.rdata);
else
$display("[SCB-PASS] Addr = %0h,\n \t Data :: Expected = %0h Actual =
%0h",trans.addr,mem[trans.addr],trans.rdata);
end
else if(trans.wr_en)
mem[trans.addr] = trans.wdata;

no_transactions++;
end
endtask
task main;
transaction trans;
Complete Scoreboard forever begin
mon2scb.get(trans);
if(trans.rd_en) begin
class scoreboard; if(mem[trans.addr] != trans.rdata)
$error("[SCB-FAIL] Addr = %0h,\n \t Data ::
//creating mailbox handle Expected = %0h Actual =
mailbox mon2scb; %0h",trans.addr,mem[trans.addr],trans.rdata);
else
//used to count the number of transactions $display("[SCB-PASS] Addr = %0h,\n \t Data ::
int no_transactions; Expected = %0h Actual =
%0h",trans.addr,mem[trans.addr],trans.rdata);
//array to use as local memory end
bit [7:0] mem[4]; else if(trans.wr_en)
mem[trans.addr] = trans.wdata;
//constructor
function new(mailbox mon2scb); no_transactions++;
//getting the mailbox handles from end
environment endtask
this.mon2scb = mon2scb;
foreach(mem[i]) mem[i] = 8'hFF; endclass
endfunction
`include "transaction.sv" Complete Environment Code
`include "generator.sv"
`include "driver.sv" //creating the mailbox
class environment; //(Same handle will be shared
//across generator and driver)
//generator and driver instance gen2driv = new();
generator gen; mon2scb = new();
driver driv; task post_test();
//creating generator and driver
monitor mon; wait(gen_ended.triggered);
gen = new(gen2driv,gen_ended);
scoreboard scb; wait(gen.repeat_count ==
driv = new(mem_vif,gen2driv);
driv.no_transactions);
mon = new(mem_vif,mon2scb);
//mailbox handle's wait(gen.repeat_count ==
scb = new(mon2scb);
mailbox gen2driv; scb.no_transactions);
endfunction
mailbox mon2scb; endtask
task pre_test();
//event for synchronization //run task
driv.reset();
//between generator and test task run;
endtask
event gen_ended; pre_test();
test();
task test();
//virtual interface post_test();
fork
virtual mem_intf mem_vif; $finish;
gen.main();
endtask
driv.main();
//constructor mon.main();
function new(virtual mem_intf mem_vif); endclass
scb.main();
//get the interface from test join_any
this.mem_vif = mem_vif; endtask
Complete SV Test bench for Adder Example
DUT

module adder(input clk, reset, input [7:0] in1, in2, output reg [8:0] out);
always@(posedge clk or posedge reset) begin
if(reset) out <= 0;
else out <= in1 + in2;
end
endmodule

Adder design produces the resultant addition of two variables on the positive edge of the clock. A
reset signal is used to clear ‘out’ signal to 0.
Note: Adder can be easily developed with combinational logic. A clock and reset are introduced to
have the flavor of a clock and reset in testbench code.
The generator creates or generates randomized transactions or
The transaction is a packet that is stimuli and passes them to the driver.
driven to the DUT or monitored by class generator;
the monitor as a pin-level activity. int count;
mailbox gen_to_drv;
transaction tr;
class transaction;
rand bit [7:0] ip1, ip2; function new(mailbox gen_to_drv);
bit [8:0] out; this.gen_to_drv = gen_to_drv;
endfunction
constraint ip_c {ip1 < 100; ip2 < 100;}
task run;
endclass repeat(count) begin
tr = new();
tr.randomize();
gen_to_drv.put(tr);
end
endtask
endclass
class driver; class monitor;
virtual add_if vif; virtual add_if vif;
mailbox gen_to_drv; mailbox mon_to_sb;
transaction tr;
function new(mailbox mon_to_sb, virtual add_if vif);
function new(mailbox gen_to_drv, virtual add_if this.vif = vif;
vif); this.mon_to_sb = mon_to_sb;
this.gen_to_drv = gen_to_drv; endfunction
this.vif = vif;
endfunction task run;
forever begin
task run; transaction mon_tr;
forever begin wait(!vif.reset);
// Driver to the DUT @(posedge vif.clk);
@(posedge vif.clk); mon_tr = new();
gen_to_drv.get(tr); mon_tr.ip1 = vif.ip1;
mon_tr.ip2 = vif.ip2;
vif.ip1 <= tr.ip1; @(posedge vif.clk);
vif.ip2 <= tr.ip2; mon_tr.out = vif.out;
@(posedge vif.clk); mon_to_sb.put(mon_tr);
tr.out <= vif.out; end
end endtask
endtask endclass
endclass
class agent; class scoreboard;
driver drv; int compare_cnt;
monitor mon; mailbox mon_to_sb;
generator gen;
function new(mailbox mon_to_sb);
mailbox gen_to_drv, mon_to_sb; this.mon_to_sb = mon_to_sb;
virtual add_if vif; endfunction

function new(virtual add_if vif, mailbox mon_to_sb); task run;


gen_to_drv = new(); forever begin
transaction tr;
drv = new(gen_to_drv, vif); tr = new();
mon = new(mon_to_sb, vif); mon_to_sb.get(tr);
gen = new(gen_to_drv); if(tr.ip1 + tr.ip2 == tr.out) begin
endfunction $display("Matched: ip1 = %0d, ip2 = %0d, out = %0d", tr.ip1,
tr.ip2, tr.out);
task run(); end
fork else begin
drv.run(); $display("NOT matched: ip1 = %0d, ip2 = %0d, out = %0d",
mon.run(); tr.ip1, tr.ip2, tr.out);
gen.run(); end
join_any compare_cnt++;
endtask end
endtask
endclass endclass
The driver interacts with DUT. It receives randomized transactions
from the generator and drives them to the driven as a pin level activity.
class env;
agent agt; The monitor observes pin-level activity on the connected interface at
scoreboard sb; the input and output of the design.

mailbox mon_to_sb;
An agent is a container that holds the generator, driver, and monitor.
function new(virtual add_if vif);
mon_to_sb = new();
agt = new(vif, mon_to_sb); The scoreboard receives the transaction packet from the monitor and
sb = new(mon_to_sb); compares it with the reference model.
endfunction An environment allows a well-mannered hierarchy and container
for agents, scoreboards
task run();
fork The test is at the top of program base_test(add_if vif);
agt.run(); the hierarchy that env env_o;
sb.run(); initiates the environment
join_any component construction initial begin
wait(agt.gen.count == and connection between env_o = new(vif);
sb.compare_cnt); them. env_o.agt.gen.count = 5;
$finish; env_o.run();
endtask end
endclass endprogram
Test Bench Top

The testbench top is a top-level component that includes interface and DUT instances. It connects
design with the testbench.

module tb_top;
bit clk;
bit reset;
always #2 clk = ~clk;

add_if vif(clk, reset);


adder DUT(.clk(vif.clk),.reset(vif.reset),.in1(vif.ip1),.in2(vif.ip2),.out(vif.out));
base_test t1(vif);

initial begin
clk = 0;
reset = 1;
#5;
reset = 0;
end
endmodule

You might also like