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.
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:
uvm_reg_bus_op struct and the bus-specific sequence item. One adapter per bus protocol.map.set_sequencer(seqr, adapter) in connect_phase routes register method calls to the correct sequencer.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.
| Field | Type | Description |
|---|---|---|
kind | uvm_access_e | UVM_READ or UVM_WRITE |
addr | uvm_reg_addr_t | Register byte address (defaults to 64-bit) |
data | uvm_reg_data_t | Write data or read-back data (defaults to 64-bit) |
n_bits | int unsigned | Number of bits in the transfer |
byte_en | uvm_reg_byte_en_t | Byte enable mask |
status | uvm_status_e | UVM_IS_OK, UVM_IS_X, or UVM_NOT_OK |
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:
put(rsp)). Set to 0 if the driver uses get_next_item()/item_done() and the response is in the same item. Getting this wrong locks the simulation or silently returns wrong data.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
read_data (PRDATA) — not write_data. If bus2reg() reads from the wrong field, all register reads return zero silently, with no error.This is the single most common source of RAL integration failures. The flag must match what the driver actually does:
| Driver model | provides_responses value | Effect if wrong |
|---|---|---|
| get_next_item() + item_done() | 0 | Set to 1 → simulation locks waiting for response that never comes |
| get() + put(rsp) | 1 | Set to 0 → reg.write/read returns immediately with wrong data; bus activity happens later |
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:
| Mode | How mirror is updated | Enable / disable | When to use |
|---|---|---|---|
| Auto prediction | The 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 prediction | Same 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. |
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
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
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);
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.| Pitfall | Symptom | Fix |
|---|---|---|
| Wrong provides_responses value | 0 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() call | reg.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 set | Predictor 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 called | All field handles are null. Fatal error on first register access. | Always call reg_block.build() explicitly after construction. |
| lock_model() not called | Address 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 wrong | Registers accessed at wrong hardware address. Writes go to wrong register silently. | Verify hardware memory map document and match add_submap() offset exactly. |
| Item | Key fact |
|---|---|
| Three integration pieces | Adapter (reg2bus/bus2reg) + set_sequencer() + Predictor (mirror updates) |
| uvm_reg_bus_op fields | kind, addr, data, n_bits, byte_en, status |
| Adapter base class | uvm_reg_adapter — extend and override reg2bus() and bus2reg() |
| reg2bus() purpose | Converts uvm_reg_bus_op → bus-specific sequence item. Returns uvm_sequence_item. |
| bus2reg() purpose | Converts bus-specific item → uvm_reg_bus_op. Extracts read data and status. |
| supports_byte_enable | 0 for APB (no byte enables). 1 for AHB/AXI with HSTRB/WSTRB. |
| provides_responses | 0 for item_done() drivers. 1 for get()/put(rsp) pipelined drivers. |
| Stimulus path wiring | map.set_sequencer(seqr, adapter) in env connect_phase |
| Explicit prediction | Default mode. External predictor observes all bus activity, updates mirror. |
| Auto prediction | Enable: map.set_auto_predict(1). Only tracks accesses initiated by RAL. |
| Predictor type | uvm_reg_predictor #(BUS_ITEM) parameterised on bus item type |
| Predictor config | m_predictor.map = APB_map; m_predictor.adapter = m_adapter; |
| Predictor connection | m_agent.ap.connect(m_predictor.bus_in) |
| Sub-system map | AHB_map.add_submap(gpio.default_map, base_offset) |
| Access GPIO at IL | pss_rm.gpio.dir_reg.write(...) — address resolved through nested maps |