Getting Started with OVM


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


Doulos, February 2008


Tutorial 2 - Configurations and Sequences



Introduction

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 Configuration Interface

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 method 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.

Configuring the Virtual Interface

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 the 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_ 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.

Sequencers

The previous example made use of ovm_random_stimulus to generate a stream of transactions. This is fine when a simple stream of random transactions is all that is required, but the ovm_sequencer class offers a far more powerful mechanism for generating structured protocols.

A user-defined sequencer is a class derived from ovm_sequencer. In this example, the simplest possible sequencer is used:

class my_sequencer extends ovm_sequencer;

  `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.

  • ovm_simple_sequence consists of a single item of the given transaction type.
  • ovm_random_sequence selects and executes a series of sequences from the array, but excluding ovm_exhaustive_sequence and ovm_random_sequence itself. By default, this only leaves ovm_simple_sequence. The number of sequences depends on the count property of the ovm_sequencer class, and defaults to a random number between 1 and 10.
  • ovm_exhaustive_sequence selects and executes every sequence in the array once, but excluding ovm_random_sequence and ovm_exhaustive_sequence itself.

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.

The class my_transaction of items generated by the sequencer must be derived from ovm_sequence_item rather than ovm_transaction:

class my_transaction extends ovm_sequence_item;
  ...  
  `ovm_object_utils_begin(my_transaction)
    ...
  `ovm_object_utils_end
  
  function new (string name = "", ovm_sequencer_base sequencer = null, 
                                  ovm_sequence parent_seq = null);
    super.new(name);
  endfunction: new
  ...
endclass: my_transaction

Compared to using ovm_transaction, the only visible difference is the constructor arguments.

The Sequencer Interface

The sequencer replaces ovm_random_stimulus in the top-level verification environment:

class my_env extends ovm_env;
  
  `ovm_component_utils(my_env)
    
  my_sequencer  m_sequencer;
  my_driver     m_driver;
  
  function new(string name = "my_env", ovm_component parent = null);
    super.new(name, parent);
  endfunction: new
    
  virtual function void build;
    super.build();

    $cast(m_sequencer, create_component("my_sequencer", "m_sequencer") );
    $cast(m_driver,    create_component("my_driver",    "m_driver") );

    set_global_timeout(10us);
  endfunction: build
    
  virtual function void connect;
    m_sequencer.seq_item_cons_if.connect_if( m_driver.seq_item_prod_if );
  endfunction: connect

endclass: my_env

This structure is very similar to the previous example, but note the names used to connect the sequencer to the driver, seq_item_cons_if and seq_item_prod_if. These are not regular TLM ports as were used to connect ovm_random_stimulus to the driver in the previous example, but instead provide specialized interfaces for producing and consuming sequences. We will see some of the methods provided by these interfaces below.

Also note that the create_component method being used to instantiate the sequencer and driver is not the method of ovm_factory, unlike the previous example. In this case, we are calling a convenience method of the ovm_component class which makes a call to ovm_factory::create_component on our behalf, filling in the appropriate instance names and references.

Now let us look at how the driver is able to pull transactions from the sequencer using the sequence item producer interface:

class my_driver extends ovm_driver;
  ...  
  virtual task run;
    ...
    repeat(count)
    begin
      ovm_sequence_item item;
      my_transaction tx;
      #10
      seq_item_prod_if.get_next_item(item);
      $cast(tx, item);
         
      ovm_report_message("",
                         $psprintf("Driving cmd = %s, addr = %d, data = %d}",
                         (tx.r0w1 ? "W" : "R"), tx.addr, tx.data));
                                        
      m_dut_if_wrapper.m_dut_if.r0w1 = tx.r0w1;
      m_dut_if_wrapper.m_dut_if.addr = tx.addr;
      m_dut_if_wrapper.m_dut_if.data = tx.data;
        
      seq_item_prod_if.item_done(item);
    end
  endtask: run
endclass: my_driver

You can see from the above that the sequence item producer interface offers two methods get_next_item and item_done, which the consumer is able to use to get the next transaction from the sequencer and to signal back to the sequencer when it is done. get_next_item is a blocking method, which only returns when the next item is available, making this a "pull" interface. item_done can be used to return a modified transaction, or just to signal completion. seq_item_prod_if is a property of the methodology base class ovm_driver, so is not defined explicitly in the user’s code.

Sequences

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).

Starting Sequences Manually

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 seq;
    repeat(2)
    begin
      seq = m_env.m_sequencer.get_sequence(
              m_env.m_sequencer.get_seq_kind("my_sequence_1") );
      assert(seq.randomize());
      m_env.m_sequencer.start_sequence( seq );
      #50;
    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 on the sequencer. The method get_seq_kind looks up a sequence by name and return the index number of the sequence in the sequencers’ list. The method get_sequence creates a new sequence object using the factory. The sequence object is then randomized before being passed to the method start_sequence, which forks off a parallel process to call the body method of the sequence (amongst other things). Because the start_sequence method is non-blocking, the run method uses a timing control #50 to consume some time before starting the next sequence. Without this timing control, the sequences would run in parallel on a single sequencer, and the sequence items would get interleaved.

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.

Creating Transactions using The Factory

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: ovm_factory::set_type_override("my_transaction",
                                        "my_transaction");
                                        
      1: ovm_factory::set_type_override("my_transaction",
                                        "my_small_transaction");
                                        
      1: ovm_factory::set_type_override("my_transaction",
                                        "my_big_transaction");
    endcase
        
    `ovm_do_with(seq_item, {addr < data;} )

  endtask: body
endclass: my_sequence

The method set_type_override of ovm_factory takes two arguments, the first being the name of the transaction class being asked for, the second being the name of the transaction class that will be generated instead. 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.

In this case, set_type_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:

  • my_sequencer generates exactly 10 simple transactions with address and data in the range 0 to 255.

When you define the macro START_SEQUENCE_MANUALLY, you should see the following:

  • The sequence my_sequence_1 is run twice from my_test. Each time it runs, my_sequence_1 generates 4 transactions, with the addresses being random, 1, 2, 3 respectively.

When you define the macro USE_TRANSACTION_FACTORY (with START_SEQUENCE_MANUALLY undefined) you should see the following:

  • The sequence my_sequence is run exactly 10 times, each time generating either a my_transaction, my_small_transaction or my_big_transaction with probabilities 0.5, 0.25, 0.25 respectively.

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.

Previous:  Tutorial 1   

Back to the full list of OVM Tutorials