Saturday, May 22, 2021

SystemVerilog Sample Testbench Environment

Let us see how we can create a simple testbench environment using SystemVerilog. This will help to get familiar with the usage of SystemVerilog to create a simple testbench.

Consider the following simple memory module as the DUT for which we will create a SystemVerilog Testbench using generic testbench components.

DUT: memory.v

module memory (
	clk,
	rstn,
	addr,
	wdata,
	wr,
	rdata,
	rvalid
	);

input clk;
input rstn;
input [4:0] addr;
input [31:0] wdata;
input wr;
output reg [31:0] rdata;
output reg rvalid;

reg [31:0] mem [31:0];

always @ (posedge clk or negedge rstn)
begin
  if (!rstn)
  begin
  mem[addr] <= mem[addr];
  rdata <= 32'd0;
  rvalid <= 1'b0; 
  end
  else
  begin
  mem[addr] <= (wr) ? wdata : mem[addr];
  rdata <= (!wr)? mem[addr] : rdata;
  rvalid <= (!wr)? 1'b1 : 1'b0;
  end
end

endmodule

As we know to completely verify the DUT, it must be tested with all combinations of inputs.
Input ports: addr, wdata, wr
Output ports: rdata, rvalid
Our testbench must randomize the inputs and obtain the DUT responses from the outputs.

DUT Functionality: 
  • Active low reset
  • Write operation takes place whenever wr pin is high, otherwise it is a read.
  • Read data (rdata) will be available one cycle after a read operation is performed.
  • Read valid (rvalid) is a pulse that indicates the first cycle of read data. 

For signals to communicate with the DUT, we need to create an interface.

interface.sv:
interface mem_if (input bit clk);

logic rstn;
logic [4:0] addr;
logic [31:0] wdata;
logic wr;
logic [31:0] rdata;
logic rvalid;

endinterface

Create the port list as a class, declaring the variables to be randomized as rand. This will be useful to store data obtained from the virtual interface. We will call this the item class.
I have added a constraint which limits the address values to between 0x1A and 0x1F.

mem_item.sv:
class mem_item;

randc logic [4:0] addr;
rand logic [31:0] wdata;
bit wr;
logic [31:0] rdata;
logic rvalid;

constraint addr_limit {addr inside {[5'h1A:5'h1F]};} //Constraint

endclass


The testbench is organized as follows:

1. Testbench Top

Function: Overall wrapper 
  • Top module of the testbench which instantiates the DUT
  • Create the virtual interface handle and connect the signals accordingly with DUT.
  • Generate clock and reset
  • Create a handle for test class, connect with virtual interface and call its run task.
  • Assertions can be placed here.
  • Functional coverage sampling can also be done.
  • Many $display statements can be present throughout the tb for easier readability and debug.

module tb_top ();

test t0;
reg clk;
always #10 clk = ~clk;

mem_if top_vif (clk);	//Virtual interface

memory dut (	//DUT
  .clk (clk),
  .rstn (top_vif.rstn),
  .addr (top_vif.addr),
  .wdata (top_vif.wdata),
  .wr (top_vif.wr),
  .rdata (top_vif.rdata),
  .rvalid (top_vif.rvalid)
  );

assert property (	//Concurrent Assertion
@(posedge clk) disable iff (!top_vif.rstn)
!top_vif.wr |-> ##1 top_vif.rvalid
);

covergroup FuncCov @(posedge clk); //Functional Coverage
  option.per_instance=1;
  coverpoint top_vif.addr {
    bins feature1 = {[5'h1A:5'h1C]};
    bins feature2 = {[5'h1D:5'h1F]};
  }
  coverpoint top_vif.wdata;
endgroup 

initial begin 	//Clock and reset
clk = 1'b0;
top_vif.rstn = 1'b0;
@(posedge clk);
@(posedge clk);
#10
top_vif.rstn = 1'b1;
end

initial begin	//Test class and functional coverage sample
FuncCov funccov = new();
funccov.sample();
t0 = new;
t0.e0.vif = top_vif;
t0.run();
@(posedge clk);
$display("\n***SIMULATION COMPLETE***");
$display("Total number of scoreboard matches: %d",t0.e0.s0.match);
$display("Total number of scoreboard errors: %d",t0.e0.s0.error);
$finish;
end

endmodule

2. Test
Function: Randomization of data according to requirement
  • Declare environment class.
  • Declare mailbox since randomized data generated in test, is sent across to the driver via mailbox.
  • In the run task of test, call the environment's run task to start the environment. Then apply the required stimulus by calling it as a separate task.
  • In the stimulus task, we randomize the signals in item class according to requirement.
  • Put the randomized data into the mailbox.

class test;

environment e0;
mailbox mbx_drv;
mem_item item;
reg [4:0] temp;

function new();
e0 = new;
item = new;
mbx_drv = new();
endfunction

virtual task run();
e0.d0.mbx_drv = mbx_drv;
$display("T=%0t [Test] Starting stimulus...",$time);
fork
e0.run();
join_none
item.wr = 1'b1;
apply_stim();
repeat (10) begin //Apply stimulus multiple times
@(e0.d0.drv_done) //Event from driver
apply_stim();
end
endtask

virtual task apply_stim();  //Data randomization
$display("\nT=%0t [Test] Applying stimulus...",$time);
temp = item.addr;
item.randomize();
item.wr = ~item.wr;
if (!item.wr) begin item.addr = temp; end
if (!e0.vif.rstn) begin
item.addr = 5'd0;
item.wdata = 32'd0;
item.wr = 1'b0;
$display("Data is reset");
end
mbx_drv.put(item); //Put in mailbox to driver
endtask

endclass

3. Environment
Function: Holds the driver, monitor and scoreboard together. 
  • Declare the components inside the environment, which are driver, monitor and scoreboard.
  • In the run task, connect the virtual interface to the components and call their respective run tasks.
  • Also connect another mailbox to send data from monitor to the scoreboard.

class environment;

driver d0;  //Each env component declared
monitor m0;
scoreboard s0;
mailbox mbx_scb;
virtual mem_if vif;

function new();
d0 = new;
m0 = new;
s0 = new;
mbx_scb = new();
endfunction

virtual task run();
m0.mbx_scb = mbx_scb; //Connect mailboxes
s0.mbx_scb = mbx_scb;
d0.vif = vif;         //Connect virtual interfaces
m0.vif = vif;
s0.vif = vif;
fork
d0.run();  //Run respective run tasks
m0.run();
s0.run();
join_none;
endtask

endclass

4. Driver
Function: Drive input stimulus to the DUT
  • In the run task, we collect data from mailbox given by test class.
  • This data is provided to the virtual interface which is inturn, connected to the DUT.
  • The completion of data driving for that clock cycle is indicated by an event. This event informs the test class that data has been driven and it is ready to drive the next set of data.
  • Forever loop is used because it has to take place until end of simulation time.

class driver;

virtual mem_if vif;
event drv_done;
mailbox mbx_drv;
mem_item item;

task run();

$display ("T=%0t [Driver] starting...", $time);

forever begin
mbx_drv.get(item);     //Get item from mailbox
vif.addr = item.addr;  //Drive
vif.wr = item.wr;
vif.wdata = item.wdata;
$display("Driven:");
$display("Addr:%h Wr:%h Wdata:%h ",item.addr,item.wr,item.wdata);
@(posedge vif.clk) -> drv_done;  //Completion event
end
endtask
endclass 

5. Monitor
Function: Accept DUT responses from the virtual interface and make basic check for validity.
  • Monitor performs the opposite function of driver, meaning it accepts data from the virtual interface.
  • In run task, data is stored in the item class and sent to the scoreboard via mailbox.

class monitor;

virtual mem_if vif;
mailbox mbx_scb;
mem_item item;

task run();

$display ("T=%0t [Monitor] starting...", $time);

forever begin
if (vif.rstn) begin
item = new;
item.addr = vif.addr; //Capture vif in item variables
item.wr = vif.wr;
item.wdata = vif.wdata;
item.rdata = vif.rdata;
item.rvalid = vif.rvalid;
$display("Read data %h sent to scoreboard",item.rdata);
mbx_scb.put(item);  //Send to scoreboard via mailbox
end
@(posedge vif.clk); 
end
endtask

endclass

6. Scoreboard
Function: Checks for data integrity issues
  • Here, we are verifying a simple memory. What we do is during every read operation, we need to check if the data being read is the same as what was written to the memory at the same address.
  • In the run task, get the item from mailbox and code the logic to perform the above check.
Logic used for data integrity check:
  • Basically, we must try and mimic the DUT. Have a dummy memory in the scoreboard which writes data to the address during write operation.
  • During read operation, store the address in the first clock cycle (flag=0).
  • In the next clock cycle (flag=1) when read data is available, compare it with the data which was written to the dummy memory at the same address. If same, then it is a scoreboard match. Else, display as scoreboard error.

class scoreboard;

virtual mem_if vif;
mailbox mbx_scb;
mem_item item;
logic [4:0] temp_addr;
logic flag;
integer match;
integer error;

logic [31:0] mem [31:0];

task run();

flag = 0; match=0; error=0;
@(posedge vif.clk);
$display ("T=%0t [Scoreboard] starting...", $time);

forever begin
@(posedge vif.clk); #1;
mbx_scb.get(item);        //Get item from mailbox
$display ("T=%0t [Scoreboard] Item Received...", $time);
if (flag==1)              //Logic to check data integrity
begin
 flag = 0;
 if (mem[temp_addr]==item.rdata && item.rvalid) 
  begin $display("Scoreboard Match, Addr:%h Data:%h",temp_addr,item.rdata); match++; end
 else 
  begin $display ("Scoreboard Error, Data1:%h Data2:%h",mem[temp_addr],item.rdata); error++; end
end
if (!item.wr)
begin
 temp_addr = item.addr;
 flag=1;
end
else
begin
 mem[item.addr] = item.wdata;
end
end
endtask

endclass

Display statements from log file:

ncsim> run
T=0 [Test] Starting stimulus...

T=0 [Test] Applying stimulus...
Data is reset
T=0 [Driver] starting...
Driven:
Addr:00 Wr:0 Wdata:00000000 
T=0 [Monitor] starting...
T=10 [Scoreboard] starting...

T=10 [Test] Applying stimulus...
Data is reset
Driven:
Addr:00 Wr:0 Wdata:00000000 

T=30 [Test] Applying stimulus...
Data is reset
Driven:
Addr:00 Wr:0 Wdata:00000000 
Read data 00000000 sent to scoreboard
T=50 [Scoreboard] Item Received...

T=50 [Test] Applying stimulus...
Driven:
Addr:1c Wr:1 Wdata:5bc68746 
Read data xxxxxxxx sent to scoreboard

T=70 [Test] Applying stimulus...
Driven:
Addr:1c Wr:0 Wdata:dbbdecb8 
T=71 [Scoreboard] Item Received...
Scoreboard Error, Data1:xxxxxxxx Data2:xxxxxxxx
Read data xxxxxxxx sent to scoreboard

T=90 [Test] Applying stimulus...
Driven:
Addr:1a Wr:1 Wdata:78e7b1bc 
T=91 [Scoreboard] Item Received...
Read data 5bc68746 sent to scoreboard

T=110 [Test] Applying stimulus...
Driven:
Addr:1a Wr:0 Wdata:5af67b86 
T=111 [Scoreboard] Item Received...
Scoreboard Match, Addr:1c Data:5bc68746
Read data 5bc68746 sent to scoreboard

T=130 [Test] Applying stimulus...
Driven:
Addr:1f Wr:1 Wdata:cf00ca20 
T=131 [Scoreboard] Item Received...
Read data 78e7b1bc sent to scoreboard

T=150 [Test] Applying stimulus...
Driven:
Addr:1f Wr:0 Wdata:57806fbe 
T=151 [Scoreboard] Item Received...
Scoreboard Match, Addr:1a Data:78e7b1bc
Read data 78e7b1bc sent to scoreboard

T=170 [Test] Applying stimulus...
Driven:
Addr:1b Wr:1 Wdata:abf4b73f 
T=171 [Scoreboard] Item Received...
Read data cf00ca20 sent to scoreboard

T=190 [Test] Applying stimulus...
Driven:
Addr:1b Wr:0 Wdata:1c6d4df8 
T=191 [Scoreboard] Item Received...
Scoreboard Match, Addr:1f Data:cf00ca20

***SIMULATION COMPLETE***
Total number of scoreboard matches:   3
Total number of scoreboard errors:    1
Simulation complete via $finish(1) at time 210 NS + 0
./tb_top.sv:53 $finish;
ncsim> exit

From here, we can understand what all processes have taken place. At the end of simulation, we can print a summary of how many scoreboard errors were detected.
One error that we see above is due to x value upon coming out of reset, which can be ignored.

Simulation Waveform:



The simulated waveform is shown above. As seen, address has been constrained within the specified values. Read data is matching the write data for the same address.

Assertions:
We can have assertions in the testbench which convey some activity that is expected from the DUT. Here according to the DUT property, rvalid will be asserted exactly one cycle after any read operation. This can be implemented as an assertion so that we can ensure that this property is always satisfied throughout simulation time.
In fact, verification through assertions is a different topic of discussion in itself, known as Formal Verification.

Coverage:
Coverage details can be viewed on a tool like Cadence IMC. We have two types of coverage: Functional and Code coverage.

So this concludes a simple testbench in SystemVerilog. Note that at present, most industries use UVM standard for verification purposes. However, it is always expected that one has a strong hold on SystemVerilog concepts.

References:
Idea has been taken from here and modified with explanations and additional features.
This is a recommended website for self-learning SystemVerilog and UVM concepts.

No comments:

Post a Comment