UVM-25: Building the Register Model — VLSI Trainers
VLSI Trainers UVM Series · UVM-25
UVM Series · UVM-25

Building the Register Model

Connecting the RAL model to the testbench — the adapter class (reg2bus / bus2reg), the three prediction modes, implementing the explicit predictor, wiring the env’s connect_phase, and the complete integrated picture from sequence through adapter through driver to DUT.

📋 Integration Architecture

After the register model is built (UVM-24), three more pieces are needed to make it functional inside the testbench. Together they form the complete RAL integration stack:

Complete RAL Integration Stack Sequence reg.write() reg.read() RAL Model gpio_reg_block uvm_reg_bus_op Adapter reg2bus() bus2reg() Sequencer APB seqr Driver APB drv DUT APB registers Monitor observes APB Predictor updates mirror ap.write(txn) predict() updates mirror Stimulus path (top row) and Mirror-update path (bottom row) are the two data flows in RAL integration. set_sequencer(seqr, adapter) in connect_phase links the map to the top-row stimulus path. vlsitrainers.com
Figure 1 — Complete RAL integration stack. Top row (stimulus path): sequence calls reg.write() → RAL generates uvm_reg_bus_op → adapter translates to APB item → sequencer → driver → DUT. Bottom row (mirror update path): monitor observes the bus, broadcasts via ap, predictor receives and calls predict() to update the register model’s internal mirror.

📋 uvm_reg_bus_op — the Generic Item

The RAL framework does not generate bus-specific sequence items. Instead it generates a generic uvm_reg_bus_op struct with six fields. The adapter’s job is to map these fields to and from the bus-specific sequence item.

FieldTypeDescription
kinduvm_access_eUVM_READ or UVM_WRITE
addruvm_reg_addr_tRegister byte address (defaults to 64-bit)
datauvm_reg_data_tWrite data or read-back data (defaults to 64-bit)
n_bitsint unsignedNumber of bits in the transfer
byte_enuvm_reg_byte_en_tByte enable mask
statusuvm_status_eUVM_IS_OK, UVM_IS_X, or UVM_NOT_OK

📋 The Adapter Class

The adapter extends uvm_reg_adapter and overrides two pure virtual functions: reg2bus() converts a uvm_reg_bus_op into a bus sequence item; bus2reg() does the reverse — after the driver completes, it extracts the response from the bus item and populates the uvm_reg_bus_op struct.

Two properties must be set in the constructor:

📋 Complete APB Adapter

class reg2apb_adapter extends uvm_reg_adapter;
  `uvm_object_utils(reg2apb_adapter)

  function new(string name = "reg2apb_adapter");
    super.new(name);
    // APB has no byte enables
    supports_byte_enable = 0;
    // APB driver uses get_next_item()/item_done() — no separate response
    provides_responses   = 0;
  endfunction

  // ── reg2bus: RAL → APB item ───────────────────────────────
  // Called when reg.write() or reg.read() is invoked
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    apb_seq_item apb;
    apb = apb_seq_item::type_id::create("apb");
    // Map generic fields to APB fields
    apb.read_not_write = (rw.kind == UVM_READ) ? 1 : 0;
    apb.addr           = rw.addr[11:0];  // trim to 12-bit APB address
    apb.write_data     = rw.data;
    apb.byte_en        = 4'b1111;        // full word — APB has no byte_en
    return apb;
  endfunction

  // ── bus2reg: APB item → RAL ───────────────────────────────
  // Called when driver completes — extracts response into rw struct
  virtual function void bus2reg(uvm_sequence_item bus_item,
                                  ref uvm_reg_bus_op rw);
    apb_seq_item apb;
    if (!$cast(apb, bus_item))
      `uvm_fatal("ADPT", "bus_item is not an apb_seq_item")

    rw.kind   = apb.read_not_write ? UVM_READ : UVM_WRITE;
    rw.addr   = apb.addr;
    rw.data   = apb.read_data;   // PRDATA on reads
    // APB PSLVERR → UVM status
    rw.status = apb.error ? UVM_NOT_OK : UVM_IS_OK;
  endfunction

endclass
In bus2reg() always populate rw.data from the read-data field, not the write-data field. For read operations, the driver populates read_data (PRDATA) — not write_data. If bus2reg() reads from the wrong field, all register reads return zero silently, with no error.

📋 provides_responses Flag — Getting It Right

This is the single most common source of RAL integration failures. The flag must match what the driver actually does:

provides_responses — Two Driver Models provides_responses = 0 Driver uses get_next_item() + item_done() Response is in the same item object item_done() completes the exchange APB driver → set to 0 provides_responses = 1 Driver uses get() + put(rsp) Request and response are separate items RAL waits for put(rsp) before completing Pipelined AHB driver → set to 1 vlsitrainers.com
Figure 2 — provides_responses must match the driver API model. A mismatch in either direction causes a failure: setting it to 1 when the driver uses item_done() locks the simulation (waiting for a response that never comes). Setting it to 0 when the driver uses put(rsp) causes the register method to return immediately with wrong data.
Driver modelprovides_responses valueEffect if wrong
get_next_item() + item_done()0Set to 1 → simulation locks waiting for response that never comes
get() + put(rsp)1Set to 0 → reg.write/read returns immediately with wrong data; bus activity happens later

📋 Three Prediction Modes

The register model maintains a mirror — a software copy of the expected hardware register state. After each register access, the mirror must be updated. There are three ways this can happen:

ModeHow mirror is updatedEnable / disableWhen to use
Auto predictionThe RAL automatically calls predict() at the end of each access it initiates. Only tracks accesses made through the RAL itself.Enable: map.set_auto_predict(1)Simple environments with no other bus masters. Quick prototyping.
Explicit prediction (default)An external uvm_reg_predictor component listens on the monitor’s analysis port and calls predict() for every observed bus transfer — including those not initiated by the RAL.Default — no extra call needed. Disable auto: map.set_auto_predict(0)Recommended for all production environments. Works with multiple bus masters and passive reuse.
Passive predictionSame as explicit prediction but the register model is used in read-only / observation mode — the sequences do not drive any bus accesses.Achieved by using passive APB agent + predictor. No register stimulus.Integration level — block-level reg model updated passively from the internal bus monitor.
Use explicit prediction in all production environments. Auto prediction only tracks accesses that go through the RAL. Any register write that happens via a raw bus sequence, a DMA controller, or a second bus master will not update the mirror. Explicit prediction sees everything — it hooks into the monitor and updates the mirror for every observed bus transfer regardless of source.

📋 Explicit Predictor Wiring

The built-in uvm_reg_predictor #(BUS_ITEM) component is parameterised on the bus item type. It subscribes to the monitor’s analysis port, uses the adapter to decode each bus item into a register operation, looks up the target register in the map, and calls predict() on it.

// ── In gpio_env: declare the predictor ───────────────────
class gpio_env extends uvm_env;
  `uvm_component_utils(gpio_env)

  gpio_agent    m_agent;
  gpio_reg_block m_reg_block;
  reg2apb_adapter m_adapter;

  // Explicit predictor — parameterised on bus item type
  uvm_reg_predictor #(apb_seq_item) m_predictor;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);

    // Get register block from config
    if (!uvm_config_db #(gpio_reg_block)::get(
          this, "", "gpio_rm", m_reg_block))
      `uvm_fatal("ENV", "No register model!")

    m_agent     = gpio_agent::type_id::create("m_agent",     this);
    m_adapter   = reg2apb_adapter::type_id::create("m_adapter");
    m_predictor = uvm_reg_predictor #(apb_seq_item)::type_id::
                    create("m_predictor", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    // ① Link map → sequencer via adapter (stimulus path)
    m_reg_block.APB_map.set_sequencer(m_agent.m_seqr, m_adapter);

    // ② Configure predictor (mirror-update path)
    m_predictor.map     = m_reg_block.APB_map;
    m_predictor.adapter = m_adapter;

    // ③ Connect monitor ap → predictor input
    m_agent.ap.connect(m_predictor.bus_in);
  endfunction

endclass

📋 Env connect_phase — Full Wiring

Three lines in connect_phase wire up the complete RAL integration. Each targets a different data path:

function void connect_phase(uvm_phase phase);

  // ── LINE 1: Stimulus path ─────────────────────────────────
  // map.set_sequencer(sequencer, adapter)
  // When reg.write/read is called, the map sends a uvm_reg_bus_op
  // to the adapter, which converts it to an APB item and sends
  // it to the sequencer → driver → DUT.
  m_reg_block.APB_map.set_sequencer(m_agent.m_seqr, m_adapter);

  // ── LINE 2: Mirror-update path (predictor config) ─────────
  // Tells the predictor which map to look up registers in,
  // and which adapter to use to decode APB items.
  m_predictor.map     = m_reg_block.APB_map;
  m_predictor.adapter = m_adapter;

  // ── LINE 3: Mirror-update path (analysis connection) ──────
  // Every bus transfer observed by the monitor is forwarded
  // to the predictor's bus_in export via the analysis port.
  m_agent.ap.connect(m_predictor.bus_in);

endfunction
All three lines are required for correct operation. Missing line 1 means register method calls produce no bus activity. Missing line 2 means the predictor cannot decode bus items. Missing line 3 means the predictor never sees any transactions and the mirror is never updated.

📋 Sub-System Register Maps

At integration level, multiple block-level register blocks are composed into a sub-system block. Each sub-block is added to the integration-level map with a base address offset. The result is a single flat address map that covers all registers across all blocks.

class pss_reg_block extends uvm_reg_block;
  `uvm_object_utils(pss_reg_block)

  rand gpio_reg_block gpio;   // GPIO block at offset 0x0000
  rand spi_reg_block  spi;    // SPI block at offset 0x1000

  uvm_reg_map AHB_map;

  function new(string name = "pss_reg_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction

  virtual function void build();
    // Create AHB integration map: base=0, 4-byte wide, little-endian
    AHB_map = create_map("AHB_map", 32'h0, 4, UVM_LITTLE_ENDIAN);

    // Create and configure GPIO sub-block
    gpio = gpio_reg_block::type_id::create("gpio");
    gpio.configure(this);     // parent = this pss_reg_block
    gpio.build();

    // Add GPIO's default_map to AHB_map at offset 0x0000
    AHB_map.add_submap(gpio.default_map, 32'h0000);

    // Create and configure SPI sub-block
    spi = spi_reg_block::type_id::create("spi");
    spi.configure(this);
    spi.build();

    // Add SPI's default_map to AHB_map at offset 0x1000
    AHB_map.add_submap(spi.default_map, 32'h1000);

    lock_model();
  endfunction
endclass

// ── Integration env connect_phase: AHB map → AHB sequencer
pss_rm.AHB_map.set_sequencer(m_ahb_agent.m_seqr, m_ahb_adapter);
m_predictor.map     = pss_rm.AHB_map;
m_predictor.adapter = m_ahb_adapter;
m_ahb_agent.ap.connect(m_predictor.bus_in);
Access GPIO registers at integration level using the full PSS path. At block level you write gpio_rm.dir_reg.write(...). At integration level with the pss_reg_block, the same register is accessed as pss_rm.gpio.dir_reg.write(...). The address is automatically resolved to 0x0000 + 0x04 = 0x0004 via the nested map offsets. Block-level sequences using gpio_rm can be reused at integration level by pointing their reg block handle at pss_rm.gpio.

📋 Common Integration Pitfalls

PitfallSymptomFix
Wrong provides_responses value0 when should be 1: reg method returns instantly with wrong read data. 1 when should be 0: simulation locks, reg.write never returns.Match to driver API: 0 for get_next_item/item_done, 1 for get/put(rsp).
Missing set_sequencer() callreg.write() and reg.read() do nothing — no APB traffic observed.Always call APB_map.set_sequencer(seqr, adapter) in env connect_phase.
Mirror not updated (no predictor)reg.read() returns stale mirror value. mirror() checks always pass even when DUT data is wrong.Instantiate uvm_reg_predictor and wire ap.connect(predictor.bus_in).
Predictor map not setPredictor compiles but crashes at runtime: null handle when looking up register.Always set both: m_predictor.map = APB_map and m_predictor.adapter = m_adapter.
register block build() not calledAll field handles are null. Fatal error on first register access.Always call reg_block.build() explicitly after construction.
lock_model() not calledAddress resolution fails. Registers have no address. Front-door access uses address 0 for all registers.lock_model() must be the last statement in the block’s build() method.
Sub-map offset wrongRegisters accessed at wrong hardware address. Writes go to wrong register silently.Verify hardware memory map document and match add_submap() offset exactly.

📋 Quick Reference

ItemKey fact
Three integration piecesAdapter (reg2bus/bus2reg) + set_sequencer() + Predictor (mirror updates)
uvm_reg_bus_op fieldskind, addr, data, n_bits, byte_en, status
Adapter base classuvm_reg_adapter — extend and override reg2bus() and bus2reg()
reg2bus() purposeConverts uvm_reg_bus_op → bus-specific sequence item. Returns uvm_sequence_item.
bus2reg() purposeConverts bus-specific item → uvm_reg_bus_op. Extracts read data and status.
supports_byte_enable0 for APB (no byte enables). 1 for AHB/AXI with HSTRB/WSTRB.
provides_responses0 for item_done() drivers. 1 for get()/put(rsp) pipelined drivers.
Stimulus path wiringmap.set_sequencer(seqr, adapter) in env connect_phase
Explicit predictionDefault mode. External predictor observes all bus activity, updates mirror.
Auto predictionEnable: map.set_auto_predict(1). Only tracks accesses initiated by RAL.
Predictor typeuvm_reg_predictor #(BUS_ITEM) parameterised on bus item type
Predictor configm_predictor.map = APB_map; m_predictor.adapter = m_adapter;
Predictor connectionm_agent.ap.connect(m_predictor.bus_in)
Sub-system mapAHB_map.add_submap(gpio.default_map, base_offset)
Access GPIO at ILpss_rm.gpio.dir_reg.write(...) — address resolved through nested maps
Scroll to Top