Chapter 5 of 6
UVM RAL: the Register Abstraction Layer
A real uvm_reg model for axil_regfile's four registers, a uvm_reg_adapter bridging front-door access to axi_txn, and the concrete payoff: RAL's mirror only updates from its own register's bus accesses, so COUNT's mirror stays stale at 0 through four DATA writes -- a real gap only a front-door read closes.
Chapter 1's register map (CTRL/STATUS/DATA/COUNT) has been accessed by raw address in every chapter so far — write(8'h08, 32'hAA), never write DATA. UVM RAL is the standardized way to stop doing that: a uvm_reg object per register, named fields instead of bit positions, and a documented, real gap in what RAL can track automatically that this chapter's payoff makes concrete rather than just asserting.
What this chapter deliberately leaves out
A full RAL deployment usually pairs front-door access (a real bus transaction, what this chapter builds) with a register predictor — a passive component that snoops bus traffic and updates the register model's mirror even for accesses RAL didn't initiate itself. That's real, useful machinery, and it's deliberately left out here to keep this chapter approachable — front-door read()/write() plus a manual mirror-vs-actual check is the whole payoff, and as the last section shows, that manual check is doing genuine work, not just a placeholder for automation that would make it unnecessary.
The register model: one uvm_reg per row of the map
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
rand uvm_reg_field enable;
rand uvm_reg_field irq_clr;
function new(string name = "ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
enable = uvm_reg_field::type_id::create("enable");
enable.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
irq_clr = uvm_reg_field::type_id::create("irq_clr");
irq_clr.configure(this, 1, 1, "WO", 0, 1'b0, 1, 1, 0);
endfunction
endclass
class status_reg extends uvm_reg;
`uvm_object_utils(status_reg)
rand uvm_reg_field irq;
function new(string name = "status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
irq = uvm_reg_field::type_id::create("irq");
irq.configure(this, 1, 0, "RO", 1, 1'b0, 1, 0, 0);
endfunction
endclass
class data_reg extends uvm_reg;
`uvm_object_utils(data_reg)
rand uvm_reg_field value;
function new(string name = "data_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
value = uvm_reg_field::type_id::create("value");
value.configure(this, 32, 0, "RW", 0, 32'h0, 1, 1, 0);
endfunction
endclass
class count_reg extends uvm_reg;
`uvm_object_utils(count_reg)
rand uvm_reg_field value;
function new(string name = "count_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
value = uvm_reg_field::type_id::create("value");
value.configure(this, 32, 0, "RO", 1, 32'h0, 1, 0, 0);
endfunction
endclassuvm_reg_field::configure's fourth argument (volatile) is the one worth reading carefully: status_reg.irq and count_reg.value are marked volatile (1), ctrl_reg.enable and data_reg.value aren't. That's a direct, deliberate reflection of chapter 1's RTL: CTRL.ENABLE and DATA only ever change in response to a bus access to their own address, so RAL's normal write-then-trust-the-mirror behavior is accurate for them. COUNT and STATUS.IRQ change as a side effect of writes to a completely different address (DATA, CTRL) — marking them volatile is how the register model itself documents "don't trust a cached value for this field," which is exactly the gap the last section of this chapter demonstrates concretely. UVM_NO_COVERAGE opts out of RAL's built-in functional coverage modeling — measuring coverage is dv-methodology's job, not this chapter's.
regfile_reg_block: binding the registers to a map
class regfile_reg_block extends uvm_reg_block;
`uvm_object_utils(regfile_reg_block)
rand ctrl_reg CTRL;
rand status_reg STATUS;
rand data_reg DATA;
rand count_reg COUNT;
function new(string name = "regfile_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
CTRL = ctrl_reg::type_id::create("CTRL");
CTRL.configure(this);
CTRL.build();
STATUS = status_reg::type_id::create("STATUS");
STATUS.configure(this);
STATUS.build();
DATA = data_reg::type_id::create("DATA");
DATA.configure(this);
DATA.build();
COUNT = count_reg::type_id::create("COUNT");
COUNT.configure(this);
COUNT.build();
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
default_map.add_reg(CTRL, 8'h00, "RW");
default_map.add_reg(STATUS, 8'h04, "RO");
default_map.add_reg(DATA, 8'h08, "RW");
default_map.add_reg(COUNT, 8'h0C, "RO");
lock_model();
endfunction
endclasscreate_map's 4 is the map's bus width in bytes (32-bit axi4lite_if, chapter 1); the four add_reg offsets are chapter 1's register map, unchanged, just expressed as named calls instead of a table a human has to cross-reference by hand. uvm_reg_block extends uvm_object, not uvm_component — there's no phase mechanism building it automatically, which is why regfile_env's build_phase below calls .build() on it explicitly. lock_model() finalizes the register model once every register is added — standard practice, called once, at the end.
axi_reg_adapter: translating between RAL and axi_txn
RAL's register-level API doesn't know anything about AXI4-Lite — it operates on an abstract uvm_reg_bus_op (kind/addr/data/status) and needs a translator to and from whatever bus transaction type the actual driver understands. uvm_reg_adapter is that translator:
class axi_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(axi_reg_adapter)
function new(string name = "axi_reg_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0;
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
axi_txn txn = axi_txn::type_id::create("txn");
txn.is_write = (rw.kind == UVM_WRITE);
txn.addr = rw.addr[7:0];
if (rw.kind == UVM_WRITE) txn.wdata = rw.data;
return txn;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
axi_txn txn;
if (!$cast(txn, bus_item)) `uvm_fatal("REG_ADAPTER", "bus2reg: cast failed")
rw.kind = txn.is_write ? UVM_WRITE : UVM_READ;
rw.addr = txn.addr;
rw.data = txn.is_write ? txn.wdata : txn.rdata;
rw.status = (txn.resp == 2'b00) ? UVM_IS_OK : UVM_NOT_OK;
endfunction
endclassreg2bus turns an abstract register operation into a real axi_txn — exactly the same class chapter 2's driver already knows how to drive, so nothing about axi_driver changes. bus2reg runs after the transaction completes and reports back whether it succeeded — rw.status is where chapter 1's DECERR/SLVERR distinction would eventually surface to RAL, though this chapter's scenario never triggers either.
Wiring it up: regfile_env and regfile_vsqr
regfile_vsqr (chapter 4) gains a second field alongside axi_sqr, and regfile_env builds the register model and binds it to the real sequencer through the adapter:
class regfile_vsqr extends uvm_sequencer;
`uvm_component_utils(regfile_vsqr)
axi_sequencer axi_sqr;
regfile_reg_block regmodel;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
class regfile_env extends uvm_env;
`uvm_component_utils(regfile_env)
axi_agent axi_agt;
irq_agent irq_agt;
irq_watcher irqw;
regfile_vsqr vsqr;
regfile_reg_block regmodel;
axi_reg_adapter adapter;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
axi_agt = axi_agent::type_id::create("axi_agt", this);
irq_agt = irq_agent::type_id::create("irq_agt", this);
irqw = irq_watcher::type_id::create("irqw", this);
vsqr = regfile_vsqr::type_id::create("vsqr", this);
regmodel = regfile_reg_block::type_id::create("regmodel");
regmodel.build();
adapter = axi_reg_adapter::type_id::create("adapter");
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
irq_agt.mon.ap.connect(irqw.imp);
vsqr.axi_sqr = axi_agt.sqr;
vsqr.regmodel = regmodel;
regmodel.default_map.set_sequencer(axi_agt.sqr, adapter);
endfunction
endclassset_sequencer is what closes the loop: once it's called, any sequence issuing a front-door register access through regmodel.default_map gets routed to axi_agt.sqr automatically, through axi_reg_adapter. That's a genuine simplification over chapter 4's approach — a sequence using RAL never has to name a sequencer explicitly the way start_item(req, -1, p_sequencer.axi_sqr) did.
The payoff: mirror vs. actual
class regfile_ral_seq extends uvm_sequence;
`uvm_object_utils(regfile_ral_seq)
`uvm_declare_p_sequencer(regfile_vsqr)
function new(string name = "regfile_ral_seq");
super.new(name);
endfunction
task body();
uvm_status_e status;
uvm_reg_data_t rdata;
regfile_reg_block regmodel = p_sequencer.regmodel;
regmodel.CTRL.write(status, 32'h1, UVM_FRONTDOOR, regmodel.default_map, this);
regmodel.DATA.write(status, 32'hAA, UVM_FRONTDOOR, regmodel.default_map, this);
regmodel.DATA.write(status, 32'hBB, UVM_FRONTDOOR, regmodel.default_map, this);
regmodel.DATA.write(status, 32'hCC, UVM_FRONTDOOR, regmodel.default_map, this);
regmodel.DATA.write(status, 32'hDD, UVM_FRONTDOOR, regmodel.default_map, this);
if (regmodel.COUNT.get() == 0)
`uvm_info("RAL", "as expected: COUNT's mirror is still 0 -- nothing ever accessed COUNT's own address, so RAL never predicted the increments that happened as a side effect of the DATA writes", UVM_LOW)
else
`uvm_error("RAL", "unexpected: COUNT's mirror changed without a bus access to COUNT's own address")
regmodel.COUNT.read(status, rdata, UVM_FRONTDOOR, regmodel.default_map, this);
if (rdata == 32'd4)
`uvm_info("RAL", $sformatf("confirmed: a real front-door read reports COUNT=%0d, and the mirror is resynced to match", rdata), UVM_LOW)
else
`uvm_error("RAL", $sformatf("expected COUNT=4 after 4 enabled DATA writes, got %0d", rdata))
regmodel.CTRL.write(status, 32'h3, UVM_FRONTDOOR, regmodel.default_map, this);
regmodel.STATUS.read(status, rdata, UVM_FRONTDOOR, regmodel.default_map, this);
if (rdata[0] == 1'b0)
`uvm_info("RAL", "confirmed: STATUS.IRQ reads back 0 after IRQ_CLR", UVM_LOW)
else
`uvm_error("RAL", "expected STATUS.IRQ to read 0 after IRQ_CLR")
endtask
endclassregmodel.COUNT.get() never touches the bus — it just returns whatever the model currently believes, no different from asking any plain variable for its value. After four DATA writes, that belief is still 0, because RAL only updates a register's mirror in response to a bus access at that register's own address, and nothing here ever accessed COUNT's address directly. This isn't a bug in the register model or a missing feature — it's the direct, correct consequence of marking COUNT.value volatile and never wiring up the predictor this chapter deliberately left out. regmodel.COUNT.read(...) is the fix: a real front-door access to COUNT's own address, which both reports the DUT's actual value (4) and resynchronizes the mirror to match it. The gap between get() and read() here is the whole reason "mirror vs. actual" is a real verification concern and not just bookkeeping — a test that only ever trusted get() for a volatile field would have silently believed something false for the entire four-write sequence.
Summary
uvm_reg/uvm_reg_field/uvm_reg_block/uvm_reg_mapare real RAL machinery — named registers and fields standing in foraxil_regfile's address map, not a simplified stand-in.- Marking
STATUS.IRQandCOUNT.valuevolatile (andCTRL.ENABLE/DATA.valuenot) is a direct, accurate reflection of chapter 1's RTL: which fields change only via their own bus address, and which change as a side effect of a write somewhere else. uvm_reg_adapter'sreg2bus/bus2regbridge RAL's abstractuvm_reg_bus_opto the concreteaxi_txnchapter 2's driver already knows how to drive — no changes toaxi_driverat all.set_sequenceron the register map means a sequence using RAL never has to name a target sequencer explicitly, unlike chapter 4'sstart_item(..., p_sequencer.axi_sqr).- The concrete payoff:
COUNT.get()(trust the mirror) reports0through fourDATAwrites that actually raisedCOUNTto4;COUNT.read()(go ask the DUT) reports the truth and resyncs the mirror. This is a real, measurable gap RAL's model documents honestly through thevolatileflag, not something the manual check merely restates.
After four DATA writes that raise the DUT's real COUNT to 4, what does regmodel.COUNT.get() return, and why?
Why is count_reg.value's uvm_reg_field configured with volatile=1, while data_reg.value is configured with volatile=0?
Which uvm_reg_map method binds a register model's front-door accesses to a real sequencer and adapter, closing the loop between RAL and the actual bus?