Global training solutions for engineers creating the world's electronics

From OVM to UVM: Getting Started with UVM - A First Example

John Aynsley, Doulos, March 2011

Now updated for UVM 1.0

From OVM to UVM

UVM is based on OVM, so from the outset it should be very straightforward to interoperate between OVM and UVM or to convert old OVM code to UVM code. We thought we would test this out by converting our existing online tutorial Getting Started with OVM - A First Example to UVM. You can see the results of this conversion below.

The conversion process from OVM to UVM was very straightforward. Just change to the directory containing your OVM code, which could be contained in subdirectories, and run the script provided with the UVM 1.0 EA release:

perl $UVM_HOME/bin/OVM_UVM_Rename.pl

Tada! Every occurrence of "ovm_" gets replaced by "uvm_" in files with extensions .v .vh .sv .svh in all subdirectories, but only if you are running under Linux. The provided script does not work out-of-the-box under Windows (even with Perl properly installed).

Another gotcha is that the Perl script does not touch script files, which typically also need to be edited to update the paths to the UVM installation, and to replace "+OVM_TESTNAME=" with "+UVM_TESTNAME=".

Having worked around the above gotchas, the converted SystemVerilog source file ran first time under UVM. Cool! The tutorial below also includes a few minor updates to bring it into line with current tool capabilities and practices. And notice that this time the very same code runs on Cadence, Mentor, and Synopsys without any hacks. Yay!

From UVM-EA to UVM-1.0

The example was converted again upon the release of UVM 1.0 in February 2011. Here is a list of the changes required to convert from UVM-EA to fully compliant UVM-1.0 code:

  • Rename the standard phase methods by appending _phase to each name
  • Add the argument (uvm_phase phase) to each of the standard phase methods
  • Don't forget to change the calls to super.build_phase(phase);
  • Replace the old sequencer macro uvm_sequencer_utils with uvm_component_utils
  • Do not use the deprecated uvm_update_sequence_lib(_and_item). Start sequences with start
  • Replace calls to set/get_config_int/string/object with the new uvm_config_db or uvm_resource_db
  • Replace set_global_timeout with calls to raise_objection and drop_objection
  • Replace uvm_top.stop_timeout, which no longer exists
  • When using uvm_class_comparator, override do_compare instead of comp

Introduction to this UVM tutorial

In this tutorial, the emphasis is on getting a simple example working rather than on understanding the broad flow of the constrained random verification process and verification component reuse.

Compiling UVM Code

The UVM installation contains four top level directories: ./bin, ./docs, ./examples and ./src.

The ./bin directory contains the Perl script used to convert from OVM to UVM.

The ./docs directory contains the UVM Class Reference Guide in HTML format and the UVM User Guide in PDF format.

The ./examples directory in the UVM release contains sample script files for Cadence, Mentor and Synopsys simulators that can be modified to compile this tutorial example.

The source code is structured into subdirectories, but you can ignore this for now. In order to gain access to the UVM installation from your SystemVerilog source code, do the following:

  • add ./src to the include path on the command line, that is, +incdir+/.../src
  • add ./src/uvm_pkg.sv to the front of the list of files being compiled
  • add the following directives to your various SystemVerilog files

`include "uvm_macros.svh"
import uvm_pkg::*;

The Verification Environment

This tutorial is based around a very simple example including a design-under-test, a verification environment (or test bench), and a test. Assuming you have written test benches in VHDL or Verilog, the structure should be reasonably obvious. The SystemVerilog code is structured as follows:

- Interface to the design-under-test

- Design-under-test (or DUT)

- Verification environment (or test bench)
  - Transaction
  - Sequencer (stimulus generator)
  - Driver
  - Top-level of verification environment
    - Instantiation of sequencer
    - Instantiation of driver

- Top-level module
  - Instantiation of interface
  - Instantiation of design-under-test
  - Test, which instantiates the verification environment
  - Process to run the test

Since this example is intended to get you started, all the code is placed in a single file. In practice, of course, the code would be spread across multiple files. Also, some pieces of the jigsaw are missing, most notably a verification component to perform checking and collect functional coverage information. It should be emphasised that the purpose of this tutorial is not to demonstrate the full power of UVM, but just to get you up-and-running.

Classes and Modules

In traditional Verilog code, modules are the basic building block used to structure designs and test benches. Modules are still important in SystemVerilog and are the main language construct used to structure RTL code, but classes are also important, particularly for building flexible and reusable verification environments and tests. Classes are best placed in packages, because packages enable re-use and also give control over the namespaces of the SystemVerilog program.

The example for this tutorial includes a verification environment consisting of a set of classes, most of which are placed textually within a package, a module representing the design-under-test, and a single top-level module coupling the two together. The actual link between the verification environment and the design-under-test is a SystemVerilog interface.

Hooking up the DUT

The SystemVerilog interface encapsulates the pin-level connections to the DUT.

interface dut_if();

  int addr;
  int data;
  bit r0w1;

  modport test (output addr, data, r0w1);
  modport dut  (input  addr, data, r0w1);

endinterface: dut_if

Of course, a real design would have several far more complex interfaces, but the same principle holds. Having written out all the connections to the DUT within the interface, the actual code for the outer layer of the DUT module becomes trivial:

module dut(dut_if.dut i_f);
  ...
endmodule: dut

As well us removing the need for lots of repetitive typing, interfaces are important because they provide the mechanism for hooking up a verification environment based on classes. In order to mix modules and classes, a module may instantiate a variable of class type, and the class object may then use hierarchical names to reference other variables in the module. In particular, a class may declare a virtual interface, and use a hierarchical name to assign the virtual interface to refer to the actual interface. In effect, a virtual interface is just a reference to an interface. The overall structure of the code is as follows:

package my_pkg;
  ...
  class my_driver extends uvm_driver;
    ...
    virtual dut_if m_dut_if;
    ...
  endclass

  class my_env extends uvm_env;
    my_driver m_driver;
    ...
  endclass
  ...
endpackage

module top;
  import my_pkg::*;

  dut_if dut_if1 ();
  dut    dut1 ( .i_f(dut_if1) );

  class my_test extends uvm_test;
    ...
    my_env m_env;
    ...
    void connect_phase(uvm_phase phase);
      m_env.m_driver.m_dut_if = dut_if1;
    endfunction: connect_phase
    ...
  endclass: my_test
  ...
endmodule: top

If you study the code above, you will see that the connect_phase method of the class my_test uses a hierarchical name to assign dut_if1, the actual DUT interface, to the virtual interface buried within the object hierarchy of the verification environment. In practice, the verification environment would consist of many classes scattered across many packages from multiple sources. The behavioral code within the verification environment can now access the pins of the DUT using a single virtual interface. The single line of code assigning the virtual interface is the only explicit dependency necessary between the verification environment and the DUT interface. In other words, the verification environment does not directly refer to the pins on the DUT, but only to the pins of the virtual interface.

Transactions

The verification environment consists of two verification components, a sequencer and a driver, and a third class representing the transaction passed between them. The sequencer creates random transactions, which are retrieved by the driver and used to stimulate the pins of the DUT. A transaction is just a collection of related data items that get passed around the verification environment as a single unit. You would create a user-defined transaction class for each meaningful collection of data items to be passed around your verification environment. Transaction classes are very often associated with busses and protocols used to communicate with the DUT.

In this example, the transaction class mirrors the trivial structure of the DUT interface:

class my_transaction extends uvm_sequence_item;

  `uvm_object_utils(my_transaction)
  
  rand int addr;
  rand int data;
  rand bit r0w1;

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

  constraint c_addr { addr >= 0; addr < 256; }
  constraint c_data { data >= 0; data < 256; }

endclass: my_transaction

The address, data and command (r0w1) fields get randomised as new transactions are created, using the constraints that are built into the transaction class. In UVM, all transactions, including sequence items, are derived from the class uvm_transaction, which provides some hidden machinery for transaction recording and for manipulating the contents of the transaction. But because the example uses a sequencer, the transaction class must be derived from the uvm_sequence_item class, which is a subclass of uvm_transaction. The constructor new is passed a string that is used to build a unique instance name for the transaction.

As transaction objects are passed around the verification environment, they may need to be copied, compared, printed, packed and unpacked. The methods necessary to do these things may be created automatically by using the uvm_object_utils and uvm_field macros, or manually by overriding do_copy, do_compare. convert2string and so on.

Verification Components

In UVM, a verification component is a SystemVerilog object of a class derived from the base class uvm_component. Verification component instances form a hierarchy, where the top-level component or components in the hierarchy are derived from the class uvm_env. Objects of type uvm_env may themselves be instantiated as verification components within other uvm_envs. You can instantiate uvm_envs and uvm_components from other uvm_envs and uvm_components, but the top-level component in the hierarchy should always be a uvm_env.

A verification component may be provided with the means to communicate with the rest of the verification environment, and may implement a set of standard methods that implement the various phases of elaboration and simulation. One such verification component is the driver, which is described here line-by-line:

class my_driver extends uvm_driver #(my_transaction);

uvm_driver is derived from uvm_component, and is the base class to be used for user-defined driver components. There is a number of such methodology base classes derived from uvm_component, each of which has a name suggestive of its role. Some of these classes add very little functionality of their own, so it is also possible to derive the user-defined class directly from uvm_component.

uvm_driver is a parameterized class – so either you must specialize it when creating your driver class, or the user-defined class must also be parameterized. In this example we have taken the former approach and specialized for the user-defined transaction class.

`uvm_component_utils(my_driver)

The uvm_component_utils macro provides factory automation for the driver. The factory will be described below, but this macro plays a similar role to the uvm_object_utils macro we saw above for the transaction class. The important point to remember is to invoke this macro from every single verification component; otherwise, bad things happen.

virtual dut_if m_dut_if;

The virtual interface is the means by which the driver communicates with the pins of the DUT, as described above.

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

The constructor for a uvm_component takes two arguments, a string used to build the unique hierarchical instance name of the component and a reference to the parent component in the hierarchy. Both arguments should always be set correctly, and the user-defined constructor should always pass its arguments to the constructor of the superclass, super.new.

task run_phase(uvm_phase phase);
  phase.raise_objection(this);
  forever
  begin
    my_transaction tx;
    #10
    phase.drop_objection(this);
    seq_item_port.get_next_item(tx);
    phase.raise_objection(this);

    `uvm_info("",$sformatf("Driving cmd = %s, addr = %d, data = %d}",
                 (tx.r0w1 ? "W" : "R"), tx.addr, tx.data), UVM_NONE);
    m_dut_if.r0w1 = tx.r0w1;
    m_dut_if.addr = tx.addr;
    m_dut_if.data = tx.data;
    seq_item_port.item_done();
  end
endtask: run_phase

endclass: my_driver

The run_phase method is one of the standard hooks called back in each of the phases of elaboration and simulation. It contains the main behavior of the component to be executed during simulation. This run_phase method contains an infinite loop to wait for some time, get the next transaction from the seq_item_port, then wiggle the pins of the DUT through the virtual interface mentioned above.

The seq_item_port is declared in the uvm_driver class itself - it is available through inheritance and its type is uvm_seq_item_pull_port. The driver uses the seq_item_port to communicate with the sequencer, which generates the transactions. It calls the get_next_item method through this port to fetch transactions of type my_transaction from the sequencer.

People sometimes express discomfort that this loop appears to run forever. What stops simulation? There are two aspects to the answer. Firstly, get_next_item is a blocking method. The call to get_next_item will not return until the next transaction is available. When there are no more transactions available, get_next_item will not return, and simulation is able to stop due to event starvation. Secondly, it is possible to set a global watchdog timer such that every run method will eventually return, even if it is starved of transactions. The timer is set by calling the method set_global_timeout.

The calls to raise_objection and drop_objection are required to end the test in an ordered fashion. The idea is that any component that is busy should raise an objection to ending the test, then drop the objection when it is finished. When every active component has dropped its objection the test will end, regardless of any periodic clock generators.

The run_phase method also makes a call to `uvm_info to print out a report. This is a method of the report handling system, which provides a standard way of logging messages during simulation. The first argument to `uvm_info is a message type, the second argument the text of the message, and the third argument is a verbosity level. Report handling can be customised based on the message type or severity, that is, information, warning, error or fatal.

The Sequencer and Sequence

The sequencer's role is to generate a stream of transactions to be consumed by the driver. A sequencer can be created by extending uvm_sequencer, or in the case of a plain sequencer, merely by creating a typedef:

typedef uvm_sequencer #(my_transaction) my_sequencer;

A sequence runs on a sequencer, and is created by extending the class uvm_sequence.

class my_sequence extends uvm_sequence #(my_transaction);
  
  `uvm_object_utils(my_sequence)

A sequence has a constructor with exactly the same form as the constructor for a transaction:

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

A sequence is distinguished from other forms of uvm_object by having a method named body, which executes the behavior of the sequence. In the most general case, the body method executes any sequence of actions. In practice, this usually means generating transactions or starting other sequences, either on the current sequencer or on another sequencer (in which case the current sequencer would be called a virtual sequencer).

task body;
  uvm_test_done.raise_objection(this);
  repeat(10)
  begin
    my_transaction tx;
    tx = my_transaction::type_id::create("tx");
    start_item(tx);
    assert( tx.randomize() );
    finish_item(tx);
  end
  uvm_test_done.drop_objection(this);
endtask: body

Individual transactions are generated within the sequence using the construct TYPE::type_id::create, which is an example of a so-called factory method. By creating instances of transaction objects using create rather than new, it becomes possible to override the actual choice of transaction object type from the top-level test, and hence to customize the behavior of the sequence from test-to-test.

The sequence communicates with the driver by calling the methods start_item and finish_item. These methods form a handshake with the methods get_next_item and item_done, called from the driver. Note that the newly minted transaction object is randomized before being sent to the driver.

Note that the body method calls raise_objection and drop_objection in order to prevent the test from finishing while the sequence is in progress.

Connecting Up The Environment

The driver and sequencer are specific verification components instantiated within the verification environment. Now we will look at how the environment in constructed from these and other components. Again, we will describe the code line-by-line.

class my_env extends uvm_env;

`uvm_component_utils(my_env)

As mentioned above, uvm_env is the base class from which all user-defined verification environments are created. In a sense, a uvm_env is just another hierarchical building block, but one that may be instantiated as a top-level verification component.

my_sequencer m_sequencer;
my_driver    m_driver;

The environment contains the sequencer (stimulus generator) and driver. The default behavior of a sequencer is to generate a sequence of up to 10 randomized transactions.

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

The constructor should look familiar, but notice that in this case the parent argument may be null because the uvm_env could be a top-level component.

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

  m_sequencer = my_sequencer::type_id::create("m_sequencer", this);
  m_driver    = my_driver   ::type_id::create("m_driver", this);

endfunction: build_phase

The build_phase method creates instances of the my_driver and my_sequencer components, but not by calling their constructors. Instead, they are instantiated using the factory, which performs polymorphic object creation. In other words, the actual type of the object being created by the factory can be set at run time such that the lines of code shown above will not necessarily create components of type my_driver or my_sequencer. It is possible to override the type of component being created from the test in order to replace my_driver with a modified driver and my_sequencer with a modified sequencer. The factory is one of the most important mechanisms in UVM, permitting the creating of flexible and reusable verification components. This flexibility is not available when directly calling the constructor new, which always creates an object of a given type as determined at compile time.

The way in which the factory creates an instance is quite complicated. All you really need to know for now is what to write to create an instance, which involves calling a create function, as shown here. The arguments passed to the method create are: one, the local instance name of the component being instantiated; and two, a reference to the parent component, which in this case is the environment.

At this stage, the verification components have been created, but not connected together.

function void connect_phase(uvm_phase phase);
  m_driver.seq_item_port.connect(m_sequencer.seq_item_export);
endfunction: connect_phase

The connect_phase method of uvm_component is the callback hook for another of the standard phases, and is used to connect ports to exports. Here, we are connecting the driver's seq_item_port to the sequencer's seq_item_export. The connect_phase method is called after the build_phase method, so you know that the components being connected will already have been instantiated, whether by new or by the factory. Notice that the method used to make the connections is also named connect.

task run_phase(uvm_phase phase);
  `uvm_info("","Called my_env::run", UVM_NONE);
endtask: run_phase

function void report_phase(uvm_phase phase);
  `uvm_info("", "Called my_env::report", UVM_NONE);
endfunction: report_phase

endclass: my_env

The run_phase and report_phase callback hooks are also included for completeness, although here they do nothing but print out a message showing they have been called. report_phase is called near the end of simulation, when the run phase is complete.

Elaboration and Simulation Phases

Now we can summarise the standard elaboration and simulation phases. The following callback hooks are called in the order shown for classes derived from uvm_component:

1. new()   The constructor
2. build_phase()   Create components using new or the factory
3. connect_phase()   Make port, export and implementation connections
4. end_of_elaboration_phase()   After all connections have been hardened
5. start_of_simulation_phase()   Just before simulation starts
6. run_phase()   Runs simulation
7. extract_phase()  Post processing 1
8. check_phase()  Post processing 2
9. report_phase()  Post processing 3
10. final_phase()   Backstop

The Test Class

We have looked at the verification environment, which generates a series of random transactions and drives them into the DUT, and at hooking up the verification environment to the DUT. We have also emphasised that a real verification environment would also have to deal with checking and coverage collection across multiple, reusable verification components. Now we look at the test, which configures the verification environment to apply a specific stimulus to the DUT. A practical verification environment must allow you to choose between multiple tests, which must be selectable and runnable with minimal overhead.

The test is represented by a class derived from the methodology base class uvm_test, and is described line-by-line below:

class my_test extends uvm_test;

  `uvm_component_utils(my_test)

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

The uvm_test class does not actually provide any functionality over-and-above a uvm_component, but the idea is that you use it as the base class for all user-defined tests.

my_env m_env;

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

  m_env = my_env::type_id::create("m_env", this);
endfunction: build_phase

The test instantiates the verification environment as a local component using the factory. It is reasonable that the test should depend on the environment, since each test will usually configure the environment to its own specific needs. The factory is used for consistency, although the flexibility provided by the factory is not needed in this case: We could equally well have used new to instantiate the environment.

function void connect_phase(uvm_phase phase);
  m_env.m_driver.m_dut_if = dut_if1;
endfunction: connect_phase

The connect_phase method hooks up the DUT interface to the virtual interface in the driver. This was already discussed above. This one assignment makes the connection between the structural SystemVerilog code, that is, modules and interfaces, and the class-based code of the verification environment.

In this simple example, the test itself doesn't seem to do anything. However, it does instantiate the environment; the sequencer will generate a default sequence of transactions and the driver will apply these to the DUT.

Running the Test

We have got the verification environment connected to the DUT, and we have got a test which could be one of many. The test is actually started by calling the run_test method. The name of the test to be run may be passed as an argument to run_test or may be given on the command line. Once again, here is the outline of the top-level module:

module top;
  ...
  dut_if dut_if1 ();
  dut    dut1 ( .i_f(dut_if1) );

  class my_test extends uvm_test;
    ...
  endclass: my_test

  initial
    run_test();

endmodule: top

The simulator can now be started from the command line as follows:

Incisive>  irun -f irun.f +UVM_TESTNAME=my_test

QuestaSim> qverilog -f questa.f -R +UVM_TESTNAME=my_test

VCS>       vcs -f vcs.f +UVM_TESTNAME=my_test

When the simulation runs, you should see the following:

  • A message saying that my_test is being run.
  • Many of the callback hooks print out a message saying that they have been called.
  • The driver prints out a series of up to 10 messages showing that it is driving transactions into the DUT. Each transaction obeys the constraints set in my_transaction.
  • The DUT prints out a series of up to 10 messages showing that it has received the corresponding commands.

You will find the source code for this first example in file uvm_getting_started_1.sv.

Click here to download the source file for this example. In exchange, we will ask you to enter some personal details. To read about how we use your details, click here. On the registration form, you will be asked whether you want us to send you further information concerning other Doulos products and services in the subject area concerned.

Next:  Easier UVM - for VHDL and Verilog Users
Previous:  UVM Verification Primer   

LINKS

Easier UVM Coding Guidelines

Easier UVM - Deeper Explanations

Easier UVM Code Generator

Easier UVM Video Tutorial

Easier UVM Paper and Poster

Easier UVM Examples Ready-to-Run on EDA Playground

 

Back to the full list of UVM Resources