The UVM-SystemC Register Layer
How to map memory-mapped registers into UVM-SystemC using uvm_reg, uvm_reg_map, and frontdoor adapters.
How to Read This Lesson
UVM-SystemC is methodology in C++ clothing. Keep the verification intent in view: reusable components, controlled stimulus, reporting, and phase-aware execution.
UVM-SystemC Register Layer (uvm_reg)
The Register Abstraction Layer (RAL) is arguably the most powerful feature of UVM. It allows you to create an abstract, object-oriented model of your hardware's memory-mapped registers, decoupling your test sequences from the physical bus protocol (e.g., TLM-2.0, APB, AXI).
In UVM-SystemC, the uvm_reg classes behave identically to their SystemVerilog counterparts. Let's dig into the Accellera UVM-SystemC source code to see how these abstractions are actually executed under the hood.
Source and LRM Trail
For UVM-SystemC, use Docs/LRMs/uvm-systemc-language-reference-manual.pdf as the methodology contract. In source, inspect .codex-src/uvm-systemc/src/uvmsc: components, phases, factory macros, sequences, sequencers, TLM ports, reporting, and configuration helpers.
1. Defining a Register
Registers are defined by inheriting from uvm_reg and instantiating uvm_reg_field objects inside the build() method.
When you call configure() on a uvm_reg_field, the UVM-SystemC kernel allocates internal data structures to track both the mirrored value (what the testbench thinks the hardware holds) and the desired value (what the testbench wants to write to the hardware).
#include <systemc>
#include <uvm>
class ctrl_reg : public uvm::uvm_reg {
public:
uvm::uvm_reg_field* enable;
uvm::uvm_reg_field* irq_mask;
UVM_OBJECT_UTILS(ctrl_reg);
ctrl_reg(const std::string& name = "ctrl_reg")
: uvm::uvm_reg(name, 32, uvm::UVM_NO_COVERAGE) {}
virtual void build() {
enable = uvm::uvm_reg_field::type_id::create("enable");
// Parameters: parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, obj
enable->configure(this, 1, 0, "RW", 0, 0, 1, 1, nullptr);
irq_mask = uvm::uvm_reg_field::type_id::create("irq_mask");
irq_mask->configure(this, 8, 8, "RW", 0, 0xFF, 1, 1, nullptr);
}
};2. Assembling the Register Block
Registers are grouped into a uvm_reg_block, which contains a uvm_reg_map that defines their physical addresses.
When you call map->add_reg(), the kernel updates an internal memory map (std::map<uint64_t, uvm_reg*>). The lock_model() method seals the block, preventing further additions and caching the hierarchical paths for fast lookup.
class sys_reg_block : public uvm::uvm_reg_block {
public:
ctrl_reg* ctrl;
uvm::uvm_reg_map* map;
UVM_OBJECT_UTILS(sys_reg_block);
sys_reg_block(const std::string& name = "sys_reg_block")
: uvm::uvm_reg_block(name, uvm::UVM_NO_COVERAGE) {}
virtual void build() {
ctrl = ctrl_reg::type_id::create("ctrl");
ctrl->configure(this, nullptr);
ctrl->build();
// Create the memory map: name, base_addr, bus_width (bytes), endianness
map = create_map("map", 0x0000, 4, uvm::UVM_LITTLE_ENDIAN);
// Add register to map at offset 0x10, with RW access
map->add_reg(ctrl, 0x10, "RW");
lock_model(); // Seal the RAL block
}
};3. The Adapter (Bridging RAL and TLM)
When a sequence calls ctrl->write(status, 0x1), the RAL does not instantly write to the bus. Instead, the UVM-SystemC kernel performs the following steps:
uvm_reg::write()calls the internaldo_write()method on theuvm_reg_map.do_write()allocates a genericuvm_reg_itemobject containing the address, data, and command.- This
uvm_reg_itemis passed to thereg2busmethod of a user-defineduvm_reg_adapter. - The adapter converts it into your specific bus transaction (e.g., a
tlm_generic_payload). - The RAL pushes the converted transaction to the Sequencer, and blocks using
wait()until the transaction is executed by the Driver. - Upon completion, the RAL calls
bus2regon the adapter to extract the response and updates the internalm_mirroredvariable.
// (Pseudocode for Adapter)
class tlm_reg_adapter : public uvm::uvm_reg_adapter {
virtual uvm::uvm_sequence_item* reg2bus(const uvm::uvm_reg_bus_op& rw) {
// Convert 'rw' to your bus transaction
// e.g., allocate a tlm_generic_payload
}
virtual void bus2reg(uvm::uvm_sequence_item* bus_item, uvm::uvm_reg_bus_op& rw) {
// Convert your bus transaction back to 'rw'
// e.g., extract response status from tlm_generic_payload
}
};This elegant layering guarantees that if the hardware team changes the bus architecture from APB to TLM-2.0, you only need to rewrite the tlm_reg_adapter. The high-level tests calling ctrl->write() remain completely untouched!
int sc_main(int argc, char* argv[]) {
// Standard UVM execution
uvm::uvm_root::get()->run_test();
return 0;
}
Comments and Corrections