Global training solutions for engineers creating the world's electronics

Tutorial 3 - The OVM Register Package (part 2)

How to use the Register Package (RGM) built-in test

Doulos, April 2010


Before reading through this tutorial, make sure to have a read through Part 1 first. The RGM package has made it easy to build our scoreboard, but we can also use its built-in sequencer and sequences to access our register map and exercise our design. RGM defines the following built-in sequences:

  • ovm_rgm_read_all_reg_seq - reads from all registers
  • ovm_rgm_write_all_reg_seq - writes to all registers
  • ovm_rgm_any_write_all_reg_seq - writes to all registers, ignoring constraints
  • ovm_rgm_wr_rd_all_reg_seq - writes to all registers and reads back their value
  • ovm_rgm_walking_one_zero_seq - writes 1/0 patterns followed by reads
  • ovm_rgm_aliasing_seq - writes a random value and reads back all registers to detect register aliasing


Not all sequences honor the register random constraints (like ovm_rgm_walking_one_zero_seq and ovm_rgm_any_write_all_reg_seq), and therefore, may be unsuitable for testing. By default, all the built-in sequences are part of the RGM register sequencer's sequence library, but inappropriate sequences can be removed using OVM's sequencer methods (e.g., remove_sequence()).

Incorporating the built-in test

Adding the RGM sequencer and sequences into our environment involves 4 steps:

  1. Create an adapter sequence
  2. Add TLM connections and adapter sequence into the Wishbone sequencer
  3. Incorporate the RGM sequencer into your environment
  4. Create a register test

Incorporated RGM Sequencer

Step 1: Create an adapter sequence

The RGM sequences generate sequence items based on the ovm_rgm_reg_op transaction class, but the Wishbone driver expects wb_trans objects. Therefore, to integrate the RGM sequencer into our environment, we need some way of converting between the two transaction object types. This is accomplished by using an adapter sequence.

The adapter sequence runs as a process on the Wishbone sequencer, receiving and sending transactions as it converts between the ovm_rgm_reg_op and wb_trans types. Meanwhile, the RGM sequencer executes the built-in sequences (as directed by the test case) in "push" mode to the Wishbone sequencer (typically, sequencers operate in "pull" mode and the driver pulls the sequence items from the sequencer). Some of the built-in sequences read back a response so a return path is established between the sequencers using an analysis port.

The adapter sequence is quite simple in principle. While it contains a body() like all sequences, the body() is essentially empty, yet it needs to block to remain active until the RGM sequencer finishes. Another method, which we will call done(), signals that the RGM sequencer is finished and unblocks the sequence:

class wb_adapter_seq extends ovm_sequence #( wb_trans );

   ovm_rgm_reg_op reg_op;
   local event block;

   function new(string name="wb_adapter_seq");;
   endfunction : new

   virtual task body();
	@block;                 	// Block for response handling

   virtual function void done();
	->block;                        // Done blocking


   `ovm_sequence_utils( wb_adapter_seq, wb_sequencer )

endclass : wb_adapter_seq

Where the real work happens is in the TLM export method that gets called by the RGM sequencer when it generates an ovm_rgm_reg_op. This method creates a wb_trans object, copies the ovm_rgm_reg_op members into it, and sends it on to the Wishbone driver. For the response path, the adapter sequence gets the response using get_response(), sets the value in the ovm_rgm_register_op object (using set_reg_value()), and writes it back to the RGM sequencer through the response analysis port. We will call this task execute_op() and add it to our sequence as follows:

class wb_adapter_seq extends ovm_sequence #( wb_trans );

   virtual task execute_op( ovm_rgm_reg_op op );

      // Create a WB transaction
      `ovm_create( req )

      // Now fill the WB trans with the RGM transaction values
      req.addr = op.get_address();
      if ( op.get_direction() == OP_WR ) begin = op.get_reg_value();
	 req.kind = TX;
	 req.kind = RX;

      // Now send the transaction to the WB driver
      ovm_report_info("WB ADAPTER", "Sending request...");
      `ovm_send( req )

      // Forward on the response to the RGM sequencer
      ovm_report_info("WB ADAPTER", "Writing response...");
      get_response( rsp );
      op.set_reg_value( );
      p_sequencer.reg_rsp_port.write( op );
endclass : wb_adapter_seq

Step 2: Add TLM connections and the adapter sequence into the Wishbone sequencer

With the adapter sequence defined, we can add the TLM connections into our Wishbone sequencer for the RGM sequencer to push register reads and writes on to the Wishbone interface. To not interfere with any other TLM connections in our Wishbone sequencer, we define a specific put export using the `ovm_blocking_put_impl_decl( _suffix ) macro that automatically defines a ovm_blocking_put_imp_reg#() class for us:

`ovm_blocking_put_imp_decl( _reg )

Inside the Wishbone sequencer, we instantiate the TLM connections and the adapter sequence. The adapter sequence's execute_op() method, which converts the ovm_rgm_register_op transactions to wb_trans objects, is called from within the put_reg() method expected by the ovm_blocking_put_imp_reg:

// Forward typedef
typedef class wb_adapter_seq;

// Create the ports for the RGM register sequencer

class wb_sequencer extends ovm_sequencer #(wb_trans);

   // Ports for the RGM sequencer
   ovm_blocking_put_imp_reg #( ovm_rgm_reg_op, wb_sequencer ) reg_req_export;
   ovm_analysis_port #( ovm_rgm_reg_op ) reg_rsp_port;
   wb_adapter_seq adapter_seq;

   // Register the sequencer with the factory
   `ovm_sequencer_utils( wb_sequencer )

   // Constructor
   function new ( string name = "", ovm_component parent = null ); name, parent );

        reg_req_export = new( "reg_req_export", this );
        reg_rsp_port   = new( "reg_rsp_port",   this );

        `ovm_update_sequence_lib_and_item( wb_trans )
   endfunction : new

   // Build phase
   function void build();;

        $cast( adapter_seq, create_object( "wb_adapter_seq", "adapter_seq" ));

   // put_reg() - Called by the ovm_blocking_put_imp_reg port above created
   // by the `ovm_blocking_put_imp_decl() macro.  The RGM sequencer will
   // put RGM transactions here and the WB sequencer will call the adapter
   // sequence to convert them to WB transactions.
   task put_reg( ovm_rgm_reg_op t );
        adapter_seq.execute_op( t );

endclass : wb_sequencer

With OVM 2.x, we can call the factory using class_name::type_id::create(), but using this method to create the adapter sequence instance does not work across all simulators due to a circular reference problem. When defining the adapter sequence, the parent sequencer (wb_sequencer) needs to be specified using the `ovm_sequence_utils() macro, which means that the wb_sequencer must be defined first. However, the wb_sequencer needs to instantiate the adapter sequence, which has not yet been defined so a forward typedef is used. Since the class has not yet been defined, some simulators complain that they cannot find type_id in the specified scope. As a workaround, we use the factory create_obj() method instead.

Step 3: Incorporate the RGM sequencer into your environment

Inside our testbench environment, we can now incorporate the RGM sequencer. This is accomplished by instantiating the sequencer, hooking it up to our Wishbone sequencer, and telling the RGM sequencer where the RGM register map exists. First, we instantiate the RGM sequencer:

class wb_spi_env extends ovm_env;
   ovm_rgm_sequencer    m_rgm_sequencer;

   // Build the member objects
   virtual function void build();;

        // Create the OVM RGM register sequencer for register testing
        set_config_int( "m_rgm_sequencer", "count", 0); // Turn off sequences
        m_rgm_sequencer = ovm_rgm_sequencer::type_id::create( "m_rgm_sequencer",
 this );
   endfunction : build


By default, we specify that the RGM sequencer should not automatically generate any sequences. Next, we connect the RGM sequencer to the Wishbone sequencer:

class wb_spi_env extends ovm_env;
   virtual function void connect;

        // Hook up the RGM sequencer to the WB sequencer
        m_wb_agent.m_wb_seqr.reg_rsp_port.connect( m_rgm_sequencer.rsp_export );
        m_rgm_sequencer.req_port.connect( m_wb_agent.m_wb_seqr.reg_req_export );

   endfunction : connect
endclass : wb_spi_env

Lastly, we need to tell the RGM sequencer where to find the RGM register map contained in our scoreboard using the RGM sequencer's set_container() method:

virtual function void connect;
   ...					// Code from above

   // Tell the RGM sequencer where the register map is
   m_rgm_sequencer.set_container( m_wb_spi_sb.m_regmap.spi );

endfunction : connect

With that, we are ready to start using the built-in RGM sequences.

Step 4: Create a register test

Everything is now instantiated, the register map is in place, and the RGM sequencer is connected to the Wishbone sequencer. A test only needs to tell the RGM sequencer to run, create the environment, start the WB adapter sequence, and then stop the adapter sequence once all sequences are complete:

class wr_rd_all_registers_test extends ovm_test;
    wb_spi_env		m_env;
    wb_sequencer        m_wb_seqr;

   `ovm_component_utils ( wr_rd_all_registers_test )

    function new (string name, ovm_component parent);, parent);

   virtual function void build();;

        // Specify the test sequence
        set_config_int("*.m_rgm_sequencer", "count", 1 );
        set_config_string("*.m_rgm_sequencer", "default_sequence",

        m_env = wb_spi_env::type_id::create( "m_env", this );
   endfunction : build

   task run();
        ovm_component   comp;

        // Get a reference to the WB sequencer in the environment
        comp = ovm_top.find( "*.m_wb_seqr" );
        $cast( m_wb_seqr, comp );

           // Fork off the adapter sequence to convert from RGM to WB
           m_wb_seqr.adapter_seq.start( m_wb_seqr );
        #0;                     // Ensure adapter seq is running before exiting

   function void extract();
        // Tell the adapter sequence to stop
endclass : wr_rd_all_registers_test

That's it! You are ready to exercise the registers in your design. If we run the simulation, we should see something like this:

OVM_INFO @ 0: ovm_test_top.m_env.m_rgm_sequencer [ovm_rgm_wr_rd_all_reg_seq] Starting...
 -o-o-o- container : spi
 -o-o-o- exclude_names :
 -o-o-o- exclude_addresses :
 -o-o-o- condition : RGM_UNCONDITIONAL
 -o-o-o- Tags :
 -o-o-o- Num of registers selected : 7
 -o-o-o- cnt : 1
 -o-o-o- HDL access : FRONTDOOR
 -o-o-o- seq_access_mode : WR_FRONTDOOR_RD_FRONTDOOR
OVM_INFO @ 0: reporter [WB ADAPTER] Sending request...
OVM_INFO @ 285600: reporter [WB ADAPTER] Writing response...
OVM_INFO @ 285600: reporter [WB ADAPTER] Sending request...
OVM_INFO @ 324700: ovm_test_top.m_env.m_rgm_sequencer [ovm_rgm_wr_rd_all_reg_seq] Body ending...

Forking off the adapter sequence could also be done in a virtual sequence, making the test case trivially simple. An example virtual sequence as well as all the source code used in this tutorial can be downloaded from here. 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 the full list of OVM Tutorials