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 reads and writes are normally performed by calling appropriate
write() methods that belong to the
uvm_reg class from where registers for the design are inherited.
class reg_ctl extends uvm_reg; ... endclass m_reg_ctl.write (status, 16'hcafe); m_reg_ctl.read (status, rdata);
But inorder to do a FRONTDOOR access, we need to convert the UVM write and read methods into real bus protocol transactions that can be driven to the DUT via its own bus interface. This conversion process is facilitated by the adapter via
bus2reg() functions. As the names imply,
reg2bus() convert register level objects into a protocol transaction and
bus2reg() vice versa. An example of how these methods work might make it more clear.
Based on the design protocol, we have to create and define a separate adapter that inherits from
uvm_reg_adapter. The method
reg2bus() accepts an item of type
uvm_reg_bus_op and assigns appropriate address, data and direction values to a specified sequence item which gets returned when this function is called.
class reg2apb_adapter extends uvm_reg_adapter; `uvm_object_utils (reg2apb_adapter) // define new function here 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 virtual function void bus2reg (uvm_sequence_item bus_item, ref uvm_reg_bus_op rw); bus_pkt 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; `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
bus2reg() does the exact opposite by accepting a bus based packet and assigns appropriate address, data and direction information to an item of type
The role of a predictor is to update the register model mirror values based on observed bus transactions. The adapter has already converted bus specific transaction to an item type that can be understood by the model. The predictor has to determine the register being accessed based on bus address, and then update the register's mirror value. 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.
uvm_reg_predictor #(bus_pkt) m_apb2reg_predictor;
Register environment integration
Let's use all the above components and integrate them in a separate register environment to make things 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 ral_my_design m_ral_model; // Register Model reg2apb_adapter m_reg2apb; // Convert Reg Tx <-> Bus-type packets uvm_reg_predictor #(bus_pkt) m_apb2reg_predictor; // Map APB tx to register in model virtual function void build_phase (uvm_phase phase); super.build_phase (phase); m_ral_model = ral_sys_my_design::type_id::create ("m_ral_model", this); m_reg2apb = reg2apb_adapter :: type_id :: create ("m_reg2apb"); m_apb2reg_predictor = uvm_reg_predictor #(bus_pkt) :: type_id :: create ("m_apb2reg_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_apb2reg_predictor.map = m_ral_model.default_map; m_apb2reg_predictor.adapter = m_reg2apb; 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 me to see a complete example !