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

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 !