Getting Started with OVM
Tutorial 3 - The OVM Register PackageDoulos, April 2010
Part 2 - How to use the Register Package (RGM) built-in test
Before reading through this tutorial, make sure to have a read through the 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 - read 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 our 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:
- Create an adapter sequence
- Add TLM connections and adapter sequence into the Wishbone sequencer
- Incorporate the RGM sequencer into your environment
- Create a register test
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 finish 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"); super.new(name); endfunction : new virtual task body(); @block; // Block for response handling endtask virtual function void done(); ->block; // Done blocking endfunction ... `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 req.data = op.get_reg_value(); req.kind = TX; end else 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( rsp.data ); p_sequencer.reg_rsp_port.write( op ); endtask 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
`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 `ovm_blocking_put_imp_decl(_reg) 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 ); super.new( 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(); super.build(); $cast( adapter_seq, create_object( "wb_adapter_seq", "adapter_seq" )); endfunction //-------------------------------------------------------------------------- // 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 ); endtask 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(); super.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); super.new(name, parent); endfunction virtual function void build(); super.build(); // Specify the test sequence set_config_int("*.m_rgm_sequencer", "count", 1 ); set_config_string("*.m_rgm_sequencer", "default_sequence", "ovm_rgm_wr_rd_all_reg_seq"); 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 // Fork off the adapter sequence to convert from RGM to WB m_wb_seqr.adapter_seq.start( m_wb_seqr ); join_none #0; // Ensure adapter seq is running before exiting endtask function void extract(); // Tell the adapter sequence to stop m_wb_seqr.adapter_seq.done(); endfunction 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