In Register Model, we have seen how to create a model that represents actual registers in a design. Now we'll look at the different components in a register environment required to perform register accesses such as read and write operations.

There are essentially four components required for a register environment :

  • A register model based on UVM classes that accurately reflect values of the design registers
  • An agent to drive actual bus transactions to the design based on some protocol
  • An adapter to convert the read and write statements from the model to protocol based bus transactions
  • A predictor to understand bus activity and update the register model to match the design contents

Register model environment

Register Adapter

uvm_reg has in-built methods called read() and write() to initiate a read and write operation to the design.


class reg_ctl extends uvm_reg;
	...
endclass

m_reg_ctl.write (status, addr, wdata); 		// Write wdata to addr
m_reg_ctl.read  (status, addr, rdata); 		// Read rdata from addr

These register read/write access calls create an internal generic register item of type uvm_reg_bus_op which is a simple struct as shown below.


typedef struct {

	uvm_access_e 		kind; 		// Access type: UVM_READ/UVM_WRITE
	uvm_reg_addr_t 		addr; 		// Bus address, default 64 bits
	uvm_reg_data_t 		data; 		// Read/Write data, default 64 bits
	int  				n_bits; 	// Number of bits being transferred
	uvm_reg_byte_en 	byte_en; 	// Byte enable
	uvm_status_e 		status;		// Result of transaction: UVM_IS_OK, UVM_HAS_X, UVM_NOT_OK

} uvm_reg_bus_op;

To convert read/write method calls into actual bus protocol accesses, the generic register item is converted to a protocol specific bus transaction item by a component called as an adapter. The adapter needs to be bidirectional so that it is able to convert generic register items to bus transactions and convert bus transaction responses back to generic register items so that it can be updated in the register model.

This conversion process is facilitated by the adapter via reg2bus() and bus2reg() functions. As the names imply, reg2bus() convert register level objects of type uvm_reg_bus_op into a protocol transaction and bus2reg() convert bus level transactions to register level objects. Bus protocols vary between designs and hence a custom adapter has to be inherited from uvm_reg_adapter to override these functions.

There are also two variables in the adapter to handle byte enables and response items. The bit supports_byte_enable should be set to 1 if the bus protocol allows enabling certain byte lanes to select certain bytes of data bus as valid. The bit provides_responses should be set to 1 if the target agent driver sends separate response items that require response handling.


// apb_adapter is inherited from "uvm_reg_adapter"
class reg2apb_adapter extends uvm_reg_adapter;
   `uvm_object_utils (apb_adapter)
   
   // Set default values for the two variables based on bus protocol
   // APB does not support either, so both are turned off
   function new(string name="apb_adapter");
   	 super.new(name);
   	 supports_byte_enable = 0;
   	 provides_responses = 0; 
   endfunction
	
	// This function accepts a register item of type "uvm_reg_bus_op" and assigns
	// address, data and other required fields to the bus protocol sequence_item
   virtual function uvm_sequence_item reg2bus (const ref uvm_reg_bus_op rw);
      bus_pkt pkt = bus_pkt::type_id::create ("pkt");
      pkt.write = (rw.kind == UVM_WRITE) ? 1: 0;
      pkt.addr  = rw.addr;
      pkt.data  = rw.data;
      `uvm_info ("adapter", $sformatf ("reg2bus addr=0x%0h data=0x%0h kind=%s", pkt.addr, pkt.data, rw.kind.name), UVM_DEBUG) 
      return pkt; 
   endfunction

	// This function accepts a bus sequence_item and assigns address/data fields to
	// the register item
   virtual function void bus2reg (uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
      bus_pkt pkt;
      
      // bus_item is a base class handle of type "uvm_sequence_item" and hence does not
      // contain addr, data properties in it. Hence bus_item has to be cast into bus_pkt
      if (! $cast (pkt, bus_item)) begin
         `uvm_fatal ("reg2apb_adapter", "Failed to cast bus_item to pkt")
      end
   
      rw.kind = pkt.write ? UVM_WRITE : UVM_READ;
      rw.addr = pkt.addr;
      rw.data = pkt.data;
      rw.status = UVM_IS_OK;	// APB does not support slave response
      `uvm_info ("adapter", $sformatf("bus2reg : addr=0x%0h data=0x%0h kind=%s status=%s", rw.addr, rw.data, rw.kind.name(), rw.status.name()), UVM_DEBUG)
   endfunction
endclass 

Since APB bus protocol does not support byte enables, the bit supports_byte_enable is set to 0. If the agent driver provides a separate response item through put() or item_done(), then the bit provides_responses should be set to 1 so that the register model knows that it has to wait for a response before converting it to a register item. If this bit is set and the agent driver does not provide any response there are chances for the simulation to hang. Since APB protocol does not support slave responses, this bit is set to 0.

Register Predictor

The register model has a different ways to update the model and keep its copy of registers in sync with the values in DUT. By default, it updates the register model every time a read or write transaction is performed. For example if a value is written to the design using write() method, it can easily update the mirrored value for that register in the model with the data written out. Similarly when a read() method gets the read data from the design, it can update the mirrored value accordingly.

regmodel role of predictor

However, it is not required to always use the register model to write into the design as individual sequences with address and data can be started on the same target agent to write into design registers. This would make the values in the register model stale, and would require an update every time some other sequence reads or writes into the design. A component called predictor can be placed on the target bus agent interface to monitor for any transactions and update the register model accordingly.


// uvm_reg_predictor class definition

class uvm_reg_predictor #(type BUSTYPE=int) extends uvm_component;
  	`uvm_component_param_utils(uvm_reg_predictor#(BUSTYPE))
  
	uvm_analysis_imp #(BUSTYPE, uvm_reg_predictor #(BUSTYPE)) bus_in;
  	uvm_reg_map map;
  	uvm_reg_adapter adapter;

	...
endclass

The uvm_reg_predictor component is a child class of uvm_subscriber and has an analysis implementation port capable of receiving bus sequence items from the target monitor. It uses the register adapter to convert the incoming bus packet into a generic register item and then looks up the address from the register map to find the correct register and update its contents. This is protocol independent and hence we do not need to define a custom class. However, we'll have to create a parameterized version of a register predictor as shown below that can be integrated within our register environment.

Steps to integrate a predictor

1. Declare a parameterized version of register predictor with target bus transaction type

// Here "bus_pkt" is the sequence item sent by the target monitor to this predictor
uvm_reg_predictor #(bus_pkt) 		m_apb_predictor;
2. Build the predictor in the register environment

virtual function void build_phase(uvm_phase phase);
	super.build_phase(phase);
	m_apb_predictor = uvm_reg_predictor#(bus_pkt)::type_id::create("m_apb_predictor", this);
endfunction
3. Connect register map, adapter and analysis ports to the predictor

virtual function void connect_phase(uvm_phase phase);
	super.connect_phase(phase);
	// 1. Provide register map to the predictor
    m_apb_predictor.map       = m_ral_model.default_map;
    
    // 2. Provide an adapter to help convert bus packet into register item
    m_apb_predictor.adapter   = m_apb_adapter;
    
    // 3. Connect analysis port of target monitor to analysis implementation of predictor
	m_apb_agent.ap.connect(m_apb_predictor.bus_in);
endfunction

Register environment integration

Let's use all the above components and integrate them in a separate register environment to make it more re-usable.


class reg_env extends uvm_env;
   `uvm_component_utils (reg_env)
   function new (string name="reg_env", uvm_component parent);
      super.new (name, parent);
   endfunction

   uvm_agent 					  m_agent; 				// Agent handle
   ral_my_design              	  m_ral_model;         	// Register Model
   reg2apb_adapter                m_apb_adapter;      	// Convert Reg Tx <-> Bus-type packets
   uvm_reg_predictor #(bus_pkt)   m_apb_predictor; 		// Map APB tx to register in model

   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      m_ral_model       = ral_my_design::type_id::create ("m_ral_model", this);
      m_apb_adapter     = m_apb_adapter :: type_id :: create ("m_apb_adapter");
      m_apb_predictor  	= uvm_reg_predictor #(bus_pkt) :: type_id :: create ("m_apb_predictor", this);

      m_ral_model.build ();
      m_ral_model.lock_model ();
      uvm_config_db #(ral_my_design)::set (null, "uvm_test_top", "m_ral_model", m_ral_model);
   endfunction

   virtual function void connect_phase (uvm_phase phase);
      super.connect_phase (phase);
      m_apb_predictor.map       = m_ral_model.default_map;
      m_apb_predictor.adapter   = m_apb_adapter;
      m_agent.ap.connect(m_apb_predictor.bus_in);
   endfunction   
endclass

There are three components that we have to declare and create in the build_phase(). It is important to note that a register model has to be locked via invocation of its lock() function in order to prevent any other testbench component or part from modifying the structure or adding registers to it. The build() method of the register model is a custom function not a part of standard UVM library, simply to initiate building sub-blocks, maps and regsiters within the model. It's a good idea to place this model somewhere in the configuration database so that other components may access it.

Now we have to provide the predictor with a mapping scheme so that it can match the address values with those of the registers in the model, and also have a handle to the adapter so that it can take the converted bus values directly. This is best done in the connect_phase() method as shown above.

Click here to see the full example !