Doulos, April 2010
On 18 December 2009, OVM 2.1 was released to OVM World. It adds some new, very useful features such as:
OVM 2.1.1 was released on 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.
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 );
or
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 ); end 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 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(); ovm_test_done.raise_objection(this); endtask virtual task body(); ... endtask : body task post_body(); ovm_test_done.drop_objection(this); endtask `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.
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()" ); endtask virtual task trans_executed( driver drv, trans tr ); endtask function new( string name = "driver_callback" ); super.new(name); endfunction 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.
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;
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 )) endtask task trans_executed( driver drv, trans tr ); `ovm_do_callbacks( driver_callbacks, drv, trans_executed( this, tr )) endtask
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 ); end endtask : run
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" ); super.new(name); endfunction `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 tr.data[i] = ~tr.data[i]; // Corrupt the data end else drv.ovm_report_warning( "error callback", "Error selecting the bit to twiddle!" ); end endtask // 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(); super.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.
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 ); super.new( name ); this.addr = addr; endfunction function void print(); ovm_report_info( get_name(), $psprintf( "My address is %x", addr )); endfunction `ovm_object_utils_begin( register_object ) `ovm_field_int( addr, OVM_ALL_ON ) ... // Other register fields here `ovm_object_utils_end 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 ); end ovm_report_info( "test", $psprintf( "registers has %d keys", registers.num())); // Print out the registers registers.first( name ); do begin r = registers.get( name ); r.print(); end while ( registers.next( name )); end 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.
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 ); end ovm_report_info( "test", $psprintf( "registers has %d elements", registers.size())); while ( registers.size() > 0 ) begin r = registers.pop_front(); r.print(); end end 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.