The first DMA job runs through the register model without a hitch. For the second job, a colleague writes the tidy version: rm.ctrl.go.set(1); rm.ctrl.update(status);. The test then waits for the interrupt, and waits, until the timeout fires. No error, no warning, and the APB monitor shows that no write to CTRL ever went out. The register model did exactly what it was told. The question is what each call actually tells it.
A register model keeps two values for every field, and the DUT holds a third. The desired value is what you want the field to become. The mirrored value is what the model believes the DUT holds right now. The DUT value is the truth, sitting in a flip-flop. Every access call moves one or more of these, and nearly every RAL bug is a case where you assumed a call moved one that it did not.
Think of a kitchen. The desired value is your shopping list, the mirror is the inventory sheet taped to the pantry door, and the DUT is the pantry itself. Writing on the list changes nothing in the pantry. Restocking the pantry is pointless if nobody updates the sheet. And the sheet is only as good as the last time someone actually looked inside. update() is the person who compares the list with the sheet, not with the pantry, and shops only for what differs. That one detail is the whole opening bug.
One arrow surprises most people: every prediction overwrites the desired value too. In the library, uvm_reg_field::do_predict ends by setting the mirrored value, the desired value and the field's value member to the same number. So a set() you have not yet pushed with update() is lost the moment any access to that register is predicted.
Call | Path | Desired | Mirrored | DUT | Takes time |
|---|---|---|---|---|---|
| none | set / read | - | - | no |
| none | random value | - | - | no |
| frontdoor (default) | v | predicted | written | yes |
| frontdoor | - | predicted | written only if | yes, or no access at all |
| frontdoor | predicted | predicted | read | yes |
| frontdoor | predicted | predicted | read; compared if | yes |
| none | v | v | - | no |
| backdoor | predicted | predicted | read / written | no bus time |
| none | reset value | reset value | - | no |
`needs_update()` is true when any field's mirrored value differs from its desired value, or the field is volatile. Read-only fields (RO, RC, RS) never count. update() calls it first and returns at once, with UVM_IS_OK, when it is false.
The path argument. write, read, update and mirror take UVM_FRONTDOOR or UVM_BACKDOOR (the default comes from the block, normally frontdoor). write(st, v, UVM_BACKDOOR) is not the same as poke(): it emulates the field policies, so a W1C bit written with 1 is cleared, while poke() deposits the raw value. The same holds for read(..., UVM_BACKDOOR) against peek() for read-clear fields.
Status. Every task returns a uvm_status_e: UVM_IS_OK, UVM_NOT_OK (a bus error, or no backdoor), or UVM_HAS_X. Ignoring it is how a test that never reached the DUT passes. Do not count on UVM_HAS_X to catch X on the bus: uvm_reg_bus_op.data is 2-state (uvm_reg_data_t is bit), so through a standard adapter an X or Z read arrives as 0, the status is UVM_IS_OK, and the mirror takes the 0. Catch X where it is still 4-state: an assertion on PRDATA, or a check in the monitor.
Randomize the model, then push it. The model's rand fields (the bench's irq_en, LEN's and KEY's value) randomize like any object, and update() writes only what changed: check the result of regmodel.randomize() with { len.value.value inside {[1:16]}; }, then update the configuration registers one by one (regmodel.len.update(status);, regmodel.key.update(status);). Not the whole block: a block-level update() also writes STATUS back and clears pending DONE or ERR. A field configured with is_rand = 0 keeps its value.
`reset()` is model-only. It loads reset values into the desired and mirrored values; it never touches the DUT. Call it when the DUT is reset, so the mirror agrees with the hardware again.
Here is how the bench's job test drives a whole DMA job by name. write() sets the desired value and drives the bus in one call, which is why the bench never needs set() for a one-off access:
// snippet: from dma_ral_job_test in the course DMA bench (dma_ral_tb.sv)
rm.key.write(st, 32'hCAFE_F00D);
rm.len.write(st, 6);
// GO is a trigger: write CTRL directly. update() would skip it on the next
// job, because the mirror already holds GO = 1 from this write.
rm.ctrl.write(st, 32'h3);rm.status.done.write(st, 1) looks like a one-bit write. On APB it cannot be: the bus has no byte enables, and the bench configures every field with individually_accessible = 0. The library then writes the whole register, and it has to invent values for the other fields. It uses 0 for W1C, W1S, W1T, RO, RC and RS neighbours (so it does not clear or set them by accident), all ones for W0-type neighbours, and the mirrored value for everything else, including RW and WO fields. For STATUS that is harmless. For CTRL it is not, because GO is WO and its mirror still holds the 1 from the last job.
The scratch test below walks KEY through every call in order and prints the three values after each step (the DUT column is read straight from dma_bd_if, so it does not disturb the mirror). The env uses the bench's default, explicit prediction.
scratch run: +UVM_TESTNAME=s_values_test (KEY; the DUT column is read through dma_bd_if)
UVM_INFO sacc_tb.sv(36) @ 0: uvm_test_top [VAL] after reset desired=0 mirrored=0 DUT=0 needs_update=0
UVM_INFO sacc_tb.sv(36) @ 0: uvm_test_top [VAL] set(1111_1111) desired=11111111 mirrored=0 DUT=0 needs_update=1
UVM_INFO sacc_tb.sv(36) @ 75000: uvm_test_top [VAL] update() returns desired=11111111 mirrored=0 DUT=0 needs_update=1
UVM_INFO sacc_tb.sv(36) @ 76000: uvm_test_top [VAL] update() + 1ns desired=11111111 mirrored=11111111 DUT=11111111 needs_update=0
UVM_INFO sacc_tb.sv(36) @ 76000: uvm_test_top [VAL] predict(2222_2222) desired=22222222 mirrored=22222222 DUT=11111111 needs_update=0
UVM_INFO sacc_tb.sv(36) @ 115000: uvm_test_top [VAL] mirror() returns desired=22222222 mirrored=22222222 DUT=11111111 needs_update=0
UVM_INFO sacc_tb.sv(36) @ 116000: uvm_test_top [VAL] mirror() + 1ns desired=11111111 mirrored=11111111 DUT=11111111 needs_update=0
UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] write(3333_3333) + 1ns desired=33333333 mirrored=33333333 DUT=33333333 needs_update=0
UVM_WARNING dma_ral_tb.sv(195) @ 156000: reporter [BD_RO] this backdoor is read-only
UVM_WARNING reg/uvm_reg.svh(1664) @ 156000: reporter [PREDICT_NOK] status UVM_NOT_OK; skip prediction.
UVM_INFO sacc_tb.sv(50) @ 156000: uvm_test_top [VAL] poke status=UVM_NOT_OK
UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] reset() desired=0 mirrored=0 DUT=33333333 needs_update=0
UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] randomize() desired=bf95e7fb mirrored=0 DUT=33333333 needs_update=1
UVM_INFO sacc_tb.sv(36) @ 156000: uvm_test_top [VAL] peek() desired=33333333 mirrored=33333333 DUT=33333333 needs_update=0`update() returns` with the mirror still 0. The DUT column also shows 0 here, but that is the backdoor-versus-clock race from lesson 6: the flop takes the value at the edge that ends the APB transfer, and a read in the same time step can still see the old one. The explicit predictor updates the mirror a moment later in that same time step. One nanosecond later all three agree. Lesson 3 is about exactly this gap.
`predict()` lies on purpose. The mirror says 2222_2222 while the DUT holds 1111_1111, and needs_update is 0: the model now believes there is nothing to write.
`mirror()` repairs it. It reads the DUT and the prediction overwrites both desired and mirrored. With UVM_CHECK it would have reported the mismatch first.
`poke()` fails cleanly. The bench's backdoor refuses writes, so the status is UVM_NOT_OK and the library skips the prediction (PREDICT_NOK).
`reset()` and `randomize()` never touch the DUT. After reset() the model says 0 while the DUT holds 3333_3333. A peek() brings the model back to the truth.
This is the opening bug, and it is war story 13 from the bench. CTRL.GO is a trigger: writing 1 starts a job, and it always reads back 0. The model declares it WO. When the first job writes CTRL with 3, the predictor applies the WO policy, which predicts the written value, so GO's mirror becomes 1 and stays 1. A read of CTRL does not fix it, because a read never changes the mirror of a WO field. Then go.set(1) makes desired = 1 = mirrored, needs_update() is false, and update() returns without a bus access. The scratch run proves it:
scratch run: s_go_update_test
UVM_INFO sacc_tb.sv(80) @ 325000: uvm_test_top [GO] job 1 done; CTRL reads 2, GO mirror=1
UVM_INFO sacc_tb.sv(83) @ 325000: uvm_test_top [GO] go.set(1): desired=1 mirrored=1 needs_update=0
UVM_INFO sacc_tb.sv(87) @ 395000: uvm_test_top [GO] after update(): STATUS=0 (busy=0)
UVM_INFO sacc_tb.sv(90) @ 395000: uvm_test_top [GO] go.predict(0); go.set(1): needs_update=1
UVM_INFO sacc_tb.sv(94) @ 505000: uvm_test_top [GO] after update(): STATUS=1 (busy=1)
UVM_INFO dma_tb.sv(411) @ 685000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_ERROR : 0The bench's fix is the plain one: write CTRL directly with write(st, 3) for every job, because a trigger is an action, not a state. If a sequence must use update(), tell the model the trigger self-cleared with go.predict(0) after each use, as the second half of the log does, or put the same rule in a callback (lesson 4).
The field-level trap has the same root. After the first job, the scratch test clears DONE with a field write and then turns off the interrupt with rm.ctrl.irq_en.write(st, 0). That field write becomes a whole-register write with GO taken from its mirror, which is 1, so it starts a second job nobody asked for:
scratch run: s_field_write_test
UVM_WARNING reg/uvm_reg_field.svh(1831) @ 225000: reporter [RegModel] Individual field access not available for field 'regmodel.status.done'. Accessing complete register instead.
UVM_INFO sacc_tb.sv(116) @ 305000: uvm_test_top [FLD] after status.done.write(1): STATUS=0
UVM_WARNING reg/uvm_reg_field.svh(1831) @ 305000: reporter [RegModel] Individual field access not available for field 'regmodel.ctrl.irq_en'. Accessing complete register instead.
UVM_INFO sacc_tb.sv(120) @ 415000: uvm_test_top [FLD] after ctrl.irq_en.write(0): STATUS=1 (busy=1)The DMA was busy again 110 ns after a write that only meant to clear IRQ_EN. If your bus has byte enables and the field fills its own byte lanes, field access can be truly individual (see the adapter contract). On a bus like APB3, write the register.
rm.len.set(10), then rm.len.read(st, v) to check the old value, then rm.len.update(st). LEN is RW and the DUT holds 6. What does the bus see?A read of 6 and no write. The read's prediction sets both the mirrored and the desired value to 6, so the set(10) is overwritten before update() runs, and needs_update() is false. Do the read first, or use write(st, 10).
rm.status.done.write(st, 1) clear only DONE, while rm.ctrl.irq_en.write(st, 0) starts a job?Both become whole-register writes because the field is not individually accessible on APB. The library fills W1C neighbours (ERR) and RO neighbours (BUSY) with 0, so the STATUS write touches only DONE. In CTRL the neighbour GO is WO, so it is filled from its mirror, which holds 1 from the last job: the write carries GO=1.
regmodel.reset() when the DUT reset is applied. The DUT went back to its reset values but the mirror kept the old ones. reset() reloads the model's reset values into the mirrored and desired values without any bus traffic.
Next: every frontdoor call above ends up as an apb_item on the bus. The Adapter Contract opens the translator in the middle and shows what it must promise.