UVM TestBench architecture

UVM TestBecnh example code

UVM TestBench to verify Memory Model

For Design specification and Verification plan, refer to Memory Model.

UVM TestBench architecture
UVM TestBench architecture

To maintain uniformity in naming the components/objects, all the component/object name’s are starts with mem_*.

TestBench Components/Objects

Sequence item

  • fields required to generate the stimulus are declared in the sequence_item.
  • sequence_item can be used as a placeholder for the activity monitored by the monitor on DUT signals.

1. sequence_item is written by extending uvm_seq_item;

class mem_seq_item extends uvm_sequence_item;  

  //Utility macro
  `uvm_object_utils(mem_seq_item)  

  //Constructor
  function new(string name = "mem_seq_item");
    super.new(name);
  endfunction

endclass

2. Declaring the fields in mem_seq_item,

class mem_seq_item extends uvm_sequence_item; 

  //data and control fields
  bit [3:0] addr;
  bit       wr_en;
  bit       rd_en;
  bit [7:0] wdata;
  bit [7:0] rdata;

  //Utility macro
  `uvm_object_utils(mem_seq_item) 

  //Constructor
  function new(string name = "mem_seq_item");
    super.new(name);
  endfunction
endclass

3. To generate the random stimulus, declare the fields as rand.

class mem_seq_item extends uvm_sequence_item; 

  //data and control fields
  rand bit [3:0] addr;
  rand bit       wr_en;
  rand bit       rd_en;
  rand bit [7:0] wdata;
       bit [7:0] rdata;

  //Utility macro
  `uvm_object_utils(mem_seq_item) 

  //Constructor
  function new(string name = "mem_seq_item");
    super.new(name);
  endfunction
endclass

4. In order to use the uvm_object methods ( copy, compare, pack, unpack, record, print, and etc ),
all the fields are registered to uvm_field_* macros.

class mem_seq_item extends uvm_sequence_item;
  //data and control fields
  rand bit [3:0] addr;
  rand bit       wr_en;
  rand bit       rd_en;
  rand bit [7:0] wdata;
       bit [7:0] rdata;
  
  //Utility and Field macros,
  `uvm_object_utils_begin(mem_seq_item)
    `uvm_field_int(addr,UVM_ALL_ON)
    `uvm_field_int(wr_en,UVM_ALL_ON)
    `uvm_field_int(rd_en,UVM_ALL_ON)
    `uvm_field_int(wdata,UVM_ALL_ON)
  `uvm_object_utils_end
  
  //Constructor
  function new(string name = "mem_seq_item");
    super.new(name);
  endfunction
  
endclass

5. Either write or read operation will be performed at once, so the constraint is added to generate wr_en and rd_en.

class mem_seq_item extends uvm_sequence_item;
  //data and control fields
  rand bit [3:0] addr;
  rand bit       wr_en;
  rand bit       rd_en;
  rand bit [7:0] wdata;
       bit [7:0] rdata;
  
  //Utility and Field macros,
  `uvm_object_utils_begin(mem_seq_item)
    `uvm_field_int(addr,UVM_ALL_ON)
    `uvm_field_int(wr_en,UVM_ALL_ON)
    `uvm_field_int(rd_en,UVM_ALL_ON)
    `uvm_field_int(wdata,UVM_ALL_ON)
  `uvm_object_utils_end
  
  //Constructor
  function new(string name = "mem_seq_item");
    super.new(name);
  endfunction
  
  //constaint, to generate any one among write and read
  constraint wr_rd_c { wr_en != rd_en; }; 
  
endclass

Complete mem_seq_item code,

class mem_seq_item extends uvm_sequence_item;
  //data and control fields
  rand bit [3:0] addr;
  rand bit       wr_en;
  rand bit       rd_en;
  rand bit [7:0] wdata;
       bit [7:0] rdata;
  
  //Utility and Field macros,
  `uvm_object_utils_begin(mem_seq_item)
    `uvm_field_int(addr,UVM_ALL_ON)
    `uvm_field_int(wr_en,UVM_ALL_ON)
    `uvm_field_int(rd_en,UVM_ALL_ON)
    `uvm_field_int(wdata,UVM_ALL_ON)
  `uvm_object_utils_end
  
  //Constructor
  function new(string name = "mem_seq_item");
    super.new(name);
  endfunction
  
  //constaint, to generate any one among write and read
  constraint wr_rd_c { wr_en != rd_en; }; 
  
endclass

Sequence

  • Sequence generates the stimulus and sends to driver via sequencer.
  • An agent can have any number of sequences.

1. A sequence is written by extending the uvm_sequence

class mem_sequence extends uvm_sequence#(mem_seq_item);
  
  `uvm_sequence_utils(mem_sequence,mem_sequencer)
  
  //Constructor
  function new(string name = "mem_sequence");
    super.new(name);
  endfunction
  
endclass

2. Logic to generate and send the sequence_item is added inside the body() method

class mem_sequence extends uvm_sequence#(mem_seq_item);
  
  `uvm_sequence_utils(mem_sequence,mem_sequencer)
  
  //Constructor
  function new(string name = "mem_sequence");
    super.new(name);
  endfunction
  
  virtual task body();

    req = mem_seq_item::type_id::create("req");
    wait_for_grant();
    req.randomize();
    send_request(req);
    wait_for_item_done();

  endtask
  
endclass

write sequence

class mem_wr_seq extends uvm_sequence#(mem_seq_item);
  
  `uvm_object_utils(mem_wr_seq)
   
  //Constructor
  function new(string name = "mem_wr_seq");
    super.new(name);
  endfunction
  
  virtual task body();
    `uvm_do_with(req,{req.wr_en == 1;})
  endtask
  
endclass

read sequence

class mem_rd_seq extends uvm_sequence#(mem_seq_item);
  
  `uvm_object_utils(mem_rd_seq)
   
  //Constructor
  function new(string name = "mem_rd_seq");
    super.new(name);
  endfunction
  
  virtual task body();
    `uvm_do_with(req,{req.rd_en == 1;})
  endtask
  
endclass

Sequencer

Sequencer is written by extending uvm_sequencer, there is no extra logic required to be added in the sequencer.

class mem_sequencer extends uvm_sequencer#(mem_seq_item);

  `uvm_component_utils(mem_sequencer) 

  //constructor
  function new(string name, uvm_component parent);
    super.new(name,parent);
  endfunction
  
endclass

Driver

driver receives the stimulus from sequence via sequencer and drives on interface signals.
1. driver is written by extending the uvm_driver

class mem_driver extends uvm_driver #(mem_seq_item);

  `uvm_component_utils(mem_driver)

  // Constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

endclass : mem_driver

2. Declare the virtual interface,

  // Virtual Interface
  virtual mem_if vif;

3. Get the interface handle using get config_db,

if(!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
       `uvm_fatal("NO_VIF",{"virtual interface must be set for: ",get_full_name(),".vif"});

4. adding the get config_db in the build_phase,

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
     if(!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
       `uvm_fatal("NO_VIF",{"virtual interface must be set for: ",get_full_name(),".vif"});
  endfunction: build_phase

5. Add driving logic. get the seq_item and drive to DUT signals,

  // run phase
  virtual task run_phase(uvm_phase phase);
    forever begin
    seq_item_port.get_next_item(req);
     ......
     .. driving logic ..
     ......
    seq_item_port.item_done();
    end
  endtask : run_phase

Complete driver code,

class mem_driver extends uvm_driver #(mem_seq_item);

  // Virtual Interface
  virtual mem_if vif;

  `uvm_component_utils(mem_driver)
    
  //uvm_analysis_port #(mem_seq_item) Drvr2Sb_port;

  // Constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
     if(!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
       `uvm_fatal("NO_VIF",{"virtual interface must be set for: ",get_full_name(),".vif"});
  endfunction: build_phase

  // run phase
  virtual task run_phase(uvm_phase phase);
    forever begin
    seq_item_port.get_next_item(req);
    //respond_to_transfer(req);
    drive();
    seq_item_port.item_done();
    end
  endtask : run_phase

  // drive 
  virtual task drive();
    req.print();
      `DRIV_IF.wr_en <= 0;
      `DRIV_IF.rd_en <= 0;
      @(posedge vif.DRIVER.clk);
      `DRIV_IF.addr <= req.addr;
    if(req.wr_en) begin
        `DRIV_IF.wr_en <= req.wr_en;
        `DRIV_IF.wdata <= req.wdata;
      //$display("\tADDR = %0h \tWDATA = %0h",req.addr,trans.wdata);
        @(posedge vif.DRIVER.clk);
      end
    if(req.rd_en) begin
        `DRIV_IF.rd_en <= req.rd_en;
        @(posedge vif.DRIVER.clk);
        `DRIV_IF.rd_en <= 0;
        @(posedge vif.DRIVER.clk);
        req.rdata = `DRIV_IF.rdata;
       // $display("\tADDR = %0h \tRDATA = %0h",trans.addr,`DRIV_IF.rdata);
      end
      $display("-----------------------------------------");
  endtask : drive

endclass : mem_driver

Monitor

  • Monitor samples the DUT signals through the virtual interface and converts the signal level activity to the transaction level.

1. The monitor is written by extending the uvm_monitor

class mem_monitor extends uvm_monitor;

  `uvm_component_utils(mem_monitor)

  // new - constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

endclass : mem_monitor

2. Declare virtual interface,

  // Virtual Interface
  virtual mem_if vif;

3. Connect interface to Virtual interface by using get method,

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
       `uvm_fatal("NOVIF",{"virtual interface must be set for: ",get_full_name(),".vif"});
  endfunction: build_phase

4. Declare analysis port,

  uvm_analysis_port #(mem_seq_item) item_collected_port;

5. Declare seq_item handle, Used as a place holder for sampled signal activity,

  mem_seq_item trans_collected;

6. Add Sampling logic in run_phase,

  • sample the interface signal and assign to trans_collected handle
  • sampling logic is placed in the forever loop
  // run phase
  virtual task run_phase(uvm_phase phase);
    forever begin
      //sampling logic     
        @(posedge vif.MONITOR.clk);
        wait(vif.monitor_cb.wr_en || vif.monitor_cb.rd_en);
        trans_collected.addr = vif.monitor_cb.addr;
      if(vif.monitor_cb.wr_en) begin
        trans_collected.wr_en = vif.monitor_cb.wr_en;
        trans_collected.wdata = vif.monitor_cb.wdata;
        trans_collected.rd_en = 0;
        @(posedge vif.MONITOR.clk);
      end
      if(vif.monitor_cb.rd_en) begin
        trans_collected.rd_en = vif.monitor_cb.rd_en;
        trans_collected.wr_en = 0;
        @(posedge vif.MONITOR.clk);
        @(posedge vif.MONITOR.clk);
        trans_collected.rdata = vif.monitor_cb.rdata;
end
    end  
  endtask : run_phase

7. After sampling, by using the write method send the sampled transaction packet to the scoreboard,

  item_collected_port.write(trans_collected);

Complete monitor code,

class mem_monitor extends uvm_monitor;

  // Virtual Interface
  virtual mem_if vif;

  uvm_analysis_port #(mem_seq_item) item_collected_port;

  // Placeholder to capture transaction information.
  mem_seq_item trans_collected;

  `uvm_component_utils(mem_monitor)

  // new - constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
    trans_collected = new();
    item_collected_port = new("item_collected_port", this);
  endfunction : new

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!uvm_config_db#(virtual mem_if)::get(this, "", "vif", vif))
       `uvm_fatal("NOVIF",{"virtual interface must be set for: ",get_full_name(),".vif"});
  endfunction: build_phase

  // run phase
  virtual task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.MONITOR.clk);
      wait(vif.monitor_cb.wr_en || vif.monitor_cb.rd_en);
        trans_collected.addr = vif.monitor_cb.addr;
      if(vif.monitor_cb.wr_en) begin
        trans_collected.wr_en = vif.monitor_cb.wr_en;
        trans_collected.wdata = vif.monitor_cb.wdata;
        trans_collected.rd_en = 0;
        @(posedge vif.MONITOR.clk);
      end
      if(vif.monitor_cb.rd_en) begin
        trans_collected.rd_en = vif.monitor_cb.rd_en;
        trans_collected.wr_en = 0;
        @(posedge vif.MONITOR.clk);
        @(posedge vif.MONITOR.clk);
        trans_collected.rdata = vif.monitor_cb.rdata;
      end
      item_collected_port.write(trans_collected);
    end 
  endtask : run_phase

endclass : mem_monitor

Agent

An agent is a container class contains a driver, a sequencer, and a monitor.

1. agent is written by extending the uvm_agent,

class mem_agent extends uvm_agent;

  // UVM automation macros for general components
  `uvm_component_utils(mem_agent)

  // constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

endclass : mem_agent

2. Declare driver, sequencer and monitor instance,

  //declaring agent components
  mem_driver    driver;
  mem_sequencer sequencer;
  mem_monitor   monitor;

3. Depending on Agent type, create agent components in the build phase,

driver and sequencer will be created only for the active agent.

  // build_phase
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    if(get_is_active() == UVM_ACTIVE) begin
      driver    = mem_driver::type_id::create("driver", this);
      sequencer = mem_sequencer::type_id::create("sequencer", this);
    end

    monitor = mem_monitor::type_id::create("monitor", this);
  endfunction : build_phase

4. Connect the driver seq_item_port to sequencer seq_item_export for communication between driver and sequencer in the connect phase

  // connect_phase
  function void connect_phase(uvm_phase phase);
    if(get_is_active() == UVM_ACTIVE) begin
      driver.seq_item_port.connect(sequencer.seq_item_export);
    end
  endfunction : connect_phase

Complete Agent code,

class mem_agent extends uvm_agent;
  //declaring agent components
  mem_driver    driver;
  mem_sequencer sequencer;
  mem_monitor   monitor;

  // UVM automation macros for general components
  `uvm_component_utils(mem_agent)

  // constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

  // build_phase
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    if(get_is_active() == UVM_ACTIVE) begin
      driver = mem_driver::type_id::create("driver", this);
      sequencer = mem_sequencer::type_id::create("sequencer", this);
    end

    monitor = mem_monitor::type_id::create("monitor", this);
  endfunction : build_phase

  // connect_phase
  function void connect_phase(uvm_phase phase);
    if(get_is_active() == UVM_ACTIVE) begin
      driver.seq_item_port.connect(sequencer.seq_item_export);
    end
  endfunction : connect_phase

endclass : mem_agent

Scoreboard

scoreboard receives the transaction from the monitor and compares it with the reference values.

1. The scoreboard is written by extending uvm_scoreboard,

class mem_scoreboard extends uvm_scoreboard;

  `uvm_component_utils(mem_scoreboard)

  // new - constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

endclass : mem_scoreboard

2. Declare and Create TLM Analysis port, ( to receive transaction pkt from Monitor),

//Declaring port
uvm_analysis_imp#(mem_seq_item, mem_scoreboard) item_collected_export;

//creating port
item_collected_export = new("item_collected_export", this);

3. The analysis export of Scoreboard is connected to the Monitor port. (Connection is done in environment connect phase)

monitor.item_collected_port.connect(scoreboard.item_collected_export);

4. write method of the scoreboard will receive the transaction packet from the monitor, on calling write method from the monitor

UVM Monitor Scoreboard connection
UVM Monitor Scoreboard connection
 
  //calling write method from monitor
  item_collected_port.write(pkt);
 
  //scoreboard write function
  virtual function void write(mem_seq_item pkt);
    pkt.print();
  endfunction : write

6. Add Sampling logic in run_phase,

  // run phase
  virtual task run_phase(uvm_phase phase);
    --- comparision logic ---    
  endtask : run_phase

Complete scoreboard code

class mem_scoreboard extends uvm_scoreboard;

  `uvm_component_utils(mem_scoreboard)
  uvm_analysis_imp#(mem_seq_item, mem_scoreboard) item_collected_export;

  // new - constructor
  function new (string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    item_collected_export = new("item_collected_export", this);
  endfunction: build_phase
  
  // write
  virtual function void write(mem_seq_item pkt);
    $display("SCB:: Pkt recived");
    pkt.print();
  endfunction : write

  // run phase
  virtual task run_phase(uvm_phase phase);
    --- comparision logic ---    
  endtask : run_phase
endclass : mem_scoreboard

Environment/env

The environment is the container class, It contains one or more agents, as well as other components such as the scoreboard, top-level monitor, and checker.

1. The environment is written by extending the uvm_env,

class mem_model_env extends uvm_env;
  
  `uvm_component_utils(mem_model_env)
    
  // new - constructor
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

endclass : mem_model_env

2. Declare the agent and scoreboard,

  mem_agent      mem_agnt;
  mem_scoreboard mem_scb;

3. Create agent and scoreboard,

  mem_agnt = mem_agent::type_id::create("mem_agnt", this);
  mem_scb  = mem_scoreboard::type_id::create("mem_scb", this);

3. Connecting monitor port to scoreboard port,

  mem_agnt.monitor.item_collected_port.connect(mem_scb.item_collected_export);

Complete environment code,

class mem_model_env extends uvm_env;
  
  //---------------------------------------
  // agent and scoreboard instance
  //---------------------------------------
  mem_agent      mem_agnt;
  mem_scoreboard mem_scb;
  
  `uvm_component_utils(mem_model_env)
  
  //--------------------------------------- 
  // constructor
  //---------------------------------------
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction : new

  //---------------------------------------
  // build_phase - crate the components
  //---------------------------------------
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    mem_agnt = mem_agent::type_id::create("mem_agnt", this);
    mem_scb  = mem_scoreboard::type_id::create("mem_scb", this);
  endfunction : build_phase
  
  //---------------------------------------
  // connect_phase - connecting monitor and scoreboard port
  //---------------------------------------
  function void connect_phase(uvm_phase phase);
    mem_agnt.monitor.item_collected_port.connect(mem_scb.item_collected_export);
  endfunction : connect_phase

endclass : mem_model_env

Test

The test defines the test scenario for the testbench.

1. test is written by extending the uvm_test,

class mem_model_test extends uvm_test;

  `uvm_component_utils(mem_model_test)

  function new(string name = "mem_model_test",uvm_component parent=null);
    super.new(name,parent);
  endfunction : new

endclass : mem_model_test

2. Declare env and sequence,

  mem_model_env env;
  mem_sequence  seq;

3. Create env and sequence,

  env = mem_model_env::type_id::create("env",this);
  seq = mem_sequence::type_id::create("seq");

4. Start sequence,

  seq.start(env.mem_agnt.sequencer);

Complete Test code,

class mem_model_test extends uvm_test;

  `uvm_component_utils(mem_model_test)

  mem_model_env env;
  mem_sequence  seq;

  function new(string name = "mem_model_test",uvm_component parent=null);
    super.new(name,parent);
  endfunction : new

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    env = mem_model_env::type_id::create("env", this);
    seq = mem_sequence::type_id::create("seq");
  endfunction : build_phase

  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
      seq.start(env.mem_agnt.sequencer);
    phase.drop_objection(this);
  endtask : run_phase

endclass : mem_model_test

TestBench_Top

TestBench top is the module, it connects the DUT and Verification environment components.

Testbench_top contains,

  • DUT instance
  • interface instance
  • run_test() method
  • virtual interface set config_db
  • clock and reset generation logic
  • wave dump logic

run test

uvm run test
uvm run test
module tbench_top;
  
  //clock and reset signal declaration
  bit clk;
  bit reset;
  
  //clock generation
  always #5 clk = ~clk;
  
  //reset Generation
  initial begin
    reset = 1;
    #5 reset =0;
  end
  
  //creatinng instance of interface, inorder to connect DUT and testcase
  mem_if intf(clk,reset);
  
  //DUT instance, interface signals are connected to the DUT ports
  memory DUT (
    .clk(intf.clk),
    .reset(intf.reset),
    .addr(intf.addr),
    .wr_en(intf.wr_en),
    .rd_en(intf.rd_en),
    .wdata(intf.wdata),
    .rdata(intf.rdata)
   );
  
  //enabling the wave dump
  initial begin 
    uvm_config_db#(virtual mem_if)::set(uvm_root::get(),"*","vif",intf);
    $dumpfile("dump.vcd"); $dumpvars;
  end
  
  initial begin 
    run_test();
  end
endmodule

Edit and Execute the Memory Model UVM TestBench code in EDA Playground.

Click to execute on   

❮ Previous Next ❯

UVM Testbench

UVM TestBench

UVM testbenches are constructed by extending uvm classes.

UVM testbench hierarchy

Below is the typical UVM testbench hierarchy diagram.

UVM TestBench Hierarchy
UVM TestBench Hierarchy

Role of each testbench element is explained below,

UVM test

The test is the topmost class. the test is responsible for,

  • configuring the testbench.
  • Initiate the testbench components construction process by building the next level down in the hierarchy ex: env.
  • Initiate the stimulus by starting the sequence.

UVM Environment

Env or environment: The environment is a container component for grouping higher level components like agent’s and scoreboard.

UVM Agent

UVM agent groups the uvm_components specific to an interface or protocol.
example: groups the components associated with BFM(Bus Functional Model).\
The components of an agent are,

UVM Sequence item

The sequence-item defines the pin level activity generated by agent (to drive to DUT through the driver) or the activity has to be observed by agent (Placeholder for the activity monitored by the monitor on DUT signals).

UVM Driver

Responsible for driving the packet level data inside sequence_item into pin level (to DUT).

UVM Sequence

Defines the sequence in which the data items need to be generated and sent/received to/from the driver.

UVM Sequencer

Responsible for routing the data packet’s(sequence_item) generated in sequence to the driver or vice verse.

UVM Monitor

Observes pin level activity on interface signals and converts into packet level which is sent to components such as scoreboards.

UVM Scoreboard

Receives data item’s from monitor’s and compares with expected values.

expected values can be either golden reference values or generated from the reference model.
For a detailed explanation of each component and methods refer to UVM Tutorial.

UVM TestBench Block Diagram

  • UVM TestBench Block Diagram with a single agent.
UVM TestBench block diagram
UVM TestBench block diagram
  • UVM TestBench Block Diagram with multiple agents and multiple instances of each
UVM TestBench diagram
UVM TestBench diagram

❮ Previous Next ❯