Skip to content

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_path into 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 by set(), not yet sent to the DUT)
  • mirrored: the model's best understanding of the register's actual current hardware value, updated automatically after a write()/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.