dma_ral_job_test waits for the DMA's interrupt, reads STATUS by name, and prints the value and the DONE bit's mirror on one line. The log says STATUS=2: done=0. Two is DONE. The read returned it. And on the same line, at the same simulation time, the model says DONE is 0. Nothing is broken, and by the next line the model agrees. The question is what happened in between.
This lesson follows one frontdoor access, that STATUS read, through every object it touches, with the real log beside each step. It is the one diagram of the register layer worth drawing from memory.
A frontdoor access goes over the real bus, exactly as software would. The opposite, a backdoor access, reads or writes the RTL directly with no bus traffic and no simulation time; the DMA bench uses one to peek KEY. The frontdoor is slower, but it is the only path that proves the address decoder, the bus protocol and the register logic together.
Think of posting a letter and getting a receipt. You hand the letter to the post office (the map), which addresses it and puts it in the right kind of envelope (the adapter) for the courier (the sequencer and driver). The courier delivers it and comes back to tell you it arrived. Separately, a clerk who watches every delivery (the monitor) writes it in the register of deliveries (the predictor updates the mirror). You learn the letter arrived before the clerk has written it down.
Of all the objects in the diagram, you write two: the adapter and the env that wires everything. The map, the register and the predictor are library classes. The adapter translates between the library's generic uvm_reg_bus_op (kind, address, data, status) and your bus's sequence item. reg2bus builds an apb_item for the map to send; bus2reg turns a completed item, from the driver or from the monitor, back into an operation:
// snippet: from the course DMA bench
// Translates between register operations and APB items. The APB driver writes
// read data and PSLVERR back into the request item, so there is no separate
// response: provides_responses = 0.
class apb_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_reg_adapter)
function new(string name = "apb_reg_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0;
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_item it = apb_item::type_id::create("it");
it.addr = rw.addr;
it.write = (rw.kind == UVM_WRITE) || (rw.kind == UVM_BURST_WRITE);
it.data = rw.data;
return it;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_item it;
if (!$cast(it, bus_item))
`uvm_fatal("NOT_APB", "bus2reg got an item that is not an apb_item")
rw.kind = it.write ? UVM_WRITE : UVM_READ;
rw.addr = it.addr;
rw.data = it.write ? it.data : it.rdata;
rw.status = it.slverr ? UVM_NOT_OK : UVM_IS_OK; // a bus error is a status, not a fatal
endfunction
endclassprovides_responses = 0 tells the map that the driver returns read data inside the request item, so the map calls bus2reg on the request once finish_item returns. A bus error (PSLVERR) becomes UVM_NOT_OK, a status, while an item of the wrong type is a wiring bug and stops the run. The full adapter contract, including byte enables and separate responses, is in Register Access.
The env builds the model, the adapter and a uvm_reg_predictor #(apb_item), then connects three things: the map to the APB sequencer and the adapter, the predictor to the same map and adapter, and the APB monitor's analysis port to the predictor's bus_in. Auto-prediction is a config knob, off by default:
// snippet: from the course DMA bench
// The block env plus the register layer: the model, the adapter on the APB
// sequencer, and an explicit predictor fed by the APB monitor.
class dma_ral_env extends dma_env;
`uvm_component_utils(dma_ral_env)
dma_reg_block regmodel;
apb_reg_adapter adapter;
uvm_reg_predictor #(apb_item) predictor;
dma_reg_backdoor bd;
virtual dma_bd_if bd_vif;
bit auto_predict; // config_db knob, default 0
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
void'(uvm_config_db#(bit)::get(this, "", "auto_predict", auto_predict));
regmodel = dma_reg_block::type_id::create("regmodel");
regmodel.build();
adapter = apb_reg_adapter::type_id::create("adapter");
predictor = uvm_reg_predictor #(apb_item)::type_id::create("predictor", this);
if (!uvm_config_db#(virtual dma_bd_if)::get(this, "", "bd_vif", bd_vif))
`uvm_fatal("NOVIF", "dma_bd_if not set for the register env")
bd = new("bd");
bd.vif = bd_vif;
regmodel.key.set_backdoor(bd);
regmodel.len.set_backdoor(bd);
regmodel.count.set_backdoor(bd);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
regmodel.map.set_sequencer(apb.sqr, adapter);
// The explicit predictor updates the mirror from what the APB monitor sees.
// That happens when the monitor publishes the transfer - in this bench a
// moment after write()/read() return, in the same time step. Code that
// reads the mirror the instant an access returns (uvm_reg_bit_bash_seq's
// rg.get(), mirror()'s expected value) then sees the old value. Tests
// that need it turn on auto_predict as well.
regmodel.map.set_auto_predict(auto_predict);
predictor.map = regmodel.map;
predictor.adapter = adapter;
apb.ap.connect(predictor.bus_in);
endfunction
endclassWith auto-prediction off, the access itself never touches the mirror. The predictor does, for every transfer the monitor sees: it runs bus2reg, looks the address up with get_reg_by_offset, and calls do_predict on that register, which applies each field's policy. Because it works from the bus, it also catches accesses the model did not make, such as a firmware sequence or another bus master. That is explicit prediction, and it is the robust default.
dma_ral_job_test at +UVM_VERBOSITY=UVM_HIGH shows each step of the diagram as a line, all at 425000 ps (paths shortened):
UVM_INFO @ 425000: reporter [RegModel] Read register via map regmodel.map: regmodel.status=0x2
UVM_INFO dma_ral_tb.sv(346) @ 425000: uvm_test_top [RAL] STATUS=2: done=0 err=0
UVM_INFO dma_tb.sv(151) @ 425000: uvm_test_top.env.apb.mon [APB_MON] RD addr=04 rdata=00000002
UVM_INFO reg/uvm_reg_predictor.svh(253) @ 425000: uvm_test_top.env.predictor [REG_PREDICT] Observed READ transaction to register regmodel.status: value='h2Read register ... status=0x2: the map's report that the frontdoor read completed. read() returns to the test here with v = 2.
STATUS=2: done=0: the test's own line. It printed DONE's mirror, which the predictor has not updated yet.
APB_MON RD addr=04: the monitor publishes the same transfer, later in the same time step.
REG_PREDICT ... value='h2: the predictor maps 0x04 to regmodel.status and predicts 2. The mirror now says DONE is 1.
Writes go the same way. A little later the test clears DONE by writing 2 to STATUS, and the predictor applies the W1C policy: written 2, predicted 0.
UVM_INFO @ 505000: reporter [RegModel] Wrote register via map regmodel.map: regmodel.status=0x2
UVM_INFO dma_tb.sv(151) @ 505000: uvm_test_top.env.apb.mon [APB_MON] WR addr=04 data=00000002
UVM_INFO reg/uvm_reg_predictor.svh(248) @ 505000: uvm_test_top.env.predictor [REG_PREDICT] Observed WRITE transaction to register regmodel.status: value='h2 : updated value = 'h0The lag cost a day while this bench was built. dma_ral_bitbash_test first ran with explicit prediction only. uvm_reg_bit_bash_seq writes a pattern, then takes its expected value from rg.get() straight after write() returns, and reads the register back to compare. With the mirror still one step behind, every expected value was the previous pattern, and the sequence failed on every bit it bashed. The design was fine.
The same lag broke the busy-LEN test the other way round. mirror(UVM_CHECK) takes its expected value from the mirror before it reads. The LEN=9 write had returned, but its prediction had not landed yet, so the check expected the old 4, read 4, and passed: a real mismatch hidden.
Both tests now set the env's auto_predict knob, which makes the map predict inside write() and read() as soon as bus2reg returns, so the mirror is current when the call returns. The predictor stays connected, and both predict the same value. That double prediction is harmless for RW and W1C fields; for a toggle (W1T) field, predicting one write twice would toggle the mirror back, so a bench with such fields picks one mode. Code of your own that reads the mirror right after an access has two options: use the value the access returned, or wait until the predictor has run (the next clock edge is enough here) before reading the mirror.
bus_in is never connected to the APB monitor, and auto-prediction is off. A test writes KEY=0xCAFEF00D and then calls rm.key.mirror(st, UVM_CHECK). What happens?The write reaches the DUT, but nothing predicts it, so KEY's mirror stays at its reset value 0. The mirror check reads 0xCAFEF00D, expects 0, and reports a RegModel mismatch. The DUT is right; the missing connection is the bug. A mirror that never moves after writes points at the prediction path first.
done=0. Name two ways to print the DONE bit correctly.Use the returned value, v[1], which is what the bus carried. Or wait until the predictor has run, for example to the next clock edge, before reading get_mirrored_value(). Turning on auto-prediction would also work, but changes the env for one print.
UVM_NOT_OK. Does the mirror change?It depends on the prediction mode and the library. With auto-prediction, the register skips the prediction when the status is not OK. With the stock uvm_reg_predictor, every observed transfer goes to do_predict: in UVM 1.2 the mirror is updated anyway, and in IEEE 1800.2 do_predict skips the update and warns PREDICT_NOK. To keep the mirror unchanged in UVM 1.2, filter in pre_predict or in a custom predictor.
Next: every arrow in the diagram has more to it. Register Access takes the access API, the adapter and the prediction modes apart, one at a time.