A test writes LEN=4, starts a DMA job, and then, while the DMA is busy, writes LEN=9. The APB transfer completes cleanly. The predictor sees it, and the register model now says LEN is 9. A moment later the test reads LEN back over the bus and gets 4. The register model reports an error. Is the DUT broken?
No. The DMA's spec says writes to LEN and KEY are ignored while the block is busy, and the DUT did exactly that. The model was confidently wrong. This lesson explains what a register model is for, the three values it juggles, and why that error is the most useful thing it can tell you.
Without a model, a test programs registers with bus writes it assembles itself. The DMA's block tests do this through helpers: reg_write(KEY, key); reg_write(LEN, 6); reg_write(CTRL, 32'h3);. Every test has to know that CTRL is at 0x00, that IRQ_EN is bit 1, that DONE clears when you write 1 to it, and what each register should read back. With five registers that is fine. A chip has thousands, they move when the floor plan changes, the block's APB port sits behind an AXI interconnect at chip level, and the same test should run on both.
A register model is one description of every register: its name, its address in each map (an address space seen from one bus), its fields (named bit ranges) and each field's access policy (RW, RO, write-1-to-clear and so on). Tests access registers by name, rm.len.write(st, 6); the map turns the name into an address, and an adapter turns the access into a transaction for whatever bus is underneath. Move the block, change the bus, and the tests stay the same.
The model also keeps a copy of what it believes each register holds, so it can predict what a read should return. That copy is where the subtlety lives. Think of a bank: the deposit slip you have filled in but not handed over, the ledger the bank keeps, and the vault itself. The ledger is updated when a teller sees a transaction. If something in the vault changes without passing a teller, the ledger does not know until someone counts.
Value | Bank analogy | What it is | Read it with |
|---|---|---|---|
Desired | The deposit slip | The value you intend the register to hold. |
|
Mirrored | The ledger | What the model believes the DUT holds, updated by prediction after each access it knows about. |
|
DUT value | The vault | What the RTL flip-flops really hold. Only the hardware changes it, and only a bus read or a backdoor peek shows it. |
|
Three things in this picture matter more than the rest. First, every prediction sets the mirrored and the desired value, so after a completed access they agree. Second, update() writes only when desired differs from mirrored, which makes it cheap to call often on configuration registers, and dangerous for a trigger bit that the mirror remembers and for registers with volatile W1C fields such as STATUS, which it writes back every time (the interrupts lesson shows a block-level update() clearing a pending DONE) (the access API lesson has the details). Third, the arrow from hardware events stops at the DUT. When the DMA finishes a job and sets DONE, no prediction runs; the mirror finds out only on the next read.
Here is the test that produced the opening scene. It uses dma_ral_env (the block env plus the model, the adapter and a predictor), turns on auto-prediction so the mirror updates the instant an access returns, and does everything by register name. Compare the LEN write with reg_write(LEN, 9): the address and the bit packing are gone, and the model now has an opinion about the result.
// snippet: from the course DMA bench
// The model meets a DUT rule it does not know: the DMA ignores writes to LEN
// while BUSY. The write goes out on the bus, the predictor updates the mirror,
// and a mirror(UVM_CHECK) read then reports the DUT's real value as a mismatch.
// This test is expected to end with one UVM_ERROR from the register model.
class dma_ral_busy_len_test extends dma_ral_base_test;
`uvm_component_utils(dma_ral_busy_len_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
uvm_config_db#(bit)::set(this, "env", "auto_predict", 1); // mirror() takes its expected value at once
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
uvm_status_e st;
uvm_reg_data_t peeked;
dma_reg_block rm = renv.regmodel;
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this);
rm.key.write(st, 32'h0000_00FF);
rm.len.write(st, 4);
rm.ctrl.write(st, 32'h3); // the DMA is now BUSY
rm.len.write(st, 9); // ignored by the DMA, believed by the mirror
`uvm_info("RAL", $sformatf("LEN mirror after the write: %0d", rm.len.get_mirrored_value()), UVM_LOW)
rm.len.mirror(st, UVM_CHECK); // reads 4, expects 9: RegModel error
// peek() after the check, not before: a peek predicts the value it reads,
// so peeking first would quietly set the mirror to 4 and hide the mismatch.
rm.len.peek(st, peeked);
`uvm_info("RAL", $sformatf("DUT LEN (backdoor peek)=%0d, mirror now %0d", peeked, rm.len.get_mirrored_value()), UVM_LOW)
if (!pkt.randomize() with { n == 4; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
rm.status.write(st, 32'h2);
#100ns;
phase.drop_objection(this);
endtask
endclassmirror(st, UVM_CHECK) reads the register over the bus, compares what came back with the mirror, reports any difference, and then sets the mirror to the value read. It is the model's way of asking was I right?
UVM_INFO dma_ral_tb.sv(385) @ 195000: uvm_test_top [RAL] LEN mirror after the write: 9
UVM_ERROR reg/uvm_reg.svh(2812) @ 235000: reporter [RegModel] Register "regmodel.len" value read from DUT (0x0000000000000004) does not match mirrored value (0x0000000000000009) (valid bit mask = 0x000000000000ffff)
UVM_INFO reg/uvm_reg.svh(2833) @ 235000: reporter [RegModel] Field value (regmodel.len[15:0]) mismatch read=16'h4 mirrored=16'h9
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) @ 515000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_ERROR : 1
UVM_FATAL : 0LEN mirror after the write: 9. The write completed on the bus, so the model predicted it. Desired and mirrored are both 9; the DUT still holds 4.
The RegModel error is mirror(UVM_CHECK) at work: read 4, expected 9. The valid bit mask 0xffff shows only the 16 modelled bits of LEN were compared.
mirror now 4. After the check, the mirror takes the value it read, so the model is back in step with the DUT. The backdoor peek confirms the DUT's value without a bus access.
4 words matched. The job ran with LEN=4, as the DUT's spec says it should. The single UVM_ERROR is this test's expected result: it exists to show the mismatch.
The model knows each field's static policy: LEN is RW, DONE is write-1-to-clear, COUNT is read-only. It does not know dynamic rules such as "LEN is ignored while BUSY", unless someone models them. The same blind spot bit the bench before: the DMA's scoreboard and predictor take a new KEY at once, and a scratch run writing KEY mid-job gave five mismatches in both checkers. A model that copies the spec's register table but not its prose has the same hole, in a new place.
While building this test there was a second trap. The first version peeked LEN through the backdoor before the mirror check, to print the DUT's value. The check then passed. A peek is an access too: it predicts the value it reads, so it quietly set the mirror to 4, and mirror(UVM_CHECK) compared 4 with 4. Any access that observes the DUT corrects the belief, so the check has to come first. That is why the test above peeks after the check, with a comment saying so.
The third limit is by design. STATUS and COUNT change without a bus write, so their fields are configured volatile. The library turns off the mirror compare for volatile fields, so mirror(UVM_CHECK) on STATUS never reports a mismatch, and reading a volatile field's mirror draws a warning from the IEEE 1800.2 library. dma_ral_job_test prints one for each STATUS bit:
UVM_WARNING reg/uvm_reg_field.svh(1327) @ 425000: reporter [UVM/FLD/GET_MIRRORED_VAL/VOL] Mirrored value returned for volatile field "regmodel.status.done" may not reflect the actual current value.For a volatile register, check the value a read() returns against your own expectation, not against the mirror. For a rule like busy-LEN, either keep the error as a known spec gap (as this bench does) or teach the model the rule with a callback or a predictor extension; Register Access shows how.
rm.len.set(9) and no other call, what do get(), get_mirrored_value() and the DUT's LEN hold, if LEN was 4 before?get() returns 9 (desired), get_mirrored_value() returns 4 and the DUT holds 4. set() touches only the desired value. A later update() would see desired 9 and mirrored 4, write 9 over the bus, and the prediction from that write would bring the mirror to 9.
rm.status.mirror(st, UVM_CHECK). Does it report a mismatch?No. DONE is configured volatile, and the library turns the compare off for volatile fields. The read still happens and the mirror takes the value read, so DONE's mirror becomes 1 silently. To check STATUS, compare the value read() returns with what you expect.
dma_ral_busy_len_test, someone moves the backdoor peek() above mirror(UVM_CHECK). What changes in the log, and why?The RegModel error disappears and the test reports 0 errors. The peek reads 4 from the DUT and predicts it, setting the mirror to 4, so the check compares 4 with 4. The DUT's behaviour is identical; only the evidence is gone.
Next: the model can only be as right as its description of each bit. Blocks, Registers, Fields and Memories builds the DMA's model field by field.