Chapter 10: UVM-SystemC

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:

  1. uvm_reg::write() calls the internal do_write() method on the uvm_reg_map.
  2. do_write() allocates a generic uvm_reg_item object containing the address, data, and command.
  3. This uvm_reg_item is passed to the reg2bus method of a user-defined uvm_reg_adapter.
  4. The adapter converts it into your specific bus transaction (e.g., a tlm_generic_payload).
  5. The RAL pushes the converted transaction to the Sequencer, and blocks using wait() until the transaction is executed by the Driver.
  6. Upon completion, the RAL calls bus2reg on the adapter to extract the response and updates the internal m_mirrored variable.
// (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