Testing an Integer Adder with AXI-Stream Interfaces on SystemVerilog
Having recently changed jobs, moving from VHDL to SystemVerilog and finding myself in a team with a separate group of verifiers, I realized that I was seriously behind in verification. In VHDL, I had previously written only simple developer tests that showed that the block performed the required function and nothing more. Each test was written from scratch and there was no code reuse. Determined to fix this problem, I immersed myself in reading SystemVerilog for Verification A Guide to Learning the Testbench Language Features for authorship Chris Spear, Greg Tumbush. After reading the book, I realized that I needed to write some kind of test project to consolidate the information I had received. I remembered that I had seen a series of articles on verification from @pcbteach on Habr, written in Verilog, and decided to write a test for an adder with AXI-Stream interfaces in SystemVerilog.
Content
1 General structure of the test environment
2 Test environment for checking the adder
3 Implementation of interfaces for testing the adder
4 Implementation of classes for testing the adder
5 Connecting the test environment and the device under test
1 General structure of the test environment
This diagram is given in the book as a support for writing a test environment.
Generator generates a transaction and sends it to Agent. At the level Generator A transaction is an instance of a class, without any binding on how that data should be transferred in the physical world.
Agent receives the transaction and forwards it to Driver (for transmission to the tested unit) and Scoreboard (to analyze the execution result).
Driver translates the transaction into its physical embodiment (controls the bus signals) and transfers it to the device under test (Device Under Test).
Scoreboard Based on the original transaction, it predicts what transaction should be output by the device under test.
Monitor receives bus signals from the output of the Device Under Test (DUT) and converts them into an instance of the class.
Checker compares transactions from Monitor And Scoreboard.
Environment combines all the blocks used for testing.
Thus, when going beyond the block Environment the transaction class instance is translated into a physical representation of signals and fed to the device under test, and from the output of the device under test the physical representation of signals is translated into a transaction class instance. Each block performs only one action, so such blocks will be more universal.
2 Test environment for checking the adder
The adder under consideration has two inputs with AXI-Stream interfaces – for loading operand 1 and operand 2. And one output with AXI-Stream interface – the result of the adder's operation. Accordingly, two copies of Generator blocks, two copies of Agent blocks, two copies of Driver blocks, one copy of Monitor block, one copy of Cheker block, one copy of Scoreboard block are required.
SystemVerilog allows you to implement interactions between blocks within an Environment in several ways:
arrays with synchronization using events;
arrays synchronized using semaphores;
mailboxes;
queues
and other options.
Also, SystemVerilog provides the ability to implement interaction between Environment and DUT using:
The interaction between blocks within the Environment will be carried out using mailboxes, the interaction between the Environment and the DUT will be carried out using virtual interfaces, which seemed easier to use to me.
3 Implementation of interfaces for testing the adder
interface AXIS_Bus #(parameter DW=8) (input clk);
logic [DW-1:0] tdata;
logic tvalid;
logic tready;
modport slave (
input tdata,
input tvalid,
output tready
);
modport master (
output tdata,
output tvalid,
input tready
);
endinterface
To interact with the device under test, the AXIS-Bus interface is used, which describes the tdata, tvalid, tready signals used by the adder block.
4 Implementation of classes for testing the adder
4.1 Implementation of classes for testing the adder
`ifndef OPERAND_ITEM_CLS_PKG__SV
`define OPERAND_ITEM_CLS_PKG__SV
package operand_item_cls_pkg;
class operand_item_cls #(parameter int DW = 4);
rand bit [DW-1:0] m_data;
function void print(string tag = "");
$display("[%s] Item value = 0x%0h", tag, m_data);
endfunction : print
endclass : operand_item_cls
endpackage : operand_item_cls_pkg
`endif //OPERAND_ITEM_CLS_PKG__SV
The operand_item_cls class is used to specify the operand that is fed to the adder input. The class has a DW parameter that defines the width (in bits) of the m_data field. The class also has a method for printing the current value of the tdata field. The m_data field has a rand modifier, which means that each time the function is called, randomize for a class object, the value of the m_data field will take a random value.
4.2 result_item_cls
`ifndef RESULT_ITEM_CLS_PKG__SV
`define RESULT_ITEM_CLS_PKG__SV
package result_item_cls_pkg;
class result_item_cls #(parameter int DW = 5);
bit [DW-1:0] m_data;
function void print(string tag = "");
$display("[%s] Item value = 0x%0h", tag, m_data);
endfunction : print
endclass : result_item_cls
endpackage : result_item_cls_pkg
`endif //RESULT_ITEM_CLS_PKG__SV
The result_item_cls class is used to get the result from the adder output. The class has a DW parameter that defines the width (in bits) of the m_data field. The class also has a method for printing the current value of the m_data field.
4.3 generator_cls
`ifndef GENERATOR_CLS_PKG__SV
`define GENERATOR_CLS_PKG__SV
package generator_cls_pkg;
`include "rand_check.svh"
import operand_item_cls_pkg::*;
class generator_cls #(parameter int DW = 4);
operand_item_cls #(.DW(DW)) item;
mailbox #(operand_item_cls #(.DW(DW))) mbx;
function new (input mailbox #(operand_item_cls #(.DW(DW))) mbx);
this.mbx = mbx;
endfunction : new
task run (input int count);
repeat (count) begin
item = new();
`SV_RAND_CHECK(item.randomize());
mbx.put(item);
end
endtask : run
endclass : generator_cls
endpackage : generator_cls_pkg
`endif //GENERATOR_CLS_PKG__SV
The generator_cls class is used to form a transaction and pass it to the agent_cls block. The transaction is an instance of the operand_item_cls class. When creating an instance of the class, a pointer to the mailbox is passed to the constructor (the new function), which is used to pass the transaction to the agent_cls block.
Transactions are created in the run method, which takes as input the number of transactions to be sent. In the loop, a transaction object is created (the mailbox does not store the object, only a reference to it), filled with a random value, and placed in the mailbox.
`SV_RAND_CHECK – a macro that checks that the filling of class instances with random values was successful.
4.4 agent_cls
`ifndef AGENT_CLS_PKG__SV
`define AGENT_CLS_PKG__SV
package agent_cls_pkg;
import operand_item_cls_pkg::*;
class agent_cls #(parameter int DW = 4);
mailbox #(operand_item_cls #(.DW(DW))) gen2agt, agt2drv, agt2scb;
operand_item_cls #(.DW(DW)) item;
function new(input mailbox #(operand_item_cls #(.DW(DW))) gen2agt, agt2drv, agt2scb);
this.gen2agt = gen2agt;
this.agt2drv = agt2drv;
this.agt2scb = agt2scb;
endfunction : new
task run();
forever begin
gen2agt.get(item);
agt2scb.put(item);
agt2drv.put(item);
end
endtask : run
endclass : agent_cls
endpackage : agent_cls_pkg
`endif //AGENT_CLS_PKG__SV
The agent_cls class is used to receive the transaction from generator_cls and pass it to driver_cls and scoreboard_cls (in fact, the transaction is duplicated). The transaction is an instance of the operand_item_cls class. When creating an instance of the class, the constructor (the new function) is passed pointers to mailboxes used to receive the transaction and pass it to the next blocks.
The transaction is sent in the run method. In an infinite loop, the transaction is received from the mailbox from generator_cls and the transaction is placed in the driver_cls and scoreboard_cls mailboxes.
4.5 driver_cls
`ifndef DRIVER_CLS_PKG__SV
`define DRIVER_CLS_PKG__SV
package driver_cls_pkg;
import operand_item_cls_pkg::*;
class driver_cls #(parameter int DATA_WIDTH = 4, parameter int AXIS_WIDTH = 8);
virtual AXIS_Bus #(.DW(AXIS_WIDTH)) vif;
mailbox #(operand_item_cls #(.DW(DATA_WIDTH))) mbx;
operand_item_cls #(.DW(DATA_WIDTH)) item;
function new (input mailbox #(operand_item_cls #(.DW(DATA_WIDTH))) mbx, input virtual AXIS_Bus #(.DW(AXIS_WIDTH)) vif);
this.mbx = mbx;
this.vif = vif;
endfunction : new
task run (input int count, input int min_delay, input int max_delay, input bit en_display = 0);
if (en_display)
$display("[driver_cls] Starting...");
vif.tvalid <= 1'b0;
repeat (count) begin
if (en_display)
$display("[driver_cls][ Waiting for item...");
mbx.get(item);
if (en_display)
item.print("driver_cls");
repeat ($urandom_range(min_delay, max_delay)) begin
@(posedge vif.clk);
end
vif.tdata <= item.m_data;
vif.tvalid <= 1'b1;
@(posedge vif.clk);
while (!vif.tready) begin
@(posedge vif.clk);
end
vif.tvalid <= 1'b0;
if (en_display)
$display("[driver_cls] Item sended...");
end
endtask : run
endclass : driver_cls
endpackage : driver_cls_pkg
`endif //DRIVER_CLS_PKG__SV
The driver_cls class is used to send a transaction to the device under test. The transaction is an instance of the operand_item_cls class. In addition to the DATA_WIDTH parameter for specifying the width of the source operand field, the class has an AXIS_WIDTH parameter for specifying the width of the AXI-Stream interface used to interact with the device under test. When creating an instance of the class in the constructore
(the new function) is passed pointers to the mailbox containing the transaction and the virtual interface connected to the device under test.
To generate switching of individual signals in the interface from the class instance, the run method is used, which takes the number of transactions as input. The method cyclically generates signals. A transaction is retrieved from the mailbox, a random time is expected before the transaction starts, the contents of the m_data field of the operand_item_cls class are set to the tdata bus of the interface, and tvalid is set to 1. The appearance of the tready signal equal to 1 is expected, after which tvalid goes to state 0, the transaction is considered transferred
4.6 monitor_cls
`ifndef MONITOR_CLS_PKG__SV
`define MONITOR_CLS_PKG__SV
package monitor_cls_pkg ;
import result_item_cls_pkg::*;
class monitor_cls #(parameter int DATA_WIDTH = 5, parameter int AXIS_WIDTH = 8);
virtual AXIS_Bus #(.DW(AXIS_WIDTH)) vif;
mailbox #(result_item_cls #(.DW(DATA_WIDTH))) mbx;
function new (input mailbox #(result_item_cls #(.DW(DATA_WIDTH))) mbx, input virtual AXIS_Bus #(.DW(AXIS_WIDTH)) vif);
this.vif = vif;
this.mbx = mbx;
endfunction : new
task run (input int count, input int min_delay, input int max_delay, input bit en_display = 0);
if (en_display)
$display("[monitor_cls] Starting...");
vif.tready <= 1'b0;
repeat (count) begin
result_item_cls #(.DW(DATA_WIDTH)) item = new;
repeat ($urandom_range(min_delay, max_delay)) begin
@(posedge vif.clk);
end
vif.tready <= 1'b1;
@(posedge vif.clk);
while (!vif.tvalid) begin
@(posedge vif.clk);
end
item.m_data = vif.tdata;
vif.tready <= 1'b0;
if (en_display)
item.print("monitor_cls");
mbx.put(item);
end
endtask : run
endclass : monitor_cls
endpackage : monitor_cls_pkg
`endif //MONITOR_CLS_PKG__SV
The monitor_cls class is used to analyze the switching of the tdata, tvalid, tready signals and to form an object of the result_item_cls class, which stores the result of the adder's operation. In addition to the DATA_WIDTH parameter for setting the width of the result field, the class has an AXIS_WIDTH parameter for setting the width of the AXI-Stream interface used when interacting with the device under test. When creating an instance of the class, pointers to the mailbox where the transaction must be placed and to the virtual interface connected to the device under test are passed to the constructor (the new function).
To form a class instance from individual interface signals, the run method is used, which takes the number of transactions as input. The method cyclically generates a class instance. A random time is expected before the transaction begins. The tready signal of the interface is set to 1, and the tvalid signal equal to 1 is expected. After this, the value of the tdata signal of the interface is entered into the m_data field of the class instance, and the tready signal takes the value 0, the transaction is considered processed.
4.7 scoreboard_cls
`ifndef SCOREBOARD_CLS_PKG__SV
`define SCOREBOARD_CLS_PKG__SV
package scoreboard_cls_pkg;
import operand_item_cls_pkg::*;
import result_item_cls_pkg::*;
class scoreboard_cls #(parameter int DW = 4);
mailbox #(operand_item_cls #(.DW(DW))) mbx_op1, mbx_op2;
mailbox #(result_item_cls #(.DW(DW+1))) mbx_res;
function new ( input mailbox #(operand_item_cls #(.DW(DW))) mbx_op1, mbx_op2, input mailbox #(result_item_cls #(.DW(DW+1))) mbx_res);
this.mbx_op1 = mbx_op1;
this.mbx_op2 = mbx_op2;
this.mbx_res = mbx_res;
endfunction : new
task run (int count, input bit en_display = 0);
operand_item_cls #(.DW(DW)) operand1, operand2;
result_item_cls #(.DW(DW+1)) result;
if (en_display)
$display("[scoreboard_cls] Starting... ");
repeat (count) begin
fork
mbx_op1.get(operand1);
mbx_op2.get(operand2);
join
result = new ();
result.m_data = operand1.m_data + operand2.m_data;
mbx_res.put(result);
end
endtask : run
endclass : scoreboard_cls
endpackage : scoreboard_cls_pkg
`endif //SCOREBOARD_CLS_PKG__SV
The scoreboard_cls class is used to calculate the expected result of the adder based on the source operands. The source operands are represented by instances of the operand_item_cls class, the expected result is represented by an instance of the result_item_cls class. When creating an instance of the class, pointers to mailboxes are passed to the constructor (the new function), in which the operands are stored and in which the expected result of the adder must be placed.
The analysis of operands and calculation of the expected result is performed in the run method, which takes the number of transactions as input. In the loop, operands are extracted from mailboxes, an object is formed to save the result, and the expected result value is entered into the mailbox.
4.8 checker_cls
`ifndef CHECKER_CLS_PKG__SV
`define CHECKER_CLS_PKG__SV
package checker_cls_pkg;
import result_item_cls_pkg::*;
class checker_cls #(parameter int DW = 5);
mailbox #(result_item_cls #(.DW(DW))) mbx_res_sc, mbx_res_mon;
int count_good;
int count_bad;
function new ( input mailbox #(result_item_cls #(.DW(DW))) mbx_res_sc, mbx_res_mon);
this.mbx_res_sc = mbx_res_sc;
this.mbx_res_mon = mbx_res_mon;
count_good = 0;
count_bad = 0;
endfunction : new
task run (int count, input bit en_display = 0);
result_item_cls #(.DW(DW)) result_sc, result_mon;
if (en_display)
$display("[checker_cls] Starting... ");
repeat (count) begin
fork
mbx_res_sc.get(result_sc);
mbx_res_mon.get(result_mon);
join
if (result_sc.m_data == result_mon.m_data) begin
count_good++;
end
else begin
count_bad++;
end
end
if (count_bad != 0) begin
$display("[checker_cls] Fail!");
$finish;
end
else if (count_good == count) begin
$display("[checker_cls] Pass!");
$finish;
end
endtask : run
endclass : checker_cls
endpackage : checker_cls_pkg
`endif //CHECKER_CLS_PKG__SV
The checker_cls class is used to check whether the expected result of the adder's operation matches the actual result of the adder's operation. The results are represented by instances of the result_item_cls class. When creating an instance of the class, pointers to mailboxes are passed to the constructor (the new function), where the results of the adder's operation (expected and actual) are stored.
The analysis of the results is performed in the run method, which takes the number of transactions as input. In the loop, the results are extracted from the mailboxes and compared. If the expected result is equal to the actual result, the counter of correct transactions is increased, otherwise, the counter of incorrect transactions is increased. If an incorrect transaction was received, the test stops.
4.9 config_cls
`ifndef CONFIG_CLS_PKG_SV
`define CONFIG_CLS_PKG_SV
package config_cls_pkg;
class config_cls;
rand bit [31:0] run_for_n_trans;
rand bit [ 7:0] min_delay;
rand bit [ 7:0] max_delay;
constraint reasonable {
run_for_n_trans inside {[1:1000]};
min_delay < max_delay;
}
endclass : config_cls
endpackage : config_cls_pkg
`endif //CONFIG_CLS_PKG_SV
The config_cls class is used to obtain random parameters for the test environment. The following fields are defined in the class:
run_for_n_trans (number of times to run the test);
min_delay (minimum delay on the AXI-Stream interface);
max_delay (maximum delay on the AXI-Stream interface).
The fields have rand identifiers, meaning that each time the randomize() function is called on an instance of the class, the fields will be filled with a random value. The run_for_n_trans field has a constraint that after calling the randomize() function, the value of the run_for_n_trans field must be in the range from 1 to 1000. The min_delay and max_delay fields have a constraint that min_delay must be less than max_delay.
4.10 environment_cls
`ifndef ENVIRONMENT_CLS_PKG__SV
`define ENVIRONMENT_CLS_PKG__SV
package environment_cls_pkg;
import operand_item_cls_pkg::*;
import result_item_cls_pkg::*;
import generator_cls_pkg::*;
import agent_cls_pkg::*;
import driver_cls_pkg::*;
import monitor_cls_pkg::*;
import scoreboard_cls_pkg::*;
import checker_cls_pkg::*;
import config_cls_pkg::*;
`include "rand_check.svh"
parameter OPERAND_QTY = 2;
class environment_cls #(parameter int DATA_WIDTH = 4, parameter int AXIS_IN_WIDTH = 8, parameter int AXIS_OUT_WIDTH = 8);
generator_cls #(.DW(DATA_WIDTH)) gen[OPERAND_QTY-1:0];
agent_cls #(.DW(DATA_WIDTH)) agt[OPERAND_QTY-1:0];
driver_cls #(.DATA_WIDTH(DATA_WIDTH), .AXIS_WIDTH(AXIS_IN_WIDTH)) drv[OPERAND_QTY-1:0];
monitor_cls #(.DATA_WIDTH(DATA_WIDTH+1), .AXIS_WIDTH(AXIS_OUT_WIDTH)) mon;
scoreboard_cls #(.DW(DATA_WIDTH)) scb;
checker_cls #(.DW(DATA_WIDTH+1)) chk;
config_cls cfg;
mailbox #(operand_item_cls #(.DW(DATA_WIDTH))) mbx_gen2agt [OPERAND_QTY-1:0];
mailbox #(operand_item_cls #(.DW(DATA_WIDTH))) mbx_agt2scb [OPERAND_QTY-1:0];
mailbox #(operand_item_cls #(.DW(DATA_WIDTH))) mbx_agt2drv [OPERAND_QTY-1:0];
mailbox #(result_item_cls #(.DW(DATA_WIDTH+1))) mbx_scb2chk;
mailbox #(result_item_cls #(.DW(DATA_WIDTH+1))) mbx_mon2chk;
virtual AXIS_Bus #(.DW(AXIS_IN_WIDTH)) operand1;
virtual AXIS_Bus #(.DW(AXIS_IN_WIDTH)) operand2;
virtual AXIS_Bus #(.DW(AXIS_OUT_WIDTH)) result;
function new( input virtual AXIS_Bus #(.DW(AXIS_IN_WIDTH)) operand1, operand2, input virtual AXIS_Bus #(.DW(AXIS_OUT_WIDTH)) result);
cfg = new();
this.operand1 = operand1;
this.operand2 = operand2;
this.result = result;
endfunction
function void gen_cfg();
`SV_RAND_CHECK(cfg.randomize());
endfunction : gen_cfg
function void build();
for (int i = 0; i < OPERAND_QTY; i++) begin
mbx_gen2agt[i] = new(1);
mbx_agt2scb[i] = new(1);
mbx_agt2drv[i] = new(1);
end
mbx_scb2chk = new(1);
mbx_mon2chk = new(1);
for (int i = 0; i < OPERAND_QTY; i++) begin
gen[i] = new(.mbx(mbx_gen2agt[i]));
agt[i] = new(.gen2agt(mbx_gen2agt[i]), .agt2drv(mbx_agt2drv[i]), .agt2scb(mbx_agt2scb[i]));
end
drv[0] = new(.mbx(mbx_agt2drv[0]), .vif(operand1));
drv[1] = new(.mbx(mbx_agt2drv[1]), .vif(operand2));
mon = new(.mbx(mbx_mon2chk), .vif(result));
scb = new(.mbx_op1(mbx_agt2scb[0]), .mbx_op2(mbx_agt2scb[1]), .mbx_res(mbx_scb2chk));
chk = new(.mbx_res_sc(mbx_scb2chk), .mbx_res_mon(mbx_mon2chk));
endfunction : build
task run();
fork
gen[0].run(cfg.run_for_n_trans);
gen[1].run(cfg.run_for_n_trans);
agt[0].run();
agt[1].run();
drv[0].run(cfg.run_for_n_trans, cfg.min_delay, cfg.max_delay);
drv[1].run(cfg.run_for_n_trans, cfg.min_delay, cfg.max_delay);
mon.run(cfg.run_for_n_trans, cfg.min_delay, cfg.max_delay);
scb.run(cfg.run_for_n_trans);
chk.run(cfg.run_for_n_trans);
join
endtask : run
endclass : environment_cls
endpackage : environment_cls_pkg
`endif //ENVIRONMENT_CLS_PKG__SV
The environment_cls class is used to connect all the blocks involved in testing. The class has the following parameters: DATA_WIDTH (the width of the operands and the result), AXIS_IN_WIDTH (the width of the AXI-Stream interface for the operands), AXIS_OUT_WIDTH (the width of the AXI-Stream interface for the result). When creating an instance of the class, pointers to virtual interfaces connected to the device under test are passed to the constructor (the new function), and an object of the config_cls class is also created.
The gen_cfg function is used to set random parameters for the test environment.
The build function creates mailboxes for exchanging transactions between blocks, as well as instances of the generator_cls, agent_cls, driver_cls, monitor_cls, scoreboard_cls blocks.
The run function starts the creation of transactions, their transmission, reception and verification.
5 Connecting the test environment and the device under test
The adder testing classes are implemented. Now we need to connect the adder testing classes and the device under test to each other.
5.1 test_cls
`ifndef TEST_CLS_PKG__SV
`define TEST_CLS_PKG__SV
package test_cls_pkg;
import environment_cls_pkg::*;
class test_cls #(parameter int DATA_WIDTH = 4, parameter int AXIS_IN_WIDTH = 8, parameter int AXIS_OUT_WIDTH = 8);
environment_cls #(.DATA_WIDTH(DATA_WIDTH), .AXIS_IN_WIDTH(AXIS_IN_WIDTH), .AXIS_OUT_WIDTH(AXIS_OUT_WIDTH)) env;
function new( input virtual AXIS_Bus #(.DW(AXIS_IN_WIDTH)) operand1, operand2, input virtual AXIS_Bus #(.DW(AXIS_OUT_WIDTH)) result);
env = new(operand1, operand2, result);
endfunction
task run ();
env.gen_cfg();
env.build();
env.run();
endtask
endclass : test_cls
endpackage : test_cls_pkg
`endif //TEST_CLS_PKG__SV
The test_cls class is used to connect the test environment. The class has the parameters DATA_WIDTH (the width of the operands and the result), AXIS_IN_WIDTH (the width of the AXI-Stream interface for the operands), AXIS_OUT_WIDTH (the width of the AXI-Stream interface for the result). When creating an instance of the class, pointers to virtual interfaces connected to the device under test are passed to the constructor (the new function).
The run method sequentially sets random parameters for testing, creates all the necessary blocks, assembles them together, and starts testing.
5.2 tb
`timescale 1ns/1ps
module tb;
import test_cls_pkg::*;
localparam CLK_PERIOD = 10ns;
localparam CNT_RESET_CYCLE = 10;
logic clk;
logic rstn;
localparam integer ADDER_WIDTH = 8;
localparam integer IN_AXIS_WIDTH = $ceil($itor(ADDER_WIDTH) / 8) * 8;
localparam integer OUT_AXIS_WIDTH = $ceil($itor(ADDER_WIDTH+1) / 8) * 8;
AXIS_Bus #(IN_AXIS_WIDTH) operand1_if (clk);
AXIS_Bus #(IN_AXIS_WIDTH) operand2_if (clk);
AXIS_Bus #(OUT_AXIS_WIDTH) result_if (clk);
initial begin : clk_gen
clk <= 1'b0;
forever begin
#(CLK_PERIOD/2);
clk <= ~clk;
end
end : clk_gen
initial begin : rst_gen
rstn <= 1'b0;
repeat (CNT_RESET_CYCLE)
@(posedge clk);
rstn <= 1'b1;
end : rst_gen
test_cls #(.DATA_WIDTH(ADDER_WIDTH), .AXIS_IN_WIDTH(IN_AXIS_WIDTH), .AXIS_OUT_WIDTH(OUT_AXIS_WIDTH)) test;
initial begin : stim_gen
if ($test$plusargs("SEED")) begin
int seed;
$value$plusargs("SEED=%d", seed);
$display("Simalation run with random seed = %0d", seed);
$urandom(seed);
end
else
$display("Simulation run with default random seed");
test = new(operand1_if, operand2_if, result_if);
@(posedge rstn);
test.run();
end : stim_gen
adder_axis_pipe_mp #(
.ADDER_WIDTH (ADDER_WIDTH)
)
u_adder_axis_pipe_mp(
.ACLK_I (clk),
.ARST_N (rstn),
.AXIS_OPERAND1_IF (operand1_if),
.AXIS_OPERAND2_IF (operand2_if),
.AXIS_RESULT_IF (result_if)
);
initial begin : watchdog
@(posedge rstn);
@(posedge clk);
$display("Transaction quantity = %4d", test.env.cfg.run_for_n_trans);
repeat(test.env.cfg.run_for_n_trans * test.env.cfg.max_delay) @(posedge clk);
$display("ERROR! Watchdog error!");
$finish;
end : watchdog
endmodule
The tb module connects the test environment and the device under test. The clk_gen block generates a clock signal for the device under test and the test environment. The rst_gen block generates a reset signal for the device under test and the test environment, the stim_gen block connects the ports of the adder_axis_pipe_mp block to the test environment and starts the test. The SEED variable, which is passed via command line parameters (PLUSARG), is used as the initial value for random.
6 Conclusion
# Simulation run with default random seed
# Transaction quantity = 568
# [checker_cls] Pass!
...
# Simalation run with random seed = 68
# Transaction quantity = 563
# [checker_cls] Pass!
A test environment for the adder with AXI-Stream interfaces was developed in the SystemVerilog language using classes. The result was a set of classes, each of which is functionally complete, which allows them to be reused использовать повторно
in other test environments. Generating random values has become easier when using the randomize() class method. Only driver_cls and monitor_cls classes interact with the physical interface. UVM testing technology is not considered, maybe later.
Source codes: https://github.com/Finnetrib/AXI-Stream-Adder/tree/axis-adder-v2