Register Abstraction (RAL)¶
What problem RAL solves¶
Any DUT with a register/memory-mapped interface needs testbench code to read/write those registers. Hand-writing that code — one task per register, manually tracking bit-field offsets, manually keeping a shadow "expected value" in sync with what was last written — is tedious and, worse, silently drifts out of sync with the actual register spec as the design evolves. RAL generates this entire layer from a register description (hand-written or, more commonly, generated from an IP-XACT/spec source), and keeps a mirror (expected-value model) automatically in sync as accesses happen.
The model hierarchy¶
uvm_reg_block (the whole register map)
└── uvm_reg_map (one address-space view onto that map)
└── uvm_reg (one register)
└── uvm_reg_field (one bit-field within that register)
class ctrl_reg extends uvm_reg;
rand uvm_reg_field enable;
rand uvm_reg_field mode;
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);
mode = uvm_reg_field::type_id::create("mode");
mode.configure(this, 2, 1, "RW", 0, 2'b00, 1, 1, 0);
endfunction
endclass
The configure() arguments encode field width, bit offset, access policy ("RW", "RO", "W1C", ...), reset value, and whether the field is individually accessible — this is the part that's almost always generated from a spec rather than hand-typed once a register map has more than a handful of registers, since hand-transcription errors here are a real, recurring source of testbench-vs-spec drift.
Front-door vs. back-door access¶
- Front-door: goes through the actual bus protocol (e.g. an AXI write), exactly as real hardware/software would access the register. Exercises the real access path, including timing and protocol — but costs simulation cycles.
- Back-door: writes/reads the register's underlying storage directly via a
hdl_pathinto the RTL, bypassing the bus entirely. Instant, but doesn't test the bus path or protocol timing at all.
ctrl_reg.write(status, 32'h1); // front-door by default
ctrl_reg.write(status, 32'h1, UVM_BACKDOOR); // explicit back-door
Back-door access requires the register model to be bound to actual RTL signal paths (add_hdl_path) — it's a simulation-only mechanism (no equivalent on real silicon), useful for fast test setup (e.g. pre-loading state before the interesting part of a test) where you deliberately don't want to spend cycles on the setup itself.
The mirror and desired/mirrored values¶
Every uvm_reg keeps two internal value copies:
desired: what you intend the register to become (set byset(), not yet sent to the DUT)mirrored: the model's best understanding of the register's actual current hardware value, updated automatically after awrite()/read()completes
ctrl_reg.set(32'h1); // sets "desired" only — nothing has happened to the DUT yet
ctrl_reg.update(); // if desired != mirrored, issues a write to sync the DUT — else does nothing
ctrl_reg.read(status, actual_value); // reads the DUT AND updates "mirrored" to match
ctrl_reg.mirror(status, UVM_CHECK); // reads the DUT and compares against mirrored, flags a UVM_ERROR on mismatch
mirror(..., UVM_CHECK) is the standard way to catch register-model-vs-hardware drift as an automatic check, rather than only noticing when some unrelated functional check downstream fails for a confusing reason that traces back to a stale register value.
update() is a no-op if nothing changed
update() only issues a bus transaction if desired != mirrored — calling it redundantly (e.g. in a loop, defensively, "just in case") costs nothing extra. This makes it safe to call liberally rather than trying to manually track whether a register actually needs re-writing.
Adapters and predictors: connecting RAL to the real bus¶
RAL doesn't know how to speak your specific bus protocol — a register adapter translates a generic uvm_reg_bus_op into your protocol's actual sequence item, and a predictor does the reverse (observes real bus traffic via the monitor's analysis port and updates the mirror automatically, even for accesses that didn't originate from RAL itself, e.g. traffic from a second bus master):
uvm_reg_predictor #(my_axi_txn) predictor;
// in connect_phase:
agent.mon.ap.connect(predictor.bus_in);
predictor.map = reg_block.default_map;
This is what lets RAL's mirror stay accurate even in a system with multiple masters or unexpected register accesses from outside the register-model-driven test flow itself.