Saturday 24 February 2018

Developing & Delivering KnowHow

Home > Knowhow > Sysverilog > Ovm > OVM 2.1 Update

OVM 2.1 Update

The latest features of OVM

Doulos, April 2010


On 18 December 2009, OVM 2.1 was released to OVM World.  It adds some new, very useful features such as:

It also includes the following minor enhancements:

  • Field macros for real numbers and associative arrays of integral types index by enumerated types
  • ovm_object::get_object_type() to return the factory wrapper object
  • ovm_random_sequence::get_count() to provide the random count used by the random sequence

Recently, OVM 2.1.1 was released (19 March 2010), but only bug and documentation fixes and minor miscellaneous changes were made. So let's take a look at the four major new enhancements added into OVM 2.1.

Objection Mechanism

Prior to OVM 2.1, there was no great way of knowing when a test was finished and when to stop the simulation. Typically, a testcase defines the default_sequence for the sequencers in your testbench, and then waits for the sequence(s) to kick-off and finish. A delay is used before calling the global_stop_request() like this:

task run;
   #1500ns global_stop_request();
endtask : run

There are two significant problems with this approach. First, the testbench components may not be finished at 1500ns---e.g., the sequencers may still be generating sequence items, the design under test may still be processing, or the scoreboard may not have checked everything. The second problem is that this delay has to be determined experiementally for every test and then hard-coded into the testcase.

With OVM 2.1, there is a better way to end a test by using the objection mechanism. An objection mechanism works by each testbench component raising or dropping an objection to finishing the simulation. A component raises an objection when it is busy doing work and drops its objection when it is idle.  When all the objections have been dropped, OVM shuts down the simulation and calls $finish automatically. This means that hard-coded delays and calls to global_stop_request() are no longer needed.

In order for the objection mechanism to work, all testbench components need to participate in raising and dropping their objections. This is easily done by calling one of the following methods on the global ovm_test_done object:

ovm_test_done.raise_objection( this );


ovm_test_done.drop_objection( this );

For example, a driver might raise and drop its objections like this:

virtual task run;
   forever begin
	seq_item_port.get_next_item( tr );

	// Raise objection - busy
	ovm_test_done.raise_objection( this );

	... 						// do work here

	seq_item_port.item_done( tr );

	// Drop objection - idle
	ovm_test_done.drop_objection( this );
endtask : run

All components like drivers, monitors, and scoreboards must put these objection calls around the regions of work code much like mutexes go around critical regions that access shared memory. Sequencers also become busy when generating sequence items, but OVM does not automatically use the objection mechanism inside of a sequencers. Rather, individual sequences must raise objections when created and executed on the sequencer. The easiest way to do this is using the pre_body() and post_body() methods as follows:

class simple_seq extends ovm_sequence #( ovm_sequence_item );

   task pre_body();

   virtual task body();
   endtask : body

   task post_body();

   `ovm_sequence_utils ( simple_seq, simple_sequencer )
endclass : simple_seq

With all components and controlling sequences participating in the objection, the testcase will automatically finish once everything has completed. This mechanism easily scales from simple testbenches to large-scale environments.

Lastly, it is a good idea to add one additional feature—a drain time. The drain time specifies an additional amount of time to wait for the environment to quiesce and drain all activity. A great place for specifying this drain time is in the top-level environment in the start_of_simulation phase:

virtual function void start_of_simulation;

   // Allows additional time before stopping immediately
   ovm_test_done.set_drain_time( this, 100ns );

endfunction : start_of_simulation

As this example shows, the drain time is set on the ovm_test_done object by calling set_drain_time(). Adding this extra time ensures that a test does not end prematurely when all the objections are dropped temporarily.


Extending functionality of testbench components in an environment is most easily and idealy done in OVM using the built-in factory mechanism. However, the latest version of OVM 2.1 also provides an alternative approach using callbacks.

Callbacks are method calls placed inside your testbench components, which can be overriden by other components---typically, a user testcase. This provides an easy mechanism for testcases to change the behavior of the verification environment without actually modifying the testbench source code. Callbacks can be used to change a transaction's randomization, the generation of data, add additional coverage terms, define custom scoreboard compare functions, inject errors, or perform any test specific modifications need.

Note, before using callbacks, consider the following admonition from the OVM User Guide:

"Callback facilities are easily abused and often end up limiting a component’s reuse potential. Their inclusion in OVM was primarily to facilitate migration of VMM environments and end-users accustomed to using them" (OVM User Guide, Version 2.1, December 2009, p. 111).

Callbacks are easy to use, but require some upfront work to setup. In particular, using callbacks requires the following four steps.

Step 1: Create a callback abstract base class

In this example, let's add some callbacks into a driver in our testbench. The first step is to create a callback virtual base class that can be used in the driver:

virtual class driver_callbacks extends ovm_callback;

   virtual task trans_received( driver drv, trans tr );
        drv.ovm_report_info( "callback", "In trans_received()" );

   virtual task trans_executed( driver drv, trans tr ); endtask

   function new( string name = "driver_callback" );;

endclass : driver_callbacks

The methods trans_received() and trans_executed() are the callback methods that will be used by the driver. The names or the arguments passed are not critical---they can be anything you want. The important point is to call these methods at the appropriate places in the driver’s main body.

Step 2: Create a user defined type for the callback

Next, we'll define a user defined type for the callback class to make it easier to declare in our driver:

typedef ovm_callbacks #( driver, driver_callbacks ) driver_cbs_t;

Step 3: Add the callback method calls to the testbench components

The next step is to add the callback method calls at the appropriate places in our driver's main body. We do this by using one of the 6 new OVM callback macros (see the OVM Reference Manual, Version 2.1). The simplest macro to use is `ovm_do_callbacks, which we will demonstrate here.

However, there is one "gotcha" with these macros. These macros contain a SystemVerilog return statement, which is evaluated when no callbacks are registered. That means if a driver embeds the callback directly inside its main body, which is typically in a forked-off infinite loop, then the driver may finish executing prematurely when the return statement is encountered. In order to avoid this, these callback macros should always be wrapped inside a method defined inside the testbench component. So in our driver, we first define the wrapper methods that invoke the callbacks:

task trans_received( driver drv, trans tr );
   `ovm_do_callbacks( driver_callbacks, drv, trans_received( this, tr ))

task trans_executed( driver drv, trans tr );
   `ovm_do_callbacks( driver_callbacks, drv, trans_executed( this, tr ))

Note in OVM, semicolons are not used after macros! With these methods defined, we can now add the callbacks into our driver’s main body:

virtual task run;
   forever begin
	seq_item_port.get_next_item( tr );

	// Call user callback before processing the transaction
	trans_received( this, tr );

	...   // drive the transaction onto the DUT's signal interface

	// Call user callback to modify the transaction before finishing
	trans_executed( this, tr );

	seq_item_port.item_done( tr );
endtask : run

Step 4: Define your custom callback functionality and register it with the driver

Now that we have all the callback hooks in place, we can finally define the specific callbacks that we are interested in. The ideal place for this is in our specific testcases. Once the callback is defined, our testcase can then register it with our driver and the driver will automatically call it when it reaches those callback points defined above in step 3.

Here is an example of a testcase that inserts a callback that injects errors in the transaction before the driver sends it. First, we will define the callback (which you will probably want to place in the same file as your testcase):

class error_cb extends driver_callbacks;

   function new( string name = "error_cb" );;

   `ovm_object_utils( error_cb )

   virtual task trans_received( driver drv, trans tr );
	bit [4:0] i;

	// Twiddle between 1 to 10 bits
	repeat ( $urandom_range( 10, 1 ) ) begin

	   assert( std::randomize(i) ) begin[i] =[i];	// Corrupt the data
		drv.ovm_report_warning( "error callback", "Error selecting the bit to twiddle!" );

   // Don't need trans_executed() in this test so leave it undefined

endclass : error_cb

Next, we will define the testcase that uses the callback:

class error_injector_test extends ovm_test;
   ...					// Macros, declarations, etc.

   virtual function void build();;

	// Use the random sequence in this testcase
	set_config_string("*.m_sequencer", "default_sequence", "random_seq" );

	// Create the environment
	m_env = my_env::type_id::create( "m_env", this );
   endfunction : build

   // Register the callback with the driver
   function void start_of_simulation;
	driver	 	drv;		// Driver in the environment
	driver_cbs_t 	cbs;		// Base callback type
	error_cb	e_cb;		// User defined callback

	// Get a pointer to the global callback object
	cbs = driver_cbs_t::get_global_cbs();

	// Create the error injecting callback
	e_cb = new( "e_cb" );

	// Find the driver where the callback will be installed
	$cast( drv, ovm_top.find( "*.m_drv" ));

	// Install the callback in the driver
	cbs.add_cb( drv, e_cb );
   endfunction : start_of_simulation

endclass : error_injector_test

Installing the error injecting callback requires several objects. First, a global callback object is created and returned by calling the static method get_global_cbs(). A pointer to the target driver is returned by using ovm_top.find(). The error injecting callback is then registered with the global driver callback object by using the add_cb() method.

With that, the callback is installed and the driver’s functionality will be extended when the driver invokes the callback methods.

Class-based dynamic associative array

The latest OVM version has implemented a dynamic associative array called an ovm_pool. The ovm_pool is parameterizable and works very much like a regular SystemVerilog associative array. Its advantage as a class object is it can be dynamically created (instead of statically declared), passed by reference, and accessed globally through a global singleton object.

Just as SystemVerilog provides the methods num(), delete(), exists(), first(), last(), next(), and prev() for associative arrays, the ovm_pool class implements their analogous counterparts. In addition, values are added to a pool by using add(), queried using get(), and accessed globally by using get_global_pool() or get_global().

A simple usage of an ovm_pool would be to create a list of register objects for use in a scoreboard register map. Here is a trivial example that illustrates a few of the method calls of ovm_pool:

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

class register_object extends ovm_object;
   int	addr;
   ...					// Other register attributes go here

   function new ( string name = "", int addr = 0 ); name );
	this.addr = addr;

   function void print();
	ovm_report_info( get_name(), $psprintf( "My address is %x", addr ));

   `ovm_object_utils_begin( register_object )
	`ovm_field_int( addr, OVM_ALL_ON )
	...				// Other register fields here

endclass : register_object

With this, an ovm_pool of register objects can be created as follows:

module test_pool;
   ovm_pool #( string, register_object ) registers = new();
   int reglist[ string ] = '{ "control"   : 'h0, "status"    : 'h4,
   			      "mode"      : 'h8, "dma_count" : 'hc,
			      "dma_addr"  : 'h10, "config"    : 'h14 };

   initial begin
	register_object	r;
	string		name;

	// Allocate the registers
	foreach ( reglist[i] ) begin
	   r = new( i, reglist[i] );
	   registers.add( i, r );

	ovm_report_info( "test", $psprintf( "registers has %d keys", registers.num()));

	// Print out the registers
	registers.first( name );
	do begin
	   r = registers.get( name );
	end while ( name ));

endmodule : test_pool

This produces the following results:

OVM_INFO @ 0: reporter [test] registers has           6 keys
OVM_INFO @ 0: reporter [config] My address is 00000014
OVM_INFO @ 0: reporter [control] My address is 00000000
OVM_INFO @ 0: reporter [dma_addr] My address is 00000010
OVM_INFO @ 0: reporter [dma_count] My address is 0000000c
OVM_INFO @ 0: reporter [mode] My address is 00000008
OVM_INFO @ 0: reporter [status] My address is 00000004

Note, OVM provides a specialized ovm_pool class called ovm_object_string_pool, which already defined using a string data type for its key. Please see the OVM Reference Guide for more information on both ovm_pool and ovm_object_string_pool.

Class-based dynamic queue

The latest OVM version has implemented a dynamic queue called an ovm_queue. The ovm_queue is parameterized and works very much like a regular SystemVerilog queue. Its advantage as a class object is it can be dynamically created (instead of statically declared), passed by reference, and accessed globally through a global singleton object.

Just as SystemVerilog provides the methods size(), insert(), delete(), pop_front(), pop_back(), push_front(), and push_back() for queues, the ovm_queue class implements their analogous counterparts. In addition, values can be retrieved using get() and accessed globally by using get_global_queue() or get_global().

As shown above with dynamic associative arrays, queues are ideal for storing many types of information. A simple application would be for registers used in a scoreboard's register map. Using the register_object example defined above, an ovm_queue could be used as follows:

module test_queue;
   ovm_queue #( register_object )	registers = new();
   int reglist[ string ] = '{ "control"  : 'h0,  "status"    : 'h4,
			      "mode"     : 'h8,  "dma_count" : 'hc,
			      "dma_addr" : 'h10, "config"    : 'h14 };

   initial begin
	register_object	r;
	string		name;

	foreach ( reglist[i] ) begin
	   r = new( i, reglist[i] );
	   registers.push_back( r );

	ovm_report_info( "test", $psprintf( "registers has %d elements", registers.size()));

	while ( registers.size() > 0 ) begin
	   r = registers.pop_front();
endmodule : test_queue

For more information on ovm_queue, refer to the OVM Reference Guide.

For an example of using the new OVM objection mechanism and callback features, we invite you to download the source code for an OVM environment built around an OpenCores SPI design. This environment will demonstrate these OVM features and many more. Click here to download the source code. 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.

Back to Getting Started with OVM

Privacy Policy Site Map Contact Us