Basic tutorial of UVM

Article catalogue

1, Basic introduction

Universal Verification Methodology (UVM) has become the verification standard of integrated circuit design. The library built by UVM class promotes the construction of test cases. Each element of UVM test cases is derived from the existing UVM class.
Each class has simulation stages, which are executed in a certain order as the methods of the class. The commonly used UVM stages are as follows:

  • build_phase is responsible for the creation and configuration of test case structure and the construction of component hierarchy.
  • connect_phase is used to connect different subcomponents in a class.
  • run_phase is the main phase in which the simulation is performed.
  • report_phase can be used to display the results of simulation

In order to implement some important methods in classes and variables, UVM provides UVM macros. The most common UVM macros are as follows:

  • uvm_component_utils: when class from UVM_ When the component class is derived, the type of a new class is registered.
  • uvm_object_utils: similar to uvm_component_utils, but the class is from class UVM_ Class derived from object.
  • uvm_field_int: register a variable in the UVM factory. This macro provides functions like copy(), compare(), and print()
  • uvm_info: print messages during simulation in the environment.
  • uvm_error: this macro sends a message with an error log.
  • uvm_fatal: when a fatal error occurs in the simulation, this macro can force the end of the simulation and print a message.

In order to explain the structure of a UVM environment, a simple adder will be used as the test design (DUT) in the test case. The UVM test platform is shown in the figure. DUT is a hardware implementation that interacts with the test bench to verify its function.

In order to realize the simulation of DUT, the sequencer generates a series of data and sends it to the DUT. Since the sequencer sends high-level data packets and the DUT only accepts data from the interface, the driver is used to convert the data packets from the sequencer into signals and send them to the DUT.
The data through the interface needs to be captured for subsequent simulation verification. Since the driver can only realize the conversion from data packet to signal, another module is needed to realize the opposite function of the driver to convert the signal into data packet. monitor is such a module, which collects the interface signals communicated between driver and DUT, converts them into data packets, and finally sends them to the reference model for comparison.
An agent usually consists of three components: a sequencer sequence generator, a driver driver and a monitor monitor. There are two types of agents: Active Agent contains the above three components; Passive Agent only includes the above monitor monitoring and driver drivers. Agent includes build phase function to create hierarchical results, and connect phase function to connect modules.
Reference model (refmod) is an ideal model conceived in the early stage before RTL implementation. Simulate the DUT at a high level of abstraction.
The comparator class is mainly used to compare the data between the reference model and the DUT. The reference model and comparator form a scoreboard to check whether the transmission generated by the DUT is correct. One or more agent s and scoreboards constitute env classes. The test class is responsible for executing the test, creating the environment, and connecting the sequence to the sequence generator. Finally, DUT and testbench are instantiated in top.

2, Learning in practice

Next, we build an adder verification environment to further understand the components in UVM. The function verification platform of adder module is divided into the following modules and classes.

1. interface

The structure of the interface is specially created to encapsulate the communication signals between blocks. modport provides direction information for the module port and controls the use of tasks and functions in a specific module.

interface input_if(input clk, rst);
    logic [31:0] A, B;
    logic valid, ready;
    
    modport port(input clk, rst, A, B, valid, output ready);
endinterface


interface output_if(input clk, rst);
    logic [31:0] data;
    logic valid, ready;
    
    modport port(input clk, rst, output valid, data, ready);
endinterface

2. Design DUT to be tested

Design under test (DUT) is the design code that needs to be verified.

module adder(input_if.port inter, output_if.port out_inter, output state);
    enum logic [1:0] {INITIAL,WAIT,SEND} state;
    
    always_ff @(posedge inter.clk)
        if(inter.rst) begin
            inter.ready <= 0;
            out_inter.data <= 'x;
            out_inter.valid <= 0;
            state <= INITIAL;
        end
        else case(state)
                INITIAL: begin
                    inter.ready <= 1;
                    state <= WAIT;
                end
                
                WAIT: begin
                    if(inter.valid) begin
                        inter.ready <= 0;
                        out_inter.data <= inter.A + inter.B;
                        out_inter.valid <= 1;
                        state <= SEND;
                    end
                end
                
                SEND: begin
                    if(out_inter.ready) begin
                        out_inter.valid <= 0;
                        inter.ready <= 1;
                        state <= WAIT;
                    end
                end
        endcase
endmodule: adder

3. Transmission packet transaction

The structure of transmission data packet is specially created to encapsulate the data communicated between blocks. The field mechanism can register the data in the data packet into the UVM factory and provide functions such as copy(), compare(), print(), and colne() for the data.

class packet_in extends uvm_sequence_item;
    rand integer A;
    rand integer B;

    `uvm_object_utils_begin(packet_in)
        `uvm_field_int(A, UVM_ALL_ON|UVM_HEX)
        `uvm_field_int(B, UVM_ALL_ON|UVM_HEX)
    `uvm_object_utils_end

    function new(string name="packet_in");
        super.new(name);
    endfunction: new
endclass: packet_in


class packet_out extends uvm_sequence_item;
    integer data;

    `uvm_object_utils_begin(packet_out)
        `uvm_field_int(data, UVM_ALL_ON|UVM_HEX)
    `uvm_object_utils_end

    function new(string name="packet_out");
        super.new(name);
    endfunction: new
endclass: packet_out

4. Sequence sequence

A UVM sequence is an object that is used to generate simulated behavior. The UVM sequence is not part of the component hierarchy. Each UVM sequence is eventually bound to a UVM sequencer. Multiple UVM sequence instances can be bound to the same UVM sequencer.

class sequence_in extends uvm_sequence #(packet_in);
    `uvm_object_utils(sequence_in)

    function new(string name="sequence_in");
        super.new(name);
    endfunction: new

    task body;
        packet_in tx;

        forever begin
            tx = packet_in::type_id::create("tx");
            start_item(tx);
            assert(tx.randomize());
            finish_item(tx);
        end
    endtask: body
endclass: sequence_in

5. sequencer

As an arbiter, the UVM sequencer is used to control the transaction flow from multiple simulation sequences. More specifically, the UVM sequencer controls the flow of transactions generated by one or more UVM sequences.

class sequencer extends uvm_sequencer #(packet_in);
    `uvm_component_utils(sequencer)

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

6. driver

The driver receives the transaction generated by the sequence from the sequencer and applies (drives) it on the DUT interface. Therefore, the driver converts the data packet simulation at the transmission level into the simulation at the pin signal level. It also has a TLM interface, which is used to receive the transmission packet transaction from the sequencer to drive the interface signal on the DUT.

typedef virtual input_if input_vif;

class driver extends uvm_driver #(packet_in);
    `uvm_component_utils(driver)
    input_vif vif;
    event begin_record, end_record;

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

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        assert(uvm_config_db#(input_vif)::get(this, "", "vif", vif));
    endfunction

    virtual task run_phase(uvm_phase phase);
        super.run_phase(phase);
        fork
            reset_signals();
            get_and_drive(phase);
            record_tr();
        join
    endtask

    virtual protected task reset_signals();
        wait (vif.rst === 1);
        forever begin
            vif.valid <= '0;
            vif.A <= 'x;
            vif.B <= 'x;
            @(posedge vif.rst);
        end
    endtask

    virtual protected task get_and_drive(uvm_phase phase);
        wait(vif.rst === 1);
        @(negedge vif.rst);
        @(posedge vif.clk);
        
        forever begin
            seq_item_port.get(req);
            -> begin_record;
            drive_transfer(req);
        end
    endtask

    virtual protected task drive_transfer(packet_in tr);
        vif.A = tr.A;
        vif.B = tr.B;
        vif.valid = 1;

        @(posedge vif.clk)
        
        while(!vif.ready)
            @(posedge vif.clk);
        
        -> end_record;
        @(posedge vif.clk); //hold time
        vif.valid = 0;
        @(posedge vif.clk);
    endtask

    virtual task record_tr();
        forever begin
            @(begin_record);
            begin_tr(req, "driver");
            @(end_record);
            end_tr(req);
        end
    endtask
endclass


typedef virtual output_if output_vif;

class driver_out extends uvm_driver #(packet_out);
    `uvm_component_utils(driver_out)
    output_vif vif;

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

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        assert(uvm_config_db#(output_vif)::get(this, "", "vif", vif));
    endfunction

    virtual task run_phase(uvm_phase phase);
        super.run_phase(phase);
        fork
            reset_signals();
            drive(phase);
        join
    endtask

    virtual protected task reset_signals();
        wait (vif.rst === 1);
        forever begin
            vif.ready <= '0;
            @(posedge vif.rst);
        end
    endtask

    virtual protected task drive(uvm_phase phase);
        wait(vif.rst === 1);
        @(negedge vif.rst);
        forever begin
          @(posedge vif.clk);
          vif.ready <= 1;
        end
    endtask
endclass

7. monitor

The monitor samples the DUT interface and captures the information in the transaction to form a transaction. These transmission packets are sent to the UVM test bench for further analysis. Therefore, it is similar to the driver, but the process is opposite. It realizes the conversion of signal collection on the pin signal into transmission data packets.
The UVM monitor can internally perform some processing on the generated transactions (such as coverage collection, inspection, logging, logging, etc.).

class monitor extends uvm_monitor;
    input_vif  vif;
    event begin_record, end_record;
    packet_in tr;
    uvm_analysis_port #(packet_in) item_collected_port;
    `uvm_component_utils(monitor)
   
    function new(string name, uvm_component parent);
        super.new(name, parent);
        item_collected_port = new ("item_collected_port", this);
    endfunction

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        assert(uvm_config_db#(input_vif)::get(this, "", "vif", vif));
        tr = packet_in::type_id::create("tr", this);
    endfunction

    virtual task run_phase(uvm_phase phase);
        super.run_phase(phase);
        fork
            collect_transactions(phase);
            record_tr();
        join
    endtask

    virtual task collect_transactions(uvm_phase phase);
        wait(vif.rst === 1);
        @(negedge vif.rst);
        
        forever begin
            do begin
                @(posedge vif.clk);
            end while (vif.valid === 0 || vif.ready === 0);
            -> begin_record;
            
            tr.A = vif.A;
            tr.B = vif.B;
            item_collected_port.write(tr);

            @(posedge vif.clk);
            -> end_record;
        end
    endtask

    virtual task record_tr();
        forever begin
            @(begin_record);
            begin_tr(tr, "monitor");
            @(end_record);
            end_tr(tr);
        end
    endtask
endclass


class monitor_out extends uvm_monitor;
    `uvm_component_utils(monitor_out)
    output_vif  vif;
    event begin_record, end_record;
    packet_out tr;
    uvm_analysis_port #(packet_out) item_collected_port;
   
    function new(string name, uvm_component parent);
        super.new(name, parent);
        item_collected_port = new ("item_collected_port", this);
    endfunction

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        assert(uvm_config_db#(output_vif)::get(this, "", "vif", vif));
        tr = packet_out::type_id::create("tr", this);
    endfunction

    virtual task run_phase(uvm_phase phase);
        super.run_phase(phase);
        fork
            collect_transactions(phase);
            record_tr();
        join
    endtask

    virtual task collect_transactions(uvm_phase phase);
        wait(vif.rst === 1);
        @(negedge vif.rst);
        
        forever begin
            do begin
                @(posedge vif.clk);
            end while (vif.valid === 0 || vif.ready === 0);
            -> begin_record;
            
            tr.data = vif.data;
            item_collected_port.write(tr);

            @(posedge vif.clk);
            -> end_record;
        end
    endtask

    virtual task record_tr();
        forever begin
            @(begin_record);
            begin_tr(tr, "monitor_out");
            @(end_record);
            end_tr(tr);
        end
    endtask
endclass

8. agent

An agent is a layered component that combines other authentication components that handle a particular DUT interface. A typical agent includes: a sequencer, a UVM sequencer used to manage the simulation flow; A driver for driving the simulated driver on the DUT interface; A monitor is used to monitor the interface of the DUT. Agents may include other components, such as coverage collectors, protocol inspectors, TLM models, and so on.

class agent extends uvm_agent;
    sequencer sqr;
    driver    drv;
    monitor   mon;

    uvm_analysis_port #(packet_in) item_collected_port;

    `uvm_component_utils(agent)

    function new(string name = "agent", uvm_component parent = null);
        super.new(name, parent);
        item_collected_port = new("item_collected_port", this);
    endfunction

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon = monitor::type_id::create("mon", this);
        sqr = sequencer::type_id::create("sqr", this);
        drv = driver::type_id::create("drv", this);
    endfunction

    virtual function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        mon.item_collected_port.connect(item_collected_port);
        drv.seq_item_port.connect(sqr.seq_item_export);
    endfunction
endclass: agent


class agent_out extends uvm_agent;
    driver_out    drv;
    monitor_out   mon;

    uvm_analysis_port #(packet_out) item_collected_port;

    `uvm_component_utils(agent_out)

    function new(string name = "agent_out", uvm_component parent = null);
        super.new(name, parent);
        item_collected_port = new("item_collected_port", this);
    endfunction

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon = monitor_out::type_id::create("mon_out", this);
        drv = driver_out::type_id::create("drv_out", this);
    endfunction

    virtual function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        mon.item_collected_port.connect(item_collected_port);
    endfunction
endclass: agent_out

9. scoreboard

The main function of the scoreboard is to check whether the behavior of the DUT is correct. The actual data packet received by the scoreboard comes from the input and output of the agent sent to the DUT, and the expected data packet received comes from the reference model. Finally, the actual data packet and the expected data packet are compared in the scoreboard.

class comparator #(type T = packet_out) extends uvm_scoreboard;
  typedef comparator #(T) this_type;
  `uvm_component_param_utils(this_type)

  const static string type_name = "comparator #(T)";

  uvm_put_imp #(T, this_type) from_refmod;
  uvm_analysis_imp #(T, this_type) from_dut;

  typedef uvm_built_in_converter #( T ) convert; 

  int m_matches, m_mismatches;
  T exp;
  bit free;
  event compared, end_of_simulation;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    from_refmod = new("from_refmod", this);
    from_dut = new("from_dut", this);
    m_matches = 0;
    m_mismatches = 0;
    exp = new("exp");
    free = 1;
  endfunction

  virtual function string get_type_name();
    return type_name;
  endfunction

  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    @(end_of_simulation);
    phase.drop_objection(this);
  endtask

  virtual task put(T t);
    if(!free) @compared;
    exp.copy(t);
    free = 0;
    
    @compared;
    free = 1;
  endtask

  virtual function bit try_put(T t);
    if(free) begin
      exp.copy(t);
      free = 0;
      return 1;
    end
    else return 0;
  endfunction

  virtual function bit can_put();
    return free;
  endfunction

  virtual function void write(T rec);
    if (free)
      uvm_report_fatal("No expect transaction to compare with", "");
    
    if(!(exp.compare(rec))) begin
      uvm_report_warning("Comparator Mismatch", "");
      m_mismatches++;
    end
    else begin
      uvm_report_info("Comparator Match", "");
      m_matches++;
    end
    
    if(m_matches+m_mismatches > 100)
      -> end_of_simulation;
    
    -> compared;
  endfunction
endclass

10. Simulation environment env

Simulation environment env is a layered component that combines other interrelated verification components. Typically, the typical components instantiated in the simulation environment env are agents, scoreboards, and even other simulation environments.
The top-level simulation environment encapsulates all verification components for DUT.

class env extends uvm_env;
    agent       mst;
    refmod      rfm;
    agent_out   slv;
    comparator #(packet_out) comp;  
    uvm_tlm_analysis_fifo #(packet_in) to_refmod;

    `uvm_component_utils(env)

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

    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mst = agent::type_id::create("mst", this);
        slv = agent_out::type_id::create("slv", this);
        rfm = refmod::type_id::create("rfm", this);
        comp = comparator#(packet_out)::type_id::create("comp", this);
    endfunction

    virtual function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        // Connect MST to FIFO
        mst.item_collected_port.connect(to_refmod.analysis_export);
        
        // Connect FIFO to REFMOD
        rfm.in.connect(to_refmod.get_export);
        
        //Connect scoreboard
        rfm.out.connect(comp.from_refmod);
        slv.item_collected_port.connect(comp.from_dut);
    endfunction

    virtual function void end_of_elaboration_phase(uvm_phase phase);
        super.end_of_elaboration_phase(phase);
    endfunction
  
    virtual function void report_phase(uvm_phase phase);
        super.report_phase(phase);
        `uvm_info(get_type_name(), $sformatf("Reporting matched %0d", comp.m_matches), UVM_NONE)
        if (comp.m_mismatches) begin
            `uvm_error(get_type_name(), $sformatf("Saw %0d mismatched samples", comp.m_mismatches))
        end
    endfunction
endclass

11. Test case test

Test cases are the top-level components in the test bench. Test cases usually perform three main functions: instantiation of top level environment, configuration environment (through factory coverage or configuration database), and simulation by calling sequences in the environment.
Typically, there is a basic UVM test with UVM environment instantiation and other common items. Then, other separate tests will extend this basic test and configure the environment in different ways or choose different sequences to run.
Test cases are dynamically instantiated at run time, allowing the UVM test bench to compile only once and run with many different tests.

class simple_test extends uvm_test;
  env env_h;
  sequence_in seq;

  `uvm_component_utils(simple_test)

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

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env_h = env::type_id::create("env_h", this);
    seq = sequence_in::type_id::create("seq", this);
  endfunction
 
  task run_phase(uvm_phase phase);
    seq.start(env_h.mst.sqr);
  endtask: run_phase

endclass

12. top floor

The top layer mainly instantiates DUT modules and test classes and configures the connection between them.

import uvm_pkg::*;
`include "uvm_macros.svh"
`include "./input_if.sv"
`include "./output_if.sv"
`include "./adder.sv"
`include "./packet_in.sv"
`include "./packet_out.sv"
`include "./sequence_in.sv"
`include "./sequencer.sv"
`include "./driver.sv"
`include "./driver_out.sv"
`include "./monitor.sv"
`include "./monitor_out.sv"
`include "./agent.sv"
`include "./agent_out.sv"
`include "./refmod.sv"
`include "./comparator.sv"
`include "./env.sv"
`include "./simple_test.sv"

//Top
module top;
  logic clk;
  logic rst;
  
  initial begin
    clk = 0;
    rst = 1;
    #22 rst = 0;
    
  end
  
  always #5 clk = !clk;
  
  logic [1:0] state;
  
  input_if in(clk, rst);
  output_if out(clk, rst);
  
  adder sum(in, out, state);

  initial begin
    `ifdef INCA
       $recordvars();
    `endif
    `ifdef VCS
        //$vcdpluson;
        $fsdbDumpfile("test.fsdb");
    	$fsdbDumpSVA();
    	$fsdbDumpvars();
        $fsdbDumpMDA();
    `endif
    `ifdef QUESTA
       $wlfdumpvars();
       set_config_int("*", "recording_detail", 1);
    `endif
    
    uvm_config_db#(input_vif)::set(uvm_root::get(), "*.env_h.mst.*", "vif", in);
    uvm_config_db#(output_vif)::set(uvm_root::get(), "*.env_h.slv.*",  "vif", out);
    
    run_test("simple_test");
  end
endmodule

13. reference mode and direct programming interface (DPI)

SystemVerilog direct programming interface (DPI) is the interface for SystemVerilog to call functions of external languages (such as c, c + +). DPI consists of two layers: SystemVerilog layer and foreign language layer, which are isolated from each other. The code of refmod is given below to illustrate the usage of DPI. The sum() function is in the file external CPP defines that once you call it in refmod, you should add the keyword "external C" before the definition of sum () function.

#include 
#include 

extern "C" int sum(int a, int b){
  return a+b;
}


import "DPI-C" context function int sum(int a, int b);

class refmod extends uvm_component;
    `uvm_component_utils(refmod)
    
    packet_in tr_in;
    packet_out tr_out;
    integer a, b;
    uvm_get_port #(packet_in) in;
    uvm_put_port #(packet_out) out;
    
    function new(string name = "refmod", uvm_component parent);
        super.new(name, parent);
        in = new("in", this);
        out = new("out", this);
    endfunction
    
    virtual function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        tr_out = packet_out::type_id::create("tr_out", this);
    endfunction: build_phase
    
    virtual task run_phase(uvm_phase phase);
        super.run_phase(phase);
        
        forever begin
            in.get(tr_in);
            tr_out.data = sum(tr_in.A, tr_in.B);
            out.put(tr_out);
        end
    endtask: run_phase
endclass: refmod

14. Simple makefile

comp: clean
	g++ -c external.cpp -o external.o
	vcs -full64  -sverilog top.sv -dpi -ntb_opts uvm -debug_pp -timescale=1ns/10ps  external.o -debug_access+cbk -lca -cm tgl+line+fsm+cond+branch

sim:
	./simv +UVM_TR_RECORD +UVM_VERBOSITY=HIGH +UVM_TESTNAME=simple_test -lca -cm tgl+line+fsm+cond+branch

all: comp sim

dbg: clean
	g++ -c external.cpp -o external.o
	vcs -full64  -sverilog top.sv -dpi -ntb_opts uvm -debug_pp -timescale=1ns/10ps  external.o -debug_access+cbk -debug_access+all -kdb -lca
	$ ./simv +UVM_TR_RECORD +UVM_VERBOSITY=HIGH +UVM_TESTNAME=simple_test -gui=verdi

clean:
	rm -rf DVEfiles csrc simv simv.daidir ucli.key .vlogansetup.args .vlogansetup.env .vcs_lib_lock simv.vdb AN.DB vc_hdrs.h *.diag *.vpd *tar.gz external.o inter.fsdb novas.conf novas_dump.log novas.rc test.fsdb verdiLog

view_waves:
	dve &

verdi:
	verdi -sverilog -ntb_opts uvm-1.2 -timescale=1ns/10ps top.sv -f filelist.f +incdir+./ -ssf ./test.fsdb &

urg_gui:
	urg -dir *.vdb -format both -report urgReportALL; \
firefox urgReportALL/tests.html &

comp function first compiles the C program through g + +, and then compiles through VCS, where - debug_ The access + CBK option enables PLI based callbacks for static networks, registers, and variables- cm tgl+line+fsm+cond+branch option enables code turnover coverage, line coverage, state machine coverage, condition coverage and branch coverage.
The sim function performs VCS simulation.
The dbg function is used for the joint debugging of VCS and verdi, and - debug is added in the compilation_ Access + all - KDB – lca option, and - gui=verdi option is added in the simulation.
It is used to start the verdi function.
urg_ The GUI function is used to view code coverage.

Code attachment

The download link of the above complete code is as follows:
https://github.com/AFEI1100/easyUVM-master.git

---------------------
Author: hh199203
Source: CSDN
Original text: https://blog.csdn.net/hh199203/article/details/110426339
Copyright notice: This article is the author's original article. Please attach the blog link for reprint!
Content resolution By: CSDN,CNBLOG blog article one click reprint plug-in

Added by brainstem on Wed, 23 Feb 2022 15:41:33 +0200