The bench's busy-LEN test ends with a RegModel error, on purpose. The DMA ignores writes to LEN while a job is running, the model does not know that rule, and mirror(UVM_CHECK) catches the disagreement. In a real project that error is the start of a decision: is it a DUT bug or a model gap? Here the spec says the DUT is right. So the model must learn the rule, and you need to know where in the model rules like that live.
A field's access policy is its contract with software: what a write does to the stored value, and what a read does to it. Think of the switches in a hotel room. Some are ordinary toggles (RW). The doorbell button does something when pressed but has no state you can see (WO). The "do not disturb" light can be turned on from inside and is cleared by housekeeping (set by hardware, cleared by a write). The model needs to know which kind each switch is, so it can predict the room after every press.
The library uses the policy in three places. Prediction computes the new mirror value from the old one and the data written or read. `update()` computes which value to write to reach the desired value. Checks skip write-only fields entirely. The policy is a string passed to configure(), and the 25 built-in names are predefined by uvm_reg_field:
Policy | On a write of value w | On a read |
|---|---|---|
RO | No effect | No effect |
RW | Field = w | No effect |
WO | Field = w | Not readable (excluded from checks) |
RC / RS | No effect | Clears / sets all bits |
WC / WS | Clears / sets all bits, whatever w is | No effect |
WRC / WRS | Field = w | Clears / sets all bits |
WSRC / WCRS | Sets / clears all bits | Clears / sets all bits |
W1C / W1S / W1T | Each 1 in w clears / sets / toggles that bit | No effect |
W0C / W0S / W0T | Each 0 in w clears / sets / toggles that bit | No effect |
W1SRC / W1CRS | Each 1 sets / clears that bit | Clears / sets all bits |
W0SRC / W0CRS | Each 0 sets / clears that bit | Clears / sets all bits |
WOC / WOS | Clears / sets all bits | Not readable |
W1 | First write after a hard reset sets the field; later writes are ignored | No effect |
WO1 | As W1, but not readable | Not readable |
get_access() can also return NOACCESS, when the map's rights for the register contradict the field's policy (a WO field in a register mapped RO, for example). Read effects are applied only to frontdoor and predictor accesses: a backdoor peek() of an RC field does not clear the mirror, because a backdoor read does not clear the hardware either.
// snippet: from the course DMA bench
// CTRL @0x00: GO is a trigger (write 1 to start, always reads 0), IRQ_EN is RW.
class dma_ctrl_reg extends uvm_reg;
`uvm_object_utils(dma_ctrl_reg)
uvm_reg_field go;
rand uvm_reg_field irq_en;
function new(string name = "dma_ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
go = uvm_reg_field::type_id::create("go");
irq_en = uvm_reg_field::type_id::create("irq_en");
// parent size lsb access volatile reset has_reset is_rand indiv
go.configure (this, 1, 0, "WO", 0, 1'b0, 1, 0, 0);
irq_en.configure(this, 1, 1, "RW", 0, 1'b0, 1, 1, 0);
endfunction
endclass
// STATUS @0x04: BUSY is read-only; DONE and ERR are set by the hardware and
// cleared by writing 1. All three change without a bus write, so all are volatile.
class dma_status_reg extends uvm_reg;
`uvm_object_utils(dma_status_reg)
uvm_reg_field busy, done, err;
function new(string name = "dma_status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
busy = uvm_reg_field::type_id::create("busy");
done = uvm_reg_field::type_id::create("done");
err = uvm_reg_field::type_id::create("err");
busy.configure(this, 1, 0, "RO", 1, 1'b0, 1, 0, 0);
done.configure(this, 1, 1, "W1C", 1, 1'b0, 1, 0, 0);
err.configure (this, 1, 2, "W1C", 1, 1'b0, 1, 0, 0);
endfunction
endclassGO is WO and not volatile. Writing 1 starts a job; reading always returns 0. WO keeps it out of every compare, so a read of CTRL never false-fails on it. The price is the mirror holding 1 after the first job (lesson 1). A callback can model the self-clear (below).
IRQ_EN is RW and randomizable. Plain storage.
BUSY is RO and volatile. The hardware changes it; software cannot.
DONE and ERR are W1C and volatile. The hardware sets them and software clears them by writing 1. Modelling them as RW would make a clear write of 1 predict DONE = 1, the opposite of the truth. With W1C the model predicts mirror & ~w natively; no custom predictor code is needed.
COUNT is RO and volatile (in dma_count_reg): it counts while the job runs.
Your spec may use a name the library lacks, such as a W1C bit that only clears when the block is idle. uvm_reg_field::define_access("W1CBUSY") registers it, returning 1 the first time and 0 if the name already exists; call it before any configure() uses the name. Registering a name gives it no behaviour: prediction falls to the default branch and treats a write like RW, and update() and checks treat it like an ordinary readable field. The behaviour comes from a callback. A custom name is still worth having, because register coverage, generated documentation and your own callbacks can key on it.
uvm_reg_cbs has hooks around accesses (pre_write, post_write, pre_read, post_read) and one hook inside prediction: post_predict. It is called for every field prediction from a write or read (frontdoor, predictor or backdoor), after the policy has computed the new value and before the mirror is written. It gets the field, the previous mirror value, the new value as an inout you may change, the kind, the path and the map. That is exactly where a side-effect rule belongs. The callback below tracks the DMA's busy state in the model and keeps LEN and KEY unchanged while it is set:
// snippet: verified in a scratch run with the DMA bench
class dma_busy_ignore_cb extends uvm_reg_cbs;
`uvm_object_utils(dma_busy_ignore_cb)
dma_reg_block rm;
bit busy; // the model's belief about STATUS.BUSY
function new(string name = "dma_busy_ignore_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 (fld == rm.ctrl.go && kind == UVM_PREDICT_WRITE && value[0] && !busy)
busy = (rm.len.value.get_mirrored_value() != 0); // GO starts a job only if LEN != 0
else if (fld == rm.status.busy && kind == UVM_PREDICT_READ)
busy = value[0]; // a STATUS read tells the truth
else if ((fld == rm.len.value || fld == rm.key.value) &&
kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && busy) begin
`uvm_info("BUSY_CB", $sformatf("%s write of %0h ignored while BUSY, mirror keeps %0h",
fld.get_parent().get_name(), value, previous), UVM_LOW)
value = previous;
end
endfunction
endclass
// In the test's connect_phase: one callback object, registered on four fields.
// cb.rm = renv.regmodel;
// uvm_reg_field_cb::add(renv.regmodel.ctrl.go, cb);
// uvm_reg_field_cb::add(renv.regmodel.status.busy, cb);
// uvm_reg_field_cb::add(renv.regmodel.len.value, cb);
// uvm_reg_field_cb::add(renv.regmodel.key.value, cb);Be honest about what it cannot see. The model learns that a job ended only when someone reads STATUS (or when you add a hook from the interrupt monitor). A test that writes LEN after the job finished but before any STATUS read would see the callback wrongly keep the old value. The same pattern models the GO trigger cleanly: a callback on ctrl.go that sets value = 0 on every write prediction makes the mirror hold 0, so update() works for the next job.
fld.set_compare(UVM_NO_CHECK) removes one field from every mirror compare without changing its policy or its prediction; set_compare(UVM_CHECK) puts it back, and get_compare() reads it. Configuring a field as volatile sets UVM_NO_CHECK for you. Use it for a field whose value you cannot predict but which is not changed by hardware either: an ID register that differs per tape-out, or a reserved field the designers may still use. Lesson 5 is about what that does to checks.
The scratch test is the bench's busy-LEN test with the callback registered (and define_access called once, twice, to show its return values). Compare it with the bench log in lesson 5, which ends with a RegModel error:
scratch run: +UVM_TESTNAME=s_busy_cb_test (dma_ral_busy_len_test plus the callback; auto_predict = 1)
UVM_INFO sacc_tb.sv(251) @ 0: uvm_test_top [POL] define_access("W1CBUSY") returned 1, again 0
UVM_INFO sacc_tb.sv(232) @ 195000: reporter [BUSY_CB] len write of 9 ignored while BUSY, mirror keeps 4
UVM_INFO dma_ral_tb.sv(385) @ 195000: uvm_test_top [RAL] LEN mirror after the write: 4
UVM_INFO sacc_tb.sv(232) @ 195000: reporter [BUSY_CB] len write of 9 ignored while BUSY, mirror keeps 4
UVM_INFO dma_ral_tb.sv(390) @ 235000: uvm_test_top [RAL] DUT LEN (backdoor peek)=4, mirror now 4
UVM_INFO dma_tb.sv(411) @ 625000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0`mirror keeps 4` and `LEN mirror after the write: 4`: the model now knows the rule, so mirror(UVM_CHECK) reads 4, expects 4, and reports nothing. UVM_ERROR : 0.
The callback fired twice for one write. This test runs with auto prediction on and the explicit predictor connected (lesson 3), so the write was predicted twice. A pure function of its inputs survives that; a callback that counts events or toggles state would not.
`define_access` returned 1, then 0. The second call found the name already registered.
Policies also drive update(), and that is where a correct-looking call bit a scratch test written for this lesson. After a job, DONE is 1 in the DUT. A sequence changes KEY with set() and pushes it with the block-level regmodel.update(st), meaning "write whatever changed":
scratch run: +UVM_TESTNAME=s_block_update_test +UVM_VERBOSITY=UVM_HIGH (APB monitor lines kept)
UVM_INFO sacc_tb.sv(497) @ 225000: uvm_test_top [UPD] DONE pending; STATUS mirror=0, status.needs_update=1, ctrl.needs_update=0
UVM_INFO dma_tb.sv(151) @ 265000: uvm_test_top.env.apb.mon [APB_MON] WR addr=04 data=00000006
UVM_INFO dma_tb.sv(151) @ 305000: uvm_test_top.env.apb.mon [APB_MON] WR addr=0c data=000000bb
UVM_INFO sacc_tb.sv(501) @ 345000: uvm_test_top [UPD] after regmodel.update(): STATUS=0, KEY mirror=bb
UVM_INFO dma_tb.sv(151) @ 345000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000000The block update wrote 0x6 to STATUS before writing KEY, and DONE was gone. Two rules combined. First, needs_update() is always true for a writable volatile field, so STATUS is always "changed". Second, for a W1C field update() writes the complement of the desired value, because writing 1 is how you make a W1C bit 0. The desired DONE was 0, so update() wrote 1 to it, and to ERR. On a real chip that is an interrupt status cleared by a configuration sequence before the interrupt handler ever saw it.
The fix is a rule for the team: call update() on the register you changed, never on a block that contains W1C status registers, or exclude those registers in the update sequence. The deeper lesson is that update() is a model-driven write. It does exactly what the policies say, which is not always what the person calling it meant.
Model it as RC. The peek() is a backdoor read: the library does not apply read side effects for the backdoor path, so the mirror takes the peeked value and the hardware is not cleared either. The frontdoor read returns the value and then predicts the field as 0, matching the hardware's clear-on-read.
define_access("RWLOCK") and configure a field with it, but add no callback. A write arrives while the lock is set. What does the model predict?The written value. Unknown names fall to the default branch of the prediction, which behaves like RW. A registered name adds no behaviour; the lock rule needs a post_predict callback (or a custom predictor).
path != UVM_BACKDOOR before keeping the old LEN value?The "ignored while BUSY" rule belongs to the bus interface. A backdoor deposit writes the flip-flop directly, whatever the DMA's state, so its prediction must not be blocked.
Next: the model now knows the DMA's rules, but some fields change on their own. Volatile Fields and Mirror Checks decides which bits a check can trust.