In a previous article, concepts and components of a simple testbench was discussed. Let us look at a practical SystemVerilog testbench example with all those verification components and how concepts in SystemVerilog has been used to create a reusable environment.

Design

 
// Note that in this protocol, write data is provided
// in a single clock along with the address while read
// data is received on the next clock, and no transactions
// can be started during that time indicated by "ready"
// signal.
 
module reg_ctrl 
  # (
     parameter ADDR_WIDTH   = 8,
     parameter DATA_WIDTH   = 16,
     parameter DEPTH     = 256,
     parameter RESET_VAL    = 16'h1234
  )
  ( input           clk,
    input           rstn,
    input [ADDR_WIDTH-1:0]   addr,
    input           sel,
    input           wr,
    input [DATA_WIDTH-1:0]   wdata,
    output reg [DATA_WIDTH-1:0]   rdata,
    output reg        ready);
 
    // Some memory element to store data for each addr
    reg [DATA_WIDTH-1:0] ctrl [DEPTH];
 
  reg  ready_dly;
  wire ready_pe;
 
  // If reset is asserted, clear the memory element
  // Else store data to addr for valid writes
  // For reads, provide read data back
  always @ (posedge clk) begin
    if (!rstn) begin
      for (int i = 0; i < DEPTH; i += 1) begin
        ctrl[i] <= RESET_VAL;
      end
    end else begin
      if (sel & ready & wr) begin
          ctrl[addr] <= wdata;
      end
 
      if (sel & ready & !wr) begin
          rdata <= ctrl[addr];
      end else begin
          rdata <= 0;
        end
    end
  end
 
  // Ready is driven using this always block
  // During reset, drive ready as 1
  // Else drive ready low for a clock low
  // for a read until the data is given back
  always @ (posedge clk) begin
    if (!rstn) begin
      ready <= 1;
    end else begin
      if (sel & ready_pe) begin
        ready <= 1;
      end
   if (sel & ready & !wr) begin
       ready <= 0;
     end
    end
  end
 
  // Drive internal signal accordingly
  always @ (posedge clk) begin
    if (!rstn) ready_dly <= 1;
       else ready_dly <= ready;
  end
 
   assign ready_pe = ~ready & ready_dly;
endmodule
 

Transaction Object

 
class reg_item;
  // This is the base transaction object that will be used
  // in the environment to initiate new transactions and 
  // capture transactions at DUT interface
  rand   bit [7:0]   addr;
  rand   bit [15:0]   wdata;
      bit [15:0]   rdata;
  rand   bit     wr;
 
  // This function allows us to print contents of the data packet
  // so that it is easier to track in a logfile
  function void print(string tag="");
    $display ("T=%0t [%s] addr=0x%0h wr=%0d wdata=0x%0h rdata=0x%0h", 
                    $time, tag, addr, wr, wdata, rdata);
  endfunction
endclass
 

Driver

 
// The driver is responsible for driving transactions to the DUT 
// All it does is to get a transaction from the mailbox if it is 
// available and drive it out into the DUT interface.
class driver;
  virtual reg_if vif;
  event drv_done;
  mailbox drv_mbx;
 
  task run();
    $display ("T=%0t [Driver] starting ...", $time);
    @ (posedge vif.clk);
 
    // Try to get a new transaction every time and then assign 
    // packet contents to the interface. But do this only if the 
    // design is ready to accept new transactions
    forever begin
      reg_item item;
 
      $display ("T=%0t [Driver] waiting for item ...", $time);
      drv_mbx.get(item);      
    item.print("Driver");
      vif.sel <= 1;
      vif.addr   <= item.addr;
      vif.wr   <= item.wr;
      vif.wdata <= item.wdata;
      @ (posedge vif.clk);
      while (!vif.ready)  begin
        $display ("T=%0t [Driver] wait until ready is high", $time);
        @(posedge vif.clk);
      end
 
      // When transfer is over, raise the done event
      vif.sel <= 0;
      ->drv_done;
    end   
  endtask
endclass
 

Monitor

 
// The monitor has a virtual interface handle with which it can monitor
// the events happening on the interface. It sees new transactions and then
// captures information into a packet and sends it to the scoreboard
// using another mailbox.
class monitor;
  virtual reg_if vif;
  mailbox scb_mbx;     // Mailbox connected to scoreboard
 
  task run();
    $display ("T=%0t [Monitor] starting ...", $time);
 
    // Check forever at every clock edge to see if there is a 
    // valid transaction and if yes, capture info into a class
    // object and send it to the scoreboard when the transaction 
    // is over.
    forever begin
      @ (posedge vif.clk);
      if (vif.sel) begin
        reg_item item = new;
        item.addr = vif.addr;
        item.wr = vif.wr;
        item.wdata = vif.wdata;
 
        if (!vif.wr) begin
          @(posedge vif.clk);
          item.rdata = vif.rdata;
        end
        item.print("Monitor");
        scb_mbx.put(item);
      end
    end
  endtask
endclass
 

Scoreboard

 
// The scoreboard is responsible to check data integrity. Since the design
// stores data it receives for each address, scoreboard helps to check if the
// same data is received when the same address is read at any later point
// in time. So the scoreboard has a "memory" element which updates it
// internally for every write operation.
class scoreboard;
  mailbox scb_mbx;
 
  reg_item refq[256];
 
  task run();
    forever begin
      reg_item item;
      scb_mbx.get(item);
      item.print("Scoreboard");
 
      if (item.wr) begin
        if (refq[item.addr] == null)
          refq[item.addr] = new;
 
        refq[item.addr] = item;
        $display ("T=%0t [Scoreboard] Store addr=0x%0h wr=0x%0h data=0x%0h", $time, item.addr, item.wr, item.wdata);
      end
 
        if (!item.wr) begin
          if (refq[item.addr] == null)
            if (item.rdata != 'h1234)
                $display ("T=%0t [Scoreboard] ERROR! First time read, addr=0x%0h exp=1234 act=0x%0h",
                                              $time, item.addr, item.rdata);
            else
              $display ("T=%0t [Scoreboard] PASS! First time read, addr=0x%0h exp=1234 act=0x%0h",
                                            $time, item.addr, item.rdata);
          else
            if (item.rdata != refq[item.addr].wdata)
              $display ("T=%0t [Scoreboard] ERROR! addr=0x%0h exp=0x%0h act=0x%0h",
                        $time, item.addr, refq[item.addr].wdata, item.rdata);
           else
             $display ("T=%0t [Scoreboard] PASS! addr=0x%0h exp=0x%0h act=0x%0h", 
                       $time, item.addr, refq[item.addr].wdata, item.rdata);
        end
    end
  endtask
endclass
 

Environment

 
// The environment is a container object simply to hold all verification 
// components together. This environment can then be reused later and all
// components in it would be automatically connected and available for use
// This is an environment without a generator.
class env;
  driver       d0;     // Driver to design
  monitor       m0;     // Monitor from design
  scoreboard     s0;     // Scoreboard connected to monitor
  mailbox       scb_mbx;   // Top level mailbox for SCB <-> MON 
  virtual reg_if   vif;     // Virtual interface handle
 
  // Instantiate all testbench components
  function new();
    d0 = new;
    m0 = new;
    s0 = new;
    scb_mbx = new();
  endfunction
 
  // Assign handles and start all components so that 
  // they all become active and wait for transactions to be
  // available
  virtual task run();
    d0.vif = vif;
    m0.vif = vif;
    m0.scb_mbx = scb_mbx;
    s0.scb_mbx = scb_mbx;
 
    fork
      s0.run();
    d0.run();
      m0.run();
    join_any
  endtask
endclass
 

Test

 
// an environment without the generator and hence the stimulus should be 
// written in the test. 
class test;
  env e0;
  mailbox drv_mbx;
 
  function new();
    drv_mbx = new();
    e0 = new();
  endfunction
 
  virtual task run();
    e0.d0.drv_mbx = drv_mbx;
 
    fork
      e0.run();
    join_none
 
    apply_stim();
  endtask
 
  virtual task apply_stim();
    reg_item item;
 
    $display ("T=%0t [Test] Starting stimulus ...", $time);
    item = new;
    item.randomize() with { addr == 8'haa; wr == 1; };
    drv_mbx.put(item);
 
    item = new;
    item.randomize() with { addr == 8'haa; wr == 0; };
    drv_mbx.put(item);
  endtask
endclass
 

Interface

 
// The interface allows verification components to access DUT signals
// using a virtual interface handle
interface reg_if (input bit clk);
  logic rstn;
  logic [7:0] addr;
  logic [15:0] wdata;
  logic [15:0] rdata;
  logic     wr;
  logic     sel;
  logic     ready;
endinterface
 

Testbench Top

 
// Top level testbench contains the interface, DUT and test handles which 
// can be used to start test components once the DUT comes out of reset. Or
// the reset can also be a part of the test class in which case all you need
// to do is start the test's run method.
module tb;
  reg clk;
 
  always #10 clk = ~clk;
  reg_if _if (clk);
 
  reg_ctrl u0 ( .clk (clk),
              .addr (_if.addr),
                 .rstn(_if.rstn),
              .sel  (_if.sel),
                 .wr (_if.wr),
              .wdata (_if.wdata),
              .rdata (_if.rdata),
              .ready (_if.ready));
 
  initial begin
    new_test t0;
 
    clk <= 0;
    _if.rstn <= 0;
    _if.sel <= 0;
    #20 _if.rstn <= 1;
 
    t0 = new;
    t0.e0.vif = _if;
    t0.run();
 
    // Once the main stimulus is over, wait for some time
    // until all transactions are finished and then end 
    // simulation. Note that $finish is required because
    // there are components that are running forever in 
    // the background like clk, monitor, driver, etc
    #200 $finish;
  end
 
  // Simulator dependent system tasks that can be used to 
  // dump simulation waves.
  initial begin
    $dumpvars;
    $dumpfile("dump.vcd");
  end
endmodule
 

Click to try this example in a simulator!   

You may also like:

You consent to our cookies if you continue to use our website. To know more about cookies, see our privacy policy. I accept cookies from this site.

Agree