Getting Started With OVM - A First Example: Now Updated For UVM 1.0
Getting Started With OVM - A First Example: Now Updated For UVM 1.0
Getting Started With OVM - A First Example: Now Updated For UVM 1.0
Home > Knowhow > Sysverilog > Uvm > From OVM to UVM: Getting Started with UVM
VHDL
FPGA
From OVM to UVM: Getting Started with UVM - A First Example
Verilog
OVM
From OVM to UVM
UVM
UVM is based on OVM, so from the outset it should be very straightforward to
VMM
interoperate between OVM and UVM or to convert old OVM code to UVM code. We
PSL
thought we would test this out by converting our existing online tutorial Getting
Perl
Started with OVM - A First Example to UVM. You can see the results of this
Tcl/Tk
conversion below.
ARM / Embedded
Video Gallery 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!
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::*;
- 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.
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.
interface dut_if();
int addr;
int data;
bit 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:
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
module top;
import my_pkg::*;
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:
`uvm_object_utils(my_transaction)
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:
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_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.
The virtual interface is the means by which the driver communicates with the pins of
the DUT, as described above.
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.
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.
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.
`uvm_object_utils(my_sequence)
A sequence has a constructor with exactly the same form as the constructor for a
transaction:
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.
`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.
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.
endfunction: build_phase
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.
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
The test is represented by a class derived from the methodology base class
uvm_test, and is described line-by-line below:
`uvm_component_utils(my_test)
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;