UVM like TB using SV

UVM like tb framework using SV: (from sample project)

Here, we build a testbench framework using all these SV constructs that we learned earlier, i.e class, program, env, interface, etc. This is how UVM framework is developed that we'll learn in other section.


usbpd_tb.sv:

`include testbench.sv //this has the program for all classes
module usbpd_tb();
 wire, reg, ...;
 function/task func1() .. endfunction //functions/tasks
 initial/always @(...) begin .... end //build osc, release porz, checkers for clk
 digtop I_digtop (.in1(tb_in1), out1(tb_out1)); //DUT inst
 i2c_bfm I_i2c_bfm ( ...);
 sram_monitor i_mon (.addr(s_addr), ...);
 cm0_tarmac u_tarmac(.en(1'b1),...); //tarmac inst for M0
 specify $width(...); ....; endspecify //we can define our own clk pulse width check, etc (similar to what's in gate models)
//property checkers included here
 property hready_checker;
  @(posedge clk) disable iff (hresetn) !hready |=> hready_i; //checker disabled if hresetn=0, else hready_i should match !hready on next clk posedge
 endproperty
 ass_prop_chk : assert property (hready_checker) //property asserted here
                      begin $display("OK"); end
               else begin $display("ERR"); end
 cov_prop_chk : cover property (hready_checker) //property covered here

 tb_intf I_tb_if(); //interface defined  below in testbench.sv
endmodule

testbench.sv:

typedef class register_base;
typedef class register_field_base;
typedef class register_block_base;
typedef class ahb_collector;

interface tb_intf(); //i/f defn
 logic data, addr, ...;
 task wrt_addr(...);
endinterface

//start of register_base class => has all reg attributes as name, value, etc.
class register_base;
   string         parent_module;
   string         name;
   enum {READ, WRITE} rw;
   bit [31:0] address;
   bit [31:0] value;             // same as mirrored_value in UVM registers
   bit [31:0] rand_value;        // same as value or desired value in UVM registers
   bit [31:0] rd_mask; //similarly wr_mask, rd_to_clr, wr0_to_clr, wr1_to_clr, wr1_to_set.

  register_block_base parent_reg_blk;
  //covergroups
   covergroup cg_bit_31 @(bit_31_wr_done_ev or bit_31_rd_done_ev); //sampled whenever these events happen
      cp_bit_31 : coverpoint {rw,value[31] } { //coverpoint cp_bit_31 defined for rw and value[31] bits
     wildcard bins tran_r0_w0_r = ({READ,1'b0} => {WRITE,1'b0} => {READ,1'bx}); // bin for transition from rd to wrt to rd
     wildcard bins trans_r1_r   = ({READ,1'b1} => {READ,1'bx});
      }
   endgroup //similar covergroup for other bits  

   register_field_base reg_fld_q[$]; // queue of register fields
   
   function new ( string module_name, string reg_name, bit [31:0] reg_address,
          bit [31:0] rd_mask, bit [31:0] wr_value, bit [31:0] rd_value, register_block_base register_block_base_inst);
      this.parent_module = module_name;
      this.nmame = reg_name;
      this.address = reg_address;
      this.value = rst_value;
      this.rd_mask = rd_mask;
      this.parent_reg_blk = register_block_base_inst;
      this.d_value   = this.value;
 
     register_block_base_inst.add_reg(module_name, this); //func defined in reg block base
   endfunction

   virtual function string get_name(); //string implies return value
      return (name); //returns name and so on for other func
   endfunction

   virtual function void reset(); //to reset fields, return value is nothing, so "void" used
      value      = rst_value;
      foreach (reg_fld_q[i]) void'(reg_fld_q[i].reset());
   endfunction
endclass //end of register_base class

//create a class for each reg
class SPARE_REG_type extends register_base;
   rand register_field #(6) RESERVED;
   rand register_field #(6) STICKY_STS_SET;
   rand register_field #(20) RESERVED1;

   function new ( string module_name, string reg_name, bit [31:0] reg_address, bit [31:0] rst_value,
                  bit [31:0] rd_mask, bit [31:0] wr_value, bit [31:0] rd_value, register_block_base reg_blk_inst);
     super.new(module_name, reg_name, reg_address, rst_value, rd_mask,  wr_value, rd_value, reg_blk_inst);
   endfunction
endclass

class AUX_REG ... endclass // and so on for all regs
//end of class for all regs in design

//start of register_field_base
class register_field_base;
   string          name;
   bit      [7:0]      start_bit;
   bit      [7:0]      length;
   register_base parent_reg;

   function new ();
   endfunction // note: nothing defined in new func, similarly other func defined with nothing in them, as they will be defined in register_field class
endclass

class register_field #(int FIELD_LENGTH = 32) extends register_field_base;
   rand bit [FIELD_LENGTH-1:0]      rand_value; // desired_value   // parameterized
   bit      [FIELD_LENGTH-1:0]      value;      // mirrrored_value // parameterized

   covergroup    reg_field_cg  @(parent_reg.wr_done_ev or parent_reg.rd_done_ev);
      option.name = name;
      cp_reg_field_value: coverpoint (value) { // coverpoint for all values of that reg field
     bins field_val[] = {[0: max_value]}; //array of bins created from field_val_0 to field_val[max_value]. So, if 0 sampled, then bin fiel_val_0 is covered and so on. advantage of array of bins is that we can see which values are missing from coverage
      }
   endgroup // reg_field_cg
 
   function new ( string fld_name, int start_bit, int fld_len, register_base parent_register);
      this.name       = fld_name; //name, start_bit, length properties come from reg field base
      this.start_bit  = start_bit;
      this.length     = fld_len;
      this.parent_reg = parent_register;
      parent_register.add_reg_field(this);
   endfunction // new

   virtual       function void reset();
      // get the reset value from the parent register and copy to the rest of values
      value      = ((parent_reg.rst_value >> start_bit) & max_value);
      prev_value = value;
   endfunction // reset
endclass

//class reg block base => largest class in terms of code => all checkers implemented here as tasks
class register_block_base;
 register_base reg_q[string][$]; //contains queue of reg base
 mailbox reg_tr_mbx;
 SPARE_REG_type SPARE_REG; //and so on for all other regs
 
   covergroup custom_SPI_cg; //specify covergroups for regs and bits which we want sampled when C pgm accesses those
    cp_mon: coverpoint SPARE_REG.MODE.value;
    ...
   endgroup

   task ... forever ... endtask
   task automatic cfg_assert(); //all IO pin checks are in this task, similarly frs_assert, gpio_assert etc
     config_assert: assert(SPARE_REG.SPARE_OUT.value === `TB.pin[12]) else $display("ASSERTION ERROR"); //similarly for all other IO pins
   endtask
 
   function void add_reg (string module_name, register_base reg_inst);    
      foreach (reg_q[module_name][i]) // check existing registers to make sure there is no duplicate address
    if (reg_q[module_name][i].get_address() == reg_inst.get_address()) //do not add reg
    begin
         $display("ERROR: module_name.substr %s", module_name.substr(0,2));
             $finish;
        end  
      reg_q[module_name].push_back(reg_inst); // push reg at end of queue,
   endfunction // add_reg
 
   function cg_sample ( ...) //called from detect_rand_request (whenever C pgm writes to loc indicating it wants sampling)
     if (testname.substr(0, 2) == "spi") custom_SPI_cg.sample();
     else if (...)                     custom_UART0_cg.sample();

   function void build();      
      create_reg_space(); // instantiate register model and covergroups etc (defined in function below)
      CONFIG_SPARE_WAKEUP_REG.rd_mask = 32'hFFF81FFF;      
   endfunction // build

   task start (); //impl as task since they have always, forever etc
      apply_hw_lock_mask(); //func defined in this class
      apply_trim_lock_mask(); //func defined in this class
      fork
     register_adapter(); forever task in reg_block_base. At every FCLK edge, it samples AHB tran, and calls gpio_sample, config_sample, etc, depending on addr.
     detect_rand_request(); //forever task (@ posedge FCLK, HWRITE=1 and HADDR=RAND_REQ_ADDR and then look at HWDATA[15:0]) to detect random req wrt to different regs (wrt rand val to reg model and copy that to sram)
     detect_incoming_rand_xfer(); //to detect tb_if.req to wrt incoming addr/data to sram + detect tb_if.done to wrt end signature once done
     test_end_detect(); //this is a forever task to detect "EXIT" inst being written
         output_checker(); //it's a fork forever task that calls bunch of other assert tasks as "cfg_assert", on event @config_debug_sampled, etc. these events are triggered in func "config_sample" (called in register_adapter task) when addr match for wrt happens on AHB bus.
      join_none //above forked process keep running for ever
  endtask // start
 
   function void create_reg_space();
      SPARE_REG = new("CONFIG", "SPARE_REG", 32'h01F0, 32'h00000000, 32'hffffffff, 32'hffffffff, 32'h00000000, 32'h00000000, 32'h00000000, 32'h00000000, 32'h00000000, 32'h00000000, 32'h00000000, this); // 0x01F0 - SPARE_WAKEUP_SET_REG
        SPARE_REG.RESERVED = new("RESERVED", 26, 6, SPARE_REG);
        SPARE_REG.STICKY_STS_SET = new("STICKY_STS_SET", 20, 6, SPARE_REG);
        SPARE_REG.RESERVED1 = new("RESERVED1", 0, 20, SPARE_REG);

     AUX_REG = new("...); //similarly for all other regs and corresponding bits
   endfunction

   task register_adapter(); //samples transaction on AHB, and depending on reg addr, puts deglitch values, etc on tb_pin_intf signals
    forever begin
      @(posedge `CPUTOP.FCLK);
        if (reg_tr_mbx.try_get(curr_tran) == 1'b0) $display ("NO AHB transaction"); //get AHB msg that was put in collector below
        else begin (if curr_tran.addr == 16'h0F30) reg_h = config_sample(curr_tran.addr[15:2], curr_tran.wr_data, curr_tran.rd_data, 0);
    else       (if curr_tran.addr == 16'h0A50) reg_h = gpio_sample(...) and so on
   endtask

endclass //end of reg block base

//AHB collector
class ahb_collector; //this collects 1 AHB transaction with addr, rddata, wrdata, etc
  string name;
  mailbox tr_mbx;
  ahb_transaction ahb_tran; //a class defined with addr, wrdata, etc
 
  function new (string name, mailbox tr_mbx);
    this.name = name;
    this.tr_mbx = tr_mbx;
  endfunction // new
      
  task body();    
     forever begin        
        @(negedge `CPUTOP.FCLK);
          ahb_tran = new(`CPUTOP.HWDATA, `CPUTOP.HRDATA, ... );
        if (tr_mbx.try_put(ahb_tran) == 1'b0) $display(ERROR"); //ahb_tran class object pushed into mailbox
         end
  endtask
endclass
               
       
//class env
class env;
   register_block_base reg_blk_inst;
   mailbox tr_mbx; //mailbox to store 1 AHB transaction from M0
   ahb_collector  ahb_collector_0;
 
   function new();
      tr_mbx       = new(1); // each module mailbox is one deep
      ahb_collector_0     = new("AHB Collector for M0p", tr_mbx);  
      reg_blk_inst = new(tr_mbx);
   endfunction // new
    
   virtual task build();
      reg_blk_inst.build(); //creates reg class for all regs and their fields by calling "create_reg_space" func
   endtask // build
   
   virtual task start();
      fork
     reg_blk_inst.start(); //task start, which itself is bunch of forked proc, called from reg block_base. all checkers, etc here
     ahb_collector_0.body(); //collects all AHB transactions
     misc_collector_0.body();
     dbg_fifo_sb_0.body();    
      join_none; //above forked proc run forever
   endtask // start
endclass // env

//testbench starts from here. this is another top level module besides usbpd_tb. It's not called from anywhere.
program testbench();
 env env_inst;

 initial begin
  env_inst = new();
  env_inst.build();
  env_inst.start();
  wait(`TB.tb_finish_test == 1); // wait for test bench to finish test (happens on receiving "EXIT" from main pgm
 end

 property .... endproperty
endprogram