This tutorial extends the example from the previous tutorial to illustrate some further features of OVM. This will involve making incremental improvements to the previous example to take it a little closer to being a reusable verification environment. In particular we will discuss the configuration interface and sequences.
The driver component in the previous example contained an infinite loop. The first improvement we will make is to limit the number of iterations by setting a parameter. Doing so will require us to introduce the OVM configuration interface, which is a very flexible and powerful mechanism for customizing the behaviour of a verification component.
class my_driver extends ovm_driver; ... virtual task run; int count; if ( !get_config_int("iterations", count) ) // In the absence of a config setting, use a default value: count = 20; repeat(count) begin ... end endtask: run endclass: my_driver
The method get_config_int searches a table of configuration settings for a field named "iterations" that is applicable to the current instance. If a field of that name is found, the method sets the value of the second argument count to the value of the "iterations" field and returns true, otherwise it returns false. In this case, if the configuration field is not found, the driver gracefully reverts to using a default value.
Entries are created in the table of configuration settings by calling one of the three methods set_config_int, set_config_string or set_config_object. Each entry consists of an instance name, a field name and a value. Values set by set_config_int can be retrieved by get_config_int, and the same for the other field types. Moreover, if there exists a field with a matching name, as created by the macro `ovm_field_int, then that field is also set to the same value. The existence of fields as created by `ovm_field_int is not necessary for the operation of the set_config_ / get_config_ methods, but such fields are set as a side-effect of executing set_config_.
That is not the end of the story. The instance and field names in the configuration table can each include wildcard characters, "*" matching any sequence of characters and "?" matching any single character. These wildcard matches are greedy, meaning for example that the instance name "*" will match any instance name whatsoever, not just names restricted to a single hierarchical level. These wildcard matches have to be used with care, but they are very powerful.
The table of configuration settings is created before-the-fact, irrespective of the actual instance names and field names used in the component hierarchy. It contains raw text strings. As the verification component hierarchy gets elaborated, fields will be set if and only if their names match the strings in the table of configurations. When combined with wildcards, this mechanism gives enormous flexibility in configuring the verification environment.
In our example, the "iterations" field is set by the test as follows:
class my_test extends ovm_test; ... my_env m_env; virtual function void build; ... set_config_int("m_env.*", "iterations", 10);
The instance name "m_env_*" means that the setting will match any call to get_config_int("iterations"...) within the instance m_env and below.
Having introduced configurations, we now have a better way of hooking the virtual interface in the verification environment to the DUT interface. In the previous example this was achieved by including the test class within the top-level model and hard wiring the connection into the test. But the test class would better be placed within a package where it could be re-used, rather than being a fixed part of the top-level module and hard wired to a specific virtual interface within the verification environment. Configurations give us the opportunity to remove this dependency between the test and the verification environment.
The idea is to define the virtual interface buried within the verification environment as a field, then use set_config_ to configure the virtual interface from the top-level module. There need be no dependency between the test and the DUT interface. This should work in principle, but there is one complication: set_config_ that cannot be used directly to set a virtual interface. To make this work, it is necessary to create an object wrapper for the virtual interface. With this one simple extra step, everything falls into place.
First, the wrapper:
class dut_if_wrapper extends ovm_object; virtual dut_if m_dut_if; function new(string name, virtual dut_if _if); super.new(name); m_dut_if = _if; endfunction : new endclass: dut_if_wrapper
The wrapper, itself an ovm_object, can be registered as a field within the driver (and later set using set_config_object).
class my_driver extends ovm_driver; dut_if_wrapper m_dut_if_wrapper; `ovm_component_utils_begin(my_driver) `ovm_field_object(m_dut_if_wrapper, OVM_ALL_ON) `ovm_component_utils_end ...
The next step is to move the test class into a package:
package my_pkg; ... class my_driver extends ovm_driver; ... endclass: m_driver class my_env extends ovm_env; ... my_driver m_driver; ... endclass: my_env class my_test extends ovm_test; ... my_env m_env; ... endclass: my_test endpackage: my_pkg
Finally, the virtual interface can be configured from the top-level module
module top; ... dut_if dut_if1 (); dut_if_wrapper m_dut_if_wrapper = new("m_dut_if_wrapper", dut_if1); initial begin set_config_object("*.m_driver", "m_dut_if_wrapper", m_dut_if_wrapper, 0); run_test(); end endmodule: top
The call to set_config_object applies to every path with an instance name finishing with the string "m_driver", and to the field named "m_dut_if_wrapper". The value being set is an ovm_object. The fourth argument, with the value 0, is a flag to indicate whether the method should clone the object or merely set a reference to the object. In this case, it sets a reference to the dut_if_wrapper constructed in the top-level module. We have now seen two applications of set_config_, to set an iteration count and to set a virtual interface. In general, set_config_ may be used to configure verification components to meet the needs of the current verification environment, or to configure the verification environment to meet the needs of the current test. Configuration settings can be propagated down the verification component hierarchy using get_config_ / set_config_ or by means of wildcards, and can be tested conditionally by a verification component. set_config_object can be used to replace whole components.
In the previous tutorial we used a sequencer in its simplest mode. Potentially, the ovm_sequencer class offers a very powerful mechanism for generating structured protocols.
A user-defined sequencer is a class derived from ovm_sequencer. In this example, as before, the simplest possible sequencer is used:
class my_sequencer extends ovm_sequencer #(my_transaction); `ovm_sequencer_utils(my_sequencer) function new(string name="", ovm_component parent=null); super.new(name, parent); `ovm_update_sequence_lib_and_item(my_transaction) endfunction: new endclass: my_sequencer
Note that the macro `ovm_sequencer_utils is used to register the sequencer, rather than `ovm_component_utils that we have seen used to register components for automation. Every sequencer holds an array of sequences from which it selects a sequence to run one-at-a-time. This array-of-sequences can be populated initially using the macro `ovm_update_sequence_lib_and_item, which adds three standard sequences to the sequencer: ovm_simple_sequence, ovm_random_sequence and ovm_exhaustive_sequence.
By default, the sequence ovm_random_sequence is selected, and this generates between 1 and 10 simple sequences. Each simple sequence generates a single transaction of the given type, my_transaction. You can change the upper limit to something other than 10 by setting the property max_random_count of the sequencer.
As things stand, running the code described above, our example would generate a sequence of between 1 and 10 randomized transactions, with the actual number being determined at random. We can choose to take control of the exact number of transaction to be generated by setting the count property of the sequencer. A good place to do this would be from the test:
class my_test extends ovm_test; ... virtual function void build; super.build(); set_config_int("m_env.*", "iterations", 50); set_config_int("*.m_sequencer", "count", 10); ...
The first set_config_int call was described above. The second call sets the count property of the sequencer to control precisely how many transactions are generated by the default sequence ovm_random_sequence.
The ovm_sequencer is not restricted to generating single transactions. The next step is to introduce a user-defined sequence, and add it to the sequence list of the sequencer. Whereas a sequencer is a component, a sequence relates a set of data items (transactions and other sequences) and includes code to generate them in the proper order. A user-defined sequence extends the base class ovm_sequence:
class my_sequence_1 extends ovm_sequence; `ovm_sequence_utils(my_sequence_1, my_sequencer) my_transaction seq_item; function new(string name="", ovm_sequencer sequencer=null, ovm_sequence parent_seq=null); super.new(name, sequencer, parent_seq); endfunction: new virtual task body; `ovm_do(seq_item) `ovm_do_with(seq_item, {addr == 1;} ); `ovm_do_with(seq_item, {addr == 2;} ); `ovm_do_with(seq_item, {addr == 3;} ); endtask: body endclass: my_sequence_1
The macro `ovm_sequence_utils registers the sequence for factory automation and adds the sequence to the list belonging to the given sequencer. At this point, my_sequence_1 becomes one of the sequences that may get selected when my_sequencer runs.
The main behaviour of a sequence is defined by its body method. A sequence typically generates a series of sequence items, where each item may be either a transaction or another sequence. The macro `ovm_do is the simplest way in which a sequence can generate a sequence item. `ovm_do creates a new object, waits for whatever is downstream of the sequencer to be ready to consume the item, randomizes the item, then sends the item downstream. The variant `ovm_do_with macro is passed an in-line constraint (using SystemVerilog constraint syntax) which is used when randomizing the item. The point about this mechanism is that the constrained randomization is applied at the last possible moment, just before the sequence item is needed. In this case, my_sequence_1 one generates a series of four transactions, where transaction numbers 2, 3, and 4 have addresses 1, 2, and 3 respectively.
Each new sequence is added to the sequence list of some sequencer; any existing sequences are still available for selection. If we run the above code as is, five sequences will be selected and run at random by my_sequencer, where each sequence is either ovm_simple_sequence (a single transaction) or my_sequence_1 (four transactions).
Having a sequencer run though all the sequences in its library at random fits well with the principles of coverage-driven verification. But sometimes, you will want to start a particular sequence explicitly. This can be done using the sequence selection and execution interfaces. Sequence execution can be controlled from within a user-defined sequence class or from the test:
class my_test extends ovm_test; ... virtual function void build; set_config_int("*.m_sequencer", "count", 0); ... task run; ovm_sequence_base seq; repeat(2) begin seq = m_env.m_sequencer.get_sequence( m_env.m_sequencer.get_seq_kind("my_sequence_1") ); assert(seq.randomize()); seq.start ( m_env.m_sequencer ); end ...
The run method above selects the sequence named "my_sequence_1" from the sequence list of the sequencer, randomizes the sequence object, then executes the sequence with the sequencer. The method get_seq_kind looks up a sequence by name and returns the index number of the sequence in the sequencer’s list. The method get_sequence creates a new sequence object using the factory. The sequence object is then randomized before calling its start method, which begins execution of the sequence on the specified sequencer.
Also, note that the count field of the sequencer is set to 0. Otherwise, the random sequence of the sequencer would still run in its own right, and sequences from the random sequence and my_sequence_1 started here would get interleaved.
It was mentioned above that the sequencer creates sequences and sequence items using the factory. This means that the type of the sequence or of the transaction object can be overridden as it is generated. In general, the factory mechanism allows a test to customize the behavior of the verification environment in a non-intrusive way. For this particular example, for the sake of illustration, we will show the factory mechanism being used to override the type of the transactions being generated as items within a sequence.
The first step is to derive some new transaction classes from my_transaction:
class my_small_transaction extends my_transaction; `ovm_object_utils(my_small_transaction) constraint c_addr { addr >= 0; addr < 2; } constraint c_data { data >= 0; data < 2; } endclass: my_small_transaction class my_big_transaction extends my_transaction; `ovm_object_utils(my_big_transaction) constraint c_addr { addr >= 254; addr < 256; } constraint c_data { data >= 254; data < 256; } endclass: my_big_transaction
Now we can define a new sequence that selects from these transaction types using factory overrides:
class my_sequence extends ovm_sequence; ... `ovm_sequence_utils(my_sequence, my_sequencer) my_transaction seq_item; ... virtual task body; randcase 2: factory.set_type_override_by_type(my_transaction::get_type(), my_default_transaction::get_type()); 1: factory.set_type_override_by_type(my_transaction::get_type(), my_small_transaction::get_type()); 1: factory.set_type_override_by_type(my_transaction::get_type(), my_big_transaction::get_type()); endcase `ovm_do_with(seq_item, {addr < data;} ) endtask: body endclass: my_sequence
The method set_type_override_by_type of ovm_factory takes two arguments, the first being the transaction class being asked for, the second being the transaction class that will be generated in its place. The sequencer my_sequencer generates transaction of type my_transaction, but the code above will override this behavior to generate the other derived transaction types with the probability given by the randcase.
Note that the first line in the randcase overrides my_transaction with a class called my_default_transaction, actually an unaltered extension of my_transaction. This allows us, in effect, to unset the override when we want to use our basic transaction. We do this because at the time of writing the factory gives an error if it detects an override of the same type, like this:
factory.set_type_override_by_type(my_transaction::get_type(), my_transaction::get_type());
Also note that in our example the override is buried within the body of the sequence. The real power of the factory mechanism only becomes apparent when you put overrides in the test, and thus modify the behavior of a running verification environment without having to touch the source code.
You will find the source code for this example in the file ovm_getting_started_2.sv.
The code makes use of some conditional compilation directives allowing you to explore the various scenarios described above. When you run simulation with no conditionally compiled code, you should see the following:
When you define the macro START_SEQUENCE_MANUALLY, you should see the following:
When you define the macro USE_TRANSACTION_FACTORY (with START_SEQUENCE_MANUALLY undefined) you should see the following:
You will find the source code for this first example in file ovm_getting_started_2.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.
Next: Tutorial 3