Getting Started with OVM


A Series of Tutorials based on a set of Simple, Complete Examples


Doulos, February 2008


Tutorial 1 - A First Example



Introduction

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 OVM Code

The two most important top level directories in the OVM 1.0 release are ./src and ./examples, which contain the source code for the OVM library and a set of examples, respectively. The source code is structured into subdirectories, but you can ignore this for now.

In order to compile OVM applications, different approaches are recommended depending on your choice of simulator. For Cadence Incisive, the best approach is:

  • to add ./src to the include path on the command line, that is, +incdir+/.../src
  • to add the following directive at the top of your various SystemVerilog files

`include "ovm.svh"

For Mentor Graphics QuestaSim, the best approach is:

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

`include "ovm_macros.svh"
import ovm_pkg::*;

Make sure that you do not put ovm_pkg.sv on the command line if you include "ovm.svh" in the source files.

The following conditional code serves to make code portable between both simulators at the time of writing:

`ifdef INCA 
  `include "ovm.svh"
`else
  `include "ovm_macros.svh"
  import ovm_pkg::*;
`endif

The ./examples directory in the OVM release contains sample script files for both simulators that can be modified to compile this tutorial example.

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
  - Driver
  - Top-level of verification environment
    - Instantiation of stimulus generator
    - 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 OVM, 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 ovm_driver;
    ...
    virtual dut_if m_dut_if;
    ...
  endclass

  class my_env extends ovm_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 ovm_test;
    ...
    my_env m_env;   
    ...
    virtual function void connect;
      m_env.m_driver.m_dut_if = dut_if1;
    endfunction: connect
    ...
  endclass: my_test
  ...
endmodule: top

If you study the code above, you will see that the connect 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 three verification components, a stimulus generator, a FIFO, and a driver, and a fourth class representing the transaction passed between them. The stimulus generator creates random transactions, which are stored in a FIFO before being passed to 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 ovm_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; }
    
  `ovm_object_utils_begin(my_transaction)
    `ovm_field_int(addr, OVM_ALL_ON + OVM_DEC)
    `ovm_field_int(data, OVM_ALL_ON + OVM_DEC)
    `ovm_field_int(r0w1, OVM_ALL_ON + OVM_BIN)
  `ovm_object_utils_end
  
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 OVM, all transactions are derived from the class ovm_transaction, which provides some hidden machinery for transaction recording and for manipulating the contents of the 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 are created automatically by the ovm_object_utils and ovm_field macros. At first, it may seem like an imposition to be required to include macros repeating the names of all of the fields in the transaction, but it turns out that these macros provide a significant convenience because the of the high degree of automation they enable.

The flag OVM_ALL_ON indicates that the given field should be copied, printed, included in any comparison for equality between two transactions, and so on. The flags OVM_DEC and OVM_BIN indicate the radix of the field to be used when printing the given field.

Verification Components

In OVM, a verification component is a SystemVerilog object of a class derived from the base class ovm_component. Verification component instances form a hierarchy, where the top-level component or components in the hierarchy are derived from the class ovm_env. Objects of type ovm_env may themselves be instantiated as verification components within other ovm_envs. You can instantiate ovm_envs and ovm_components from other ovm_envs and ovm_components, but the top-level component in the hierarchy should always be an ovm_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 ovm_driver;

ovm_driver is derived from ovm_component, and is the base class to be used for user-defined driver components. There are a number of such methodology base classes derived from ovm_component, each of which have a name suggestive of their 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 ovm_component (or from ovm_threaded_component - see below).

ovm_get_port #(my_transaction) get_port;

The get_port is the means by which the driver communicates with the stimulus generator. The class ovm_get_port represents a transaction-level port that implements the get(), try_get() and can_get() methods. These methods actually originated as part of the SystemC TLM-1.0 transaction-level modeling standard. The driver calls these methods through this port to fetch transactions of type my_transaction from the stimulus generator.

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.

`ovm_component_utils(my_driver)

The ovm_component_utils macro provides factory automation for the driver. The factory will be described below, but this macro plays a similar role to the ovm_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.

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

The constructor for an ovm_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.

function void build;
  super.build();
  get_port = new("get_port", this);
endfunction : build

The build method is the first of the standard hooks called back in each of the phases of elaboration and simulation. The build phase is when all of the verification components get instantiated. This build method starts by calling the build method of its superclass, as build methods always should, then instantiates the get port by calling its constructor new. This particular component has no other child components, but if it did, they would be instantiated here.

virtual task run;
  forever
  begin
    my_transaction tx;
    #10
    get_port.get(tx);

    ovm_report_message("",$psprintf("Driving cmd = %s, addr = %d, data = %d}",
                                    (tx.r0w1 ? "W" : "R"), tx.addr, tx.data));
    m_dut_if.r0w1 = tx.r0w1;
    m_dut_if.addr = tx.addr;
    m_dut_if.data = tx.data;
  end
endtask: run

endclass: my_driver

The run method is another standard callback, and contains the main behavior of the component to be executed during simulation. Actually, run does not belong to ovm_component but to ovm_threaded_component. Only threaded components have a run method that is executed during simulation. This run method contains an infinite loop to get the next transaction from the get port, wait for some time, 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 is a blocking method. The call to get will not return until the next transaction is available. When there are no more transactions available, get 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. In OVM-1.0, without the timeout, although simulation will stop when there are no more events, the later phases of simulation do not get called.

The run method also makes a call to ovm_report_message 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 ovm_report_message is a message type, and the second argument the text of the message. Report handling can be customised based on the message type or severity, that is, information, warning, error or fatal. ovm_report_message generates a report with severity OVM_INFO.

Connecting Up The Environment

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

class my_env extends ovm_env;

`ovm_component_utils(my_env)

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

ovm_random_stimulus #(my_transaction) m_stimulus;
tlm_fifo            #(my_transaction) m_fifo;
my_driver                             m_driver;

The environment contains the stimulus generator, FIFO, and driver. ovm_random_stimulus is a built-in utility class that provides an easy way of generating a stream of random transactions in simple cases. OVM contains several such utility classes that are provided as a convenience. There are other more sophisticated and powerful approaches that are more appropriate in many situations, but for this and many other simple cases, ovm_random_stimulus will do just fine.

tlm_fifo is another built-in utility class that represents a transaction FIFO, storing a queue of transactions of a given type. A tlm_fifo has a put export at one end for adding transactions, and a get export at the other for removing transactions. In OVM, as in SystemC, a port can be bound to an export such that a method call made through the port (for example, the call to get through the get_port shown above,) is implemented behind the export.

function new(string name = "my_env", ovm_component parent = null);
  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 ovm_env could be a top-level component.

virtual function void build;
  super.build();

  m_stimulus = new("m_stimulus", this);
  m_fifo     = new("m_fifo",     this);

The build method creates instances of the ovm_random_stimulus and tlm_fifo by calling their constructors, remembering to pass in an instance name and a reference to the parent component, this. At this stage, the verification components have been created, but not connected together.

  $cast(m_driver, ovm_factory::create_component("my_driver", 
                          "ovm_test_top.m_env", "m_driver", this) );
endfunction: build

The build method also creates an instance of the my_driver component, but not by calling the constructor. Instead, the driver is 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 line of code shown above will not necessarily create a component of type my_driver. It is possible to override the type of component being created from the test in order to replace my_driver with a modified driver. The factory is one of the most important mechanisms in OVM, 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. For example, m_fifo = new would always create an object of type tlm_fifo.

The arguments passed to the method create_component are: one, the name of the component as registered using the ovm_component_utils macro; two, the hierarchical instance name of the parent component; three, the local instance name of the component being instantiated; and four, a reference to the parent component. As an aside, the class ovm_component also provides a create_component method which has a simplified argument list and saves you the bother of adding ovm_factory:: to the call. The underlying method of ovm_factory is used here to show what is really going on.

virtual function void connect;
  m_stimulus.blocking_put_port.connect(m_fifo.put_export);
  m_driver.get_port.connect(m_fifo.get_export);
endfunction: connect

The connect method of ovm_component is the callback hook for another of the standard phases, and is used to connect ports to exports. The connect method is called after the build 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. The connect callback should only be used to connect ports to exports. There are two other related callbacks, export_connections to connect exports to exports going down the component hierarchy, and import_connections to connect ports to ports going up the component hierarchy. The right callback must be used for the right job, because connections have to be made in the order export-to-export, followed by port-to-export, and finally port-to-port.

virtual function void configure;
  m_stimulus.set_report_id_action("stimulus generation", OVM_NO_ACTION);
endfunction: configure

The configure method is the next callback hook to be called, and incidentally, it is only called for threaded components, that is, components with a run method. The configure method gives an opportunity to set variables and execute code immediately before entering the run phase proper. In this example, the configure method is used to set the action of the ovm_random_stimulus component by calling a method of the report handler. The method set_report_id_action modifies the actions to be executed whenever a report of a given type occurs within the given component, the report type being “stimulus generation” in this case. The flag OVM_NO_ACTION suppresses all actions, that is, silences the reporting of messages of this type from ovm_random_stimulus.

task run();
  ovm_report_message("","Called my_env::run");
endtask: run
     
virtual function void report;
  ovm_report_message("", "Called my_env::report");
endfunction: report
    
endclass: my_env

The run and report callback hooks are also included for completeness, although they do nothing but print out a message showing they have been called. report 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 ovm_component:

  1. new() --------------------- The constructor
  2. build() --------------------- Create components using new or the factory
  3. export_connections() --- Make export-to-export connections
  4. connect() ----------------- Make port-to-export connections
  5. import_connections() --- Make port-to-port connections
  6. end_of_elaboration() --- After all connections have been hardened
  7. configure() --------------- For threaded components only
  8. run() ---------------------- For threaded components only
  9. extract() ------------------ Post-processing 1
  10. check() ------------------- Post-processing 2
  11. report() ------------------- Post-processing 3

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 ovm_test, and is described line-by-line below:

class my_test extends ovm_test;
  
  `ovm_component_utils(my_test)
    
  function new(string name = "my_test", ovm_component parent = null);
    super.new(name,parent);
  endfunction: new

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

my_env m_env;   

virtual function void build;
  super.build();

  $cast(m_env, ovm_factory::create_component("my_env", "ovm_test_top", 
                                             "m_env", this) );
  set_global_timeout(1us);
endfunction: build

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.

The method set_global_timeout sets the watchdog timer referred to above to ensure that every run method will eventually return, particularly those run methods that are waiting on events or empty queues.

virtual function void connect;
  m_env.m_driver.m_dut_if = dut_if1;
endfunction: connect

The connect 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.

task run;
  tiny_transaction tx = new;
  m_env.m_stimulus.generate_stimulus(tx, 10);
endtask: run

endclass: my_test

The run method kicks off the verification environment by calling the generate_stimulus method of class ovm_random_stimulus to generate exactly 10 transactions. The first argument tx will be used as the template for each random transaction as it is generated, and is used to pass test-specific constraints into the generator. The class tiny_transaction is specific to this test, and is defined as follows:

class tiny_transaction extends my_transaction;

  `ovm_object_utils(tiny_transaction)

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

endclass: tiny_transaction

This is a neat trick, because it means that the constraints are part of the test, not hard wired into the verification environment. If generate_stimulus were to be called with a null argument instead of a transaction, only the constraints built into class my_transaction would be applied.

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 tiny_transaction extends my_transaction;
    ...
  endclass: tiny_transaction
  
  class my_test extends ovm_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 +OVM_TESTNAME=my_test

QuestaSim> vsim top +OVM_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 10 messages showing that it is driving transactions into the DUT. Each transaction obeys the constraints set in tiny_transaction.
  • The DUT prints out a series of 10 messages showing that it has received the corresponding commands

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

Click here to download both the source file for this example and this page in PDF format. 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.

Previous:  Tutorial 0   

Next:  Tutorial 2

Back to the full list of OVM Tutorials