A threshold register is written with 0x40, and the very next mirror(UVM_CHECK) fails: read 16'h10, mirrored 16'h40. The designer is unbothered. THRESH is double-buffered: software writes a pending copy, and the new value takes effect, and reads back, only after a write to COMMIT. The RTL is right, the spec is right, and the register model, which assumes that a written value is the value you read back, is wrong.
Real register files are full of storage that one address does not map to one-to-one: shadow (double-buffered) registers, alias addresses for the same flops, paged and indexed windows. Every one of them breaks the model's default assumption. This lesson models the first two with field callbacks, verified on the scratch slave, and shows where paged registers need a user frontdoor and a custom predictor instead.
A shadow register is a draft and a published document: you edit the draft (the pending copy), and nothing changes for readers until you press publish (the commit trigger). An alias is a second door into the same room: two addresses, one set of flops, sometimes with different access policies at each address (a read-only view, or a write-1-to-set view of a RW register). A paged register is a lift: one address (the window) reaches different floors depending on a page register you wrote first.
The register model's default is one storage element per register, updated by its own field policy. The tools to change that are, in order of reach:
Tool | Scope | Use it for |
|---|---|---|
Field callback ( | One field: it may change the value about to be stored, and may predict other fields | Shadow registers, aliases, side effects of one write on another register |
Custom predictor (extend | Every observed bus transfer, with state of your own | Rules spanning many registers, paged windows seen on the bus |
User frontdoor ( | How the model reaches a register at all | Paged and indexed registers with no address of their own |
Custom access policies and the callback hooks themselves are introduced in Access policies and callbacks; this lesson applies them to storage shapes.
post_predict runs inside do_predict after the field policy has computed the new value and before it is stored. Its value argument is inout, so a callback can replace the value that lands in the mirror. It runs for write and read predictions, but not for a direct predict() call (UVM_PREDICT_DIRECT, the default kind of predict()), so a callback checks kind and acts only on the case it models.
// snippet: scratch callbacks (uvm_path_e is the UVM 1.2 name; IEEE 1800.2 keeps it as an alias of uvm_door_e)
class shadow_cb extends uvm_reg_cbs;
uvm_reg_data_t pending = 'h10; // THRESH's reset value
function new(string name = "shadow_cb");
super.new(name);
endfunction
virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
inout uvm_reg_data_t value, input uvm_predict_e kind,
input uvm_path_e path, input uvm_reg_map map);
if (kind == UVM_PREDICT_WRITE) begin
pending = value; // the write lands in the pending copy
value = previous; // what a read returns has not changed
end
endfunction
endclass// snippet: scratch callbacks
class commit_cb extends uvm_reg_cbs;
shadow_cb shadow;
uvm_reg_field target; // THRESH.value
function new(string name = "commit_cb");
super.new(name);
endfunction
virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
inout uvm_reg_data_t value, input uvm_predict_e kind,
input uvm_path_e path, input uvm_reg_map map);
if (kind == UVM_PREDICT_WRITE && value[0]) begin
void'(target.predict(shadow.pending));
value = 0; // GO is a trigger: the mirror keeps 0
end
endfunction
endclassThe last line solves a second problem for free. Without it, GO's mirror holds 1 after the first commit, so commit.go.set(1); commit.update() sees desired equal to mirrored and writes nothing: the trap the bench avoids for CTRL.GO by writing CTRL directly. With the callback, GO's mirror returns to 0 and update() works.
// snippet: scratch callbacks
class alias_cb extends uvm_reg_cbs;
uvm_reg_field a, b; // CFG.mode and CFG_ALIAS.mode
local bit busy;
function new(string name = "alias_cb");
super.new(name);
endfunction
virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
inout uvm_reg_data_t value, input uvm_predict_e kind,
input uvm_path_e path, input uvm_reg_map map);
if (busy) return; // defensive: see the text
busy = 1;
if (fld == a) void'(b.predict(value));
else void'(a.predict(value));
busy = 0;
endfunction
endclass
// in the env, after the model is built:
cm.shadow = sh; cm.target = rm.thresh.value;
al.a = rm.cfg.mode; al.b = rm.cfg_alias.mode;
uvm_reg_field_cb::add(rm.thresh.value, sh);
uvm_reg_field_cb::add(rm.commit.go, cm);
uvm_reg_field_cb::add(rm.cfg.mode, al);
uvm_reg_field_cb::add(rm.cfg_alias.mode, al);b.predict(value) is a direct prediction, and direct predictions do not call callbacks (the library's uvm_reg_field::do_predict calls post_predict only for write and read predictions), so the partner's callback does not fire and the pair cannot ping-pong. The busy flag is there for the day someone changes that call to predict(value, .kind(UVM_PREDICT_WRITE)) to get policy handling: write predictions do run callbacks, and the two fields would call each other forever. If the alias has a different policy (say CFG_ALIAS is W1S, a set-bits view), the callback predicts the resulting value on the other field rather than copying the written one.
The scratch test writes THRESH = 0x40, checks it, writes COMMIT, checks again, writes CFG_ALIAS = 0x33 and checks CFG. Scratch run, Verilator:
UVM_INFO mini_tb.sv(412) @ 145000: uvm_test_top [SHADOW] after writing 40: thresh mirror=10
UVM_INFO mini_tb.sv(417) @ 225000: uvm_test_top [SHADOW] after COMMIT: thresh mirror=40, commit.go mirror=0
UVM_INFO mini_tb.sv(421) @ 305000: uvm_test_top [SHADOW] after writing 33 to CFG_ALIAS: cfg mirror=33
UVM_INFO mini_tb.sv(424) @ 335000: uvm_test_top [SHADOW] commit.go.set(1): needs_update=1
UVM_ERROR : 0All three mirror checks pass, and the last line shows update() on COMMIT would now really write. The predictions come from the explicit predictor on the CPU bus, so the callbacks fire for writes made by any sequence or by any agent on that bus, not only for accesses made through the model.
The same test with the callbacks not registered (Verilator, scratch run):
UVM_INFO mini_tb.sv(412) @ 145000: uvm_test_top [SHADOW] after writing 40: thresh mirror=40
UVM_ERROR reg/uvm_reg.svh(2812) @ 175000: reporter [RegModel] Register "rm.thresh" value read from DUT (0x0000000000000010) does not match mirrored value (0x0000000000000040) (valid bit mask = 0x000000000000ffff)
UVM_INFO mini_tb.sv(417) @ 225000: uvm_test_top [SHADOW] after COMMIT: thresh mirror=10, commit.go mirror=1
UVM_ERROR reg/uvm_reg.svh(2812) @ 255000: reporter [RegModel] Register "rm.thresh" value read from DUT (0x0000000000000040) does not match mirrored value (0x0000000000000010) (valid bit mask = 0x000000000000ffff)
UVM_INFO mini_tb.sv(421) @ 305000: uvm_test_top [SHADOW] after writing 33 to CFG_ALIAS: cfg mirror=0
UVM_ERROR reg/uvm_reg.svh(2812) @ 335000: reporter [RegModel] Register "rm.cfg" value read from DUT (0x0000000000000033) does not match mirrored value (0x0000000000000000) (valid bit mask = 0x00000000000000ff)
UVM_INFO mini_tb.sv(424) @ 335000: uvm_test_top [SHADOW] commit.go.set(1): needs_update=0
UVM_ERROR : 3Read the middle two errors together; they are the instructive part. The first check fails as expected. But mirror() also updates the mirror with what it read, so after the failed check the model holds 0x10, correct for that instant. COMMIT then makes the DUT return 0x40, the model knows nothing about commits, and the second check fails in the opposite direction. A reviewer seeing expected 40, got 10 followed by expected 10, got 40 often suspects a flaky DUT. The real cause is one missing rule in the model, and the fix is to model the rule, never to add set_compare(UVM_NO_CHECK) to THRESH and lose the check altogether.
A paged register file has one data window and a PAGE register; the register you reach depends on PAGE. Two things change. The model reaches such a register through a user frontdoor, a sequence that writes PAGE and then accesses the window, attached with add_reg(rg, offset, rights, 1 /*unmapped*/, frontdoor) or set_frontdoor(). And the explicit predictor can no longer help: the bus shows writes to PAGE and to the window, never to bank 1 CFG, so get_reg_by_offset finds the window register, not the paged one.
// snippet: a user frontdoor for a paged register
class paged_frontdoor extends uvm_reg_frontdoor;
`uvm_object_utils(paged_frontdoor)
uvm_reg page_sel, window; // PAGE and the shared data window
uvm_reg_data_t page; // the page this register lives in
function new(string name = "paged_frontdoor");
super.new(name);
endfunction
virtual task body();
uvm_status_e st;
page_sel.write(st, page, .parent(this));
if (rw_info.kind == UVM_WRITE) window.write(st, rw_info.value[0], .parent(this));
else window.read (st, rw_info.value[0], .parent(this));
rw_info.status = st;
endtask
endclassChoose one of two prediction strategies and write it down. Either predict paged registers from their own accesses (auto prediction on for them: the map predicts after the frontdoor finishes) and accept that writes made outside the model are invisible, or extend uvm_reg_predictor so its write() tracks the last PAGE value seen on the bus and predicts the paged register whenever a window access arrives. The second is more work and is the only one that survives firmware or a second master writing the window. Model the window register itself with UVM_NO_CHECK, since its mirror holds whichever page was touched last. The library also ships uvm_reg_indirect_data for the common index-plus-data pattern.
kind == UVM_PREDICT_READ. Why is that correct?A read returns the active copy, which is exactly what the mirror should hold, so the value the policy computed from the read is already right. Only a write needs special handling, because it lands in the pending copy that reads cannot see. Intercepting reads too would stop the mirror from ever resynchronising with the DUT.
busy flag from alias_cb and, separately, changes b.predict(value) to b.predict(value, .kind(UVM_PREDICT_WRITE)). What happens on the first write to CFG?Removing the flag alone changes nothing: predict() defaults to a direct prediction, which does not call post_predict. With the write kind, the prediction of CFG_ALIAS.mode runs its callbacks, which is the same object, which predicts CFG.mode with the write kind again, and so on: unbounded recursion until the simulator runs out of stack. The guard makes each side update the other exactly once whatever kind is used.
mirror(UVM_CHECK). What happens, and why?The check fails (or passes only by luck). The predictor sees a PAGE write and a window write; it predicts the window register, never the paged one, so the paged register's mirror never changed. Turn on auto prediction for the paged registers, or extend the predictor to track PAGE and predict the paged register from window accesses.
Next: status registers that the hardware sets on its own are the hardest storage of all to predict. Interrupts with the Register Model writes an interrupt handler that reads and clears them through the model.