A UVM register environment connects a register model to a real bus, so that calls like reg.write() and reg.read() become protocol transactions on the DUT. This article covers the components such an environment needs to perform register accesses, and how to put them together.

9 min read | Intermediate Level

What You'll Learn

  • The four parts of a register environment and how data flows between them
  • Write a register adapter with reg2bus() and bus2reg()
  • Use a register predictor to keep the model in sync with all bus traffic
  • Integrate the model, adapter and predictor in a reusable environment

A register environment needs four components:

  • A register model built from UVM classes that accurately reflects the values of the design registers
  • An agent that drives real bus transactions to the design using its protocol
  • An adapter that converts the model's read and write requests into protocol-specific bus transactions, and back
  • A predictor that watches bus activity and updates the register model to match the design
Register model environment

A reg.write() call in a sequence goes to the register model, which hands a generic register item to the adapter. The adapter turns it into a bus item for the agent's sequencer, and the driver applies it to the DUT. The monitor observes the transaction, and the predictor uses it to update the model.

Register Adapter

uvm_reg has built-in methods called read() and write() that start a read or write operation on the design.


class reg_ctl extends uvm_reg;
	...
endclass

m_reg_ctl.write (status, wdata); 		// Write wdata to REG_CTL
m_reg_ctl.read  (status, rdata); 		// Read REG_CTL into rdata

The register already knows its own address from the register map, so these calls take only a status variable and the data. Each call creates 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_t 	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 turn read and write calls into real bus accesses, a component called an adapter converts this generic register item into a protocol-specific bus transaction. The adapter must work in both directions. It converts register items into bus transactions, and it converts bus transactions, including read responses, back into register items so that the register model can be updated.

The adapter provides two functions for this, reg2bus() and bus2reg(). As the names imply, reg2bus() converts a register-level object of type uvm_reg_bus_op into a protocol transaction, and bus2reg() converts a bus-level transaction back into a register-level object. Bus protocols differ between designs, so a custom adapter must extend uvm_reg_adapter and override both functions.

The adapter also has two variables that describe the bus:

  • supports_byte_enable - set to 1 if the protocol can enable individual byte lanes, so only some bytes of the data bus are valid
  • provides_responses - set to 1 if the agent's driver returns separate response items that the register model must wait for

// apb_adapter is inherited from "uvm_reg_adapter"
class reg2apb_adapter extends uvm_reg_adapter;
   `uvm_object_utils (reg2apb_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 

The APB protocol does not support byte enables, so supports_byte_enable is set to 0.

If the agent's driver returns a separate response item, for example through put_response() or item_done(rsp), set provides_responses to 1. The register model then waits for that response before converting it back into a register item. This APB driver does not return separate responses, so the bit is set to 0.

If provides_responses is 1 but the driver never sends a response, every register access waits forever and the simulation hangs. The setting must match what the driver actually does.

Quick Check

Q: A read access returns data from the DUT. Which adapter function puts that data into the register model?

Show Answer

bus2reg(). It copies the bus item's data, including the read data, into the uvm_reg_bus_op that the register model receives.

Register Predictor

The register model can keep its copy of the registers in sync with the DUT in two ways:

ModeHow the mirror is updatedLimitation
Implicit (auto) predictionThe model updates the mirror after each of its own read() and write() calls. Enabled with default_map.set_auto_predict(1)Accesses made without the model are not seen
Explicit predictionA predictor watches the bus monitor and updates the mirror for every transaction it observesNeeds a predictor component and a monitor

With implicit prediction, a value written with write() is copied into the mirrored value, and a value returned by read() updates the mirror accordingly.

regmodel role of predictor

However, the register model is not the only way to write to the design. Other sequences can start bus items with an address and data directly on the same agent, as Sequence #2 does in the figure. The model never sees these accesses, so its values go stale.

A predictor solves this. It is connected to the target agent's monitor, receives every observed transaction, and updates 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 extends uvm_component and has an analysis implementation port, bus_in, that receives bus sequence items from the target monitor. It uses the register adapter to convert each incoming bus packet into a generic register item. It then looks up the address in the register map to find the matching register and updates its mirrored value. This works for any protocol, so no custom predictor class is needed. You only create a version parameterized with your bus item type, as shown below.

Where is Explicit Prediction Used?

In SoC testbenches, registers are often written by more than one master: the register model, a CPU model running firmware, or a DMA engine. Explicit prediction is the standard choice there, because it is the only mode that keeps the model correct no matter which master performed the access.

Steps to integrate a predictor

1. Declare a predictor parameterized with the 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 the register map, the adapter and the analysis port 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

All the components above can be combined into a separate register environment, which makes them easier to reuse.


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

   bus_agent 					  m_agent; 				// Agent handle, assigned by the parent env
   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     = reg2apb_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

Three components are declared and created in build_phase(): the register model, the adapter and the predictor. The agent handle m_agent is not created here; the parent environment that owns the agent assigns it.

The register model must be locked by calling its lock_model() function. This prevents other parts of the testbench from changing its structure or adding registers. The model's build() method is a user-defined function, not part of the standard UVM library, that builds the sub-blocks, maps and registers inside the model. It is a good idea to store the model in the configuration database so that tests and sequences can get it.

In connect_phase(), the predictor receives the register map, so it can match addresses with registers in the model, and a handle to the adapter, so it can convert observed bus items. Its bus_in port is then connected to the agent's analysis port.

For the model to send front-door accesses through the agent, its map must also be linked to the agent's sequencer and the adapter, with m_ral_model.default_map.set_sequencer(...). This step is shown in Connecting Register Env.

For a complete working testbench, see the UVM Register Model Example.

Common Mistakes

Mistake #1: Declaring the agent handle with the base type

Wrong: uvm_agent m_agent; followed by m_agent.ap.connect(...)
Right: declare the handle with the actual agent class, such as bus_agent m_agent;

Why? The analysis port ap is a member of your agent class, not of uvm_agent. Accessing it through a base-class handle does not compile.

Mistake #2: Not linking the map to a sequencer

A register model with an adapter and a predictor still cannot reach the DUT through the front door until its map knows which sequencer to use. Without set_sequencer(), every front-door write() and read() fails with an error instead of reaching the DUT.

Mistake #3: A predictor parameterized with the wrong item type

Wrong: uvm_reg_predictor #(uvm_sequence_item) connected to a monitor port of type bus_pkt
Right: uvm_reg_predictor #(bus_pkt), the exact type the monitor writes

Why? The predictor's bus_in port is typed by its parameter, and an analysis port can only connect to an implementation of the same transaction type.

Practice Exercise

Challenge: An Adapter for a Different Item

Your APB agent uses the item apb_item with fields paddr, pwdata, prdata and pwrite. Write reg2bus() and bus2reg() for it. Read data must come from prdata, and write data from pwdata.

Hint

In bus2reg(), check pwrite to decide which data field to copy into rw.data.

Solution

class apb_reg_adapter extends uvm_reg_adapter;
  `uvm_object_utils (apb_reg_adapter)

  function new (string name = "apb_reg_adapter");
    super.new (name);
    supports_byte_enable = 0;                 // APB has no byte lanes
    provides_responses   = 0;                 // Driver returns no separate response
  endfunction

  virtual function uvm_sequence_item reg2bus (const ref uvm_reg_bus_op rw);
    apb_item item = apb_item::type_id::create ("item");
    item.pwrite = (rw.kind == UVM_WRITE);
    item.paddr  = rw.addr;
    item.pwdata = rw.data;                    // Ignored by the DUT on reads
    return item;
  endfunction

  virtual function void bus2reg (uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    apb_item item;
    if (!$cast (item, bus_item))
      `uvm_fatal ("ADAPTER", "bus_item is not an apb_item")
    rw.kind   = item.pwrite ? UVM_WRITE : UVM_READ;
    rw.addr   = item.paddr;
    rw.data   = item.pwrite ? item.pwdata : item.prdata;   // Read data comes from prdata
    rw.status = UVM_IS_OK;
  endfunction
endclass

Copying pwdata on reads is a common bug. It makes every read appear to return the last write data, so the mirror and the read value are wrong.

Summary

In this tutorial, you learned:

  • A register environment combines a register model, a bus agent, an adapter and a predictor
  • Register reads and writes create a generic uvm_reg_bus_op item
  • The adapter converts between uvm_reg_bus_op and bus items with reg2bus() and bus2reg()
  • supports_byte_enable and provides_responses must match the protocol and the driver
  • Explicit prediction with uvm_reg_predictor keeps the mirror correct even when other sequences access the bus
  • The model is built and locked with lock_model() in build_phase, and the predictor is connected in connect_phase
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5