The Register Abstraction Layer
The FIFO testbench built across the previous sections had no registers to model at all — every bit of its behavior was controlled directly through wr_en/rd_en pins on the interface. Plenty of real DUTs instead expose a register map: a set of named, addressable configuration and status registers sitting behind a bus. Testing those directly — hardcoding addresses, bit offsets, and expected reset values inline in every sequence — means a typo in an address silently talks to the wrong register instead of failing loudly, and a register map change means hunting down every hardcoded literal across every test that touches it.
The Register Abstraction Layer (RAL) — uvm_reg_field, uvm_reg, uvm_reg_block — models the DUT's register map as an object hierarchy instead, so tests read and write registers by name, through a class API, with the actual bus protocol underneath handled entirely by pieces built once and reused everywhere. This page and the three after it introduce RAL using a small PWM (pulse-width modulator) peripheral as a running example — four registers, simple enough to see the whole mechanism clearly.
The register map this section builds
| Offset | Register | Fields | Access |
|---|---|---|---|
0x00 | CTRL | enable (bit 0), polarity (bit 1) | RW |
0x04 | PERIOD | period (bits 15:0) | RW |
0x08 | DUTY | duty (bits 15:0) | RW |
0x0C | STATUS | running (bit 0) | RO |
uvm_reg_field: one field, one place its properties live
class pwm_ctrl_reg extends uvm_reg;
rand uvm_reg_field enable;
rand uvm_reg_field polarity;
`uvm_object_utils(pwm_ctrl_reg)
function new(string name = "pwm_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);
polarity = uvm_reg_field::type_id::create("polarity");
polarity.configure(this, 1, 1, "RW", 0, 1'b0, 1, 1, 0);
endfunction
endclass
configure()'s argument order is the part worth memorizing rather than guessing at each time: parent register, size in bits, LSB position, access policy string, volatile, reset value, has-reset, is-rand, individually-accessible. "RW" here is one of several legal access strings ("RO", "WO", "W1C", and others exist for less common hardware behaviors) — every field on this page's example uses "RW" or "RO", the two that cover most real registers. The last argument, individually-accessible, controls whether calling write() on this one field alone actually writes only this field's bits — both enable and polarity above pass 0 for it, meaning a field-level write() on either one still issues a full register-width bus write, using the other field's current mirrored value to fill in the bits it isn't targeting, rather than somehow writing fewer bits than the register's own bus access size allows.
The other three registers follow the same shape — pwm_period_reg and pwm_duty_reg each with one 16-bit "RW" field, pwm_status_reg with one 1-bit "RO" field (running) and, notably, no rand on either the field or the register itself: a read-only field has nothing meaningful to randomize for a write that can never legally happen through the bus.
uvm_reg_block: the registers, plus where they live on the bus
class pwm_reg_block extends uvm_reg_block;
rand pwm_ctrl_reg ctrl;
rand pwm_period_reg period;
rand pwm_duty_reg duty;
pwm_status_reg status;
`uvm_object_utils(pwm_reg_block)
function new(string name = "pwm_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
ctrl = pwm_ctrl_reg::type_id::create("ctrl");
period = pwm_period_reg::type_id::create("period");
duty = pwm_duty_reg::type_id::create("duty");
status = pwm_status_reg::type_id::create("status");
ctrl.configure(this); ctrl.build();
period.configure(this); period.build();
duty.configure(this); duty.build();
status.configure(this); status.build();
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
default_map.add_reg(ctrl, 'h00, "RW");
default_map.add_reg(period, 'h04, "RW");
default_map.add_reg(duty, 'h08, "RW");
default_map.add_reg(status, 'h0C, "RO");
endfunction
endclass
create_map()'s third argument (4) is the number of bytes moved per bus access — 4, matching a 32-bit bus, the same width every field's configure() call above assumed. Every register's placement on the bus ('h00 through 'h0C) is declared exactly once, here, in add_reg() — no address literal needs to appear anywhere else, in any sequence, ever again.
Reading and writing through the model
uvm_status_e status;
regmodel.ctrl.write(status, 32'h1, .parent(this));
regmodel.period.write(status, 32'd1000, .parent(this));
regmodel.status.read(status, rdata, .parent(this));
write()/read() are tasks, not functions — they take real simulation time, exactly as a real bus transaction would, because underneath they are one (the next page covers exactly how). Nowhere in this call site does an address or a bit slice appear — that's the entire payoff of the model built above.
Desired value vs. mirrored value
RAL tracks two separate values per field, not one:
- Desired value — a shadow value queued up to be written next, set via
field.set()(or as a side effect ofwrite()), but not yet necessarily reflected in the actual DUT. - Mirrored value — RAL's current belief about what the DUT register actually holds right now, read via
field.get_mirrored_value().
set() alone never touches the DUTregmodel.ctrl.enable.set(1'b1) only updates the desired value in the model — no bus transaction happens until a separate update() (or a direct write()) call actually sends it. Confusing set() for a write is a common early mistake specifically because both exist in the same API and the naming doesn't scream the distinction.
Implicit vs. explicit prediction
Both write() and read() need some way to keep the mirrored value in sync with reality once a frontdoor access completes — and there are genuinely two different mechanisms for that, easy to conflate:
- Implicit prediction (
AUTO_PREDICT) — enabled per map viaregmodel.default_map.set_auto_predict(1). When an access goes through the RAL API itself, the frontdoor sequence's own completion updates the mirror immediately, inline, with no extra component required. Simple to set up, but blind to anything that doesn't go throughreg.write()/reg.read()— a status bit the DUT clears on its own, or a raw bus transaction issued by something other than RAL, leaves the mirror silently stale. - Explicit (passive) prediction — a dedicated
uvm_reg_predictorcomponent sits on the bus monitor's analysis port and observes every transaction on the bus, regardless of who issued it, translating each one through an adapter and calling the map'spredict()directly. It catches DUT-initiated and non-RAL bus activity that auto-predict would miss entirely, at the cost of needing that extra component wired up — covered next.
Most real testbenches default to explicit prediction as the source of truth for exactly that reason: a register model that silently drifts from the real DUT state is worse than no model at all, since it fails quietly rather than loudly.
A third value: the reset value
Desired and mirrored aren't the only value RAL tracks per field — there's also the reset value, the one supplied back in configure() (the 1'b0 literal in enable.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0) above). Two points distinguish it from the other two:
regmodel.reset()re-synchronizes the mirrored value back to each field's stored reset value — call it right after actually resetting the DUT, so RAL's belief about hardware state matches the real post-reset register contents rather than staying stale from before reset.- Multiple named reset values are possible, not just one —
get_reset()takes an optional reset-kind string (defaulting to"HARD"), so a register whose fields behave differently across a hard reset versus a softer, register-only reset can store both under different names rather than being forced to pick one.
What's next
The register model built here is inert on its own — write() has nowhere to send its access without an adapter translating it into a real bus protocol, and (for explicit prediction) a predictor watching that bus. The next page wires both into an actual environment.