The next block on your chip has 64 filter coefficients but only two APB addresses: write a coefficient number to IDX at 0x00, then read or write the coefficient through DATA at 0x04. The test writers want to say rm.coef[17].write(st, v) like any other register, and they want mirror checks on the coefficients. The map cannot help: a register map turns a register into one address, and these registers have none. Something has to know the two-step recipe.
A user-defined frontdoor replaces the map's built-in "one register, one bus transfer" step with a sequence you write. When a register has a frontdoor, write() and read() hand the whole access to it: the frontdoor receives the abstract access in rw_info (kind, value, status), issues whatever bus transfers it takes, and fills in the status and read data. Think of a letter to someone in a large office: the post knows only the building's address, so the envelope says "care of reception, attention room 17". The frontdoor is the reception desk that knows how to deliver it.
uvm_reg_frontdoor is a uvm_reg_sequence, so its body() can do anything a sequence can: start bus items on its sequencer (the map fills that in if you leave it null), call other registers' write()/read() with itself as the parent, or start items on a completely different agent. You attach it with rg.set_frontdoor(fd, map), or pass it to map.add_reg(rg, offset, rights, unmapped, fd). An unmapped register has no address of its own and can be reached only through its frontdoor (or a backdoor).
The DMA has no indirect registers, so a scratch test for this lesson adds a tiny APB slave with IDX at 0x00 and a four-word table behind DATA at 0x04, driven by a second instance of the bench's unchanged apb_agent and apb_reg_adapter:
// snippet: the scratch indirect slave (zero wait states, no errors)
module ind_slave (apb_if bus);
logic [1:0] idx;
logic [31:0] tbl [4];
assign bus.pready = 1'b1;
assign bus.pslverr = 1'b0;
always_comb bus.prdata = (bus.paddr == 8'h00) ? {30'b0, idx} : tbl[idx];
always_ff @(posedge bus.clk or negedge bus.rst_n)
if (!bus.rst_n) begin
idx <= 0; foreach (tbl[i]) tbl[i] <= 0;
end else if (bus.psel && bus.penable && bus.pwrite) begin
if (bus.paddr == 8'h00) idx <= bus.pwdata[1:0];
else if (bus.paddr == 8'h04) tbl[idx] <= bus.pwdata;
end
endmoduleThe frontdoor knows its table index and issues two APB items: select, then access. Note that it reports status from both transfers; a frontdoor that forgets to set rw_info.status returns whatever the item held before, and a failed select would go unnoticed:
// snippet: verified in a scratch run with the DMA bench's APB agent
class idx_data_frontdoor extends uvm_reg_frontdoor;
`uvm_object_utils(idx_data_frontdoor)
int unsigned index;
function new(string name = "idx_data_frontdoor"); super.new(name); endfunction
virtual task body();
apb_item sel = apb_item::type_id::create("sel");
apb_item acc = apb_item::type_id::create("acc");
start_item(sel);
sel.addr = 8'h00; sel.write = 1; sel.data = index;
finish_item(sel);
start_item(acc);
acc.addr = 8'h04; acc.write = (rw_info.kind == UVM_WRITE); acc.data = rw_info.value[0];
finish_item(acc);
if (rw_info.kind == UVM_READ) rw_info.value[0] = acc.rdata;
rw_info.status = (sel.slverr || acc.slverr) ? UVM_NOT_OK : UVM_IS_OK;
endtask
endclassThe scratch block models the table twice, to compare the two ways the library offers. tbl_a[] uses the frontdoor above, added as unmapped registers. tbl_b[] uses the library's ready-made `uvm_reg_indirect_data`: a register at the DATA address that is configured with the IDX register and the table, and that installs its own frontdoors on the table registers when it is added to the map:
// snippet: from the scratch ind_block::build()
idx = ind_idx_reg::type_id::create("idx");
idx.configure(this); idx.build();
foreach (tbl_b[i]) begin // the library's way: configure, do not map
tbl_b[i] = ind_word_reg::type_id::create($sformatf("tbl_b%0d", i));
tbl_b[i].configure(this); tbl_b[i].build();
regs_b[i] = tbl_b[i];
end
data = new("data", 32, UVM_NO_COVERAGE); // uvm_reg_indirect_data
data.configure(idx, regs_b, this);
map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
map.add_reg(idx, 'h00, "RW");
map.add_reg(data, 'h04, "RW"); // adds tbl_b[] as unmapped, with frontdoors
foreach (tbl_a[i]) begin // the hand-written way
idx_data_frontdoor fd = idx_data_frontdoor::type_id::create($sformatf("fd%0d", i));
fd.index = i;
tbl_a[i] = ind_word_reg::type_id::create($sformatf("tbl_a%0d", i));
tbl_a[i].configure(this); tbl_a[i].build();
map.add_reg(tbl_a[i], 'h0, "RW", 1, fd); // unmapped: reached only through fd
end
lock_model();uvm_reg_indirect_data also marks itself NO_REG_TESTS, since a bit-bash of the DATA address alone is meaningless, and it refuses set(), get(), peek() and poke() on itself: it is a window, not storage.
The test writes tbl_a[2] with explicit prediction only, reads it back both ways, mirror-checks it, and finally switches the map to auto prediction for one more write. Each line prints the mirrors of idx, tbl_a[2], tbl_b[2] (the same hardware word, modelled the library's way) and tbl_a[3]:
scratch run: +UVM_TESTNAME=s_indirect_test (explicit prediction; auto prediction switched on before the last write)
UVM_INFO sacc_tb.sv(382) @ 96000: uvm_test_top [IND] tbl_a[2].write (UVM_IS_OK) idx=2 tbl_a2=0 tbl_b2=5555aaaa tbl_a3=0
UVM_INFO sacc_tb.sv(391) @ 156000: uvm_test_top [IND] tbl_a[2].read = 5555aaaa
UVM_INFO sacc_tb.sv(393) @ 216000: uvm_test_top [IND] tbl_b[2].read = 5555aaaa
UVM_ERROR reg/uvm_reg.svh(2812) @ 275000: reporter [RegModel] Register "rm.tbl_a2" value read from DUT (0x000000005555aaaa) does not match mirrored value (0x0000000000000000) (valid bit mask = 0x00000000ffffffff)
UVM_INFO reg/uvm_reg.svh(2833) @ 275000: reporter [RegModel] Field value (rm.tbl_a2[31:0]) mismatch read=32'h5555aaaa mirrored=32'h0
UVM_INFO sacc_tb.sv(382) @ 276000: uvm_test_top [IND] tbl_a[2].mirror(CHECK) idx=2 tbl_a2=0 tbl_b2=5555aaaa tbl_a3=0
UVM_INFO sacc_tb.sv(382) @ 336000: uvm_test_top [IND] auto: tbl_a[3].write idx=3 tbl_a2=0 tbl_b2=5555aaaa tbl_a3=beef0003
UVM_ERROR : 1The frontdoor works. The write went out as IDX=2, DATA=5555AAAA, and both reads, through either model, return 5555AAAA from the hardware.
`tbl_a2=0`, but `tbl_b2=5555aaaa`. The predictor saw a write to 0x04. That address belongs to data, the uvm_reg_indirect_data, which routes the prediction to the table entry selected by the IDX mirror: tbl_b[2]. Nothing in the model maps any address to tbl_a[2], so its mirror never moved.
The mirror check fails on a correct DUT: the read returned 5555AAAA, the mirror expected 0. (A failing check normally repairs the mirror through its read's prediction. Here that read, too, was predicted into tbl_b[2], so tbl_a2 still shows 0.)
With auto prediction on, `tbl_a3=beef0003` at once. The map predicts the register the test called, whatever addresses its frontdoor used.
The log is the war story. A hand-written frontdoor plus explicit-only prediction leaves the indirect registers' mirrors frozen at their reset values, and the first mirror check on them fails on a working design. Teams then often "fix" the check rather than the model. The real fixes:
Use `uvm_reg_indirect_data` for a plain index-plus-data scheme. The stock uvm_reg_predictor recognises it and predicts the selected entry, so explicit prediction keeps working, including for traffic from other masters.
Enable auto prediction for the map, as the last line of the log does, if only your register model ever touches the table.
Extend the predictor for schemes the library does not know (paged banks, a page register in another block): track the selector in the predictor and predict the right entry from each DATA transfer.
A second trap is concurrency. The library serializes accesses per register (each register holds its own semaphore while an access is in progress), but every table register is a different register with its own frontdoor object. Two threads writing tbl_a[1] and tbl_a[2] can interleave as IDX=1, IDX=2, DATA, DATA, and both values land in entry 2. uvm_reg_indirect_data's frontdoor locks the IDX and DATA registers for the access; a hand-written one needs a shared lock:
// snippet: one semaphore shared by every frontdoor of the same IDX/DATA pair
class idx_data_frontdoor_safe extends idx_data_frontdoor;
`uvm_object_utils(idx_data_frontdoor_safe)
static semaphore pair_lock = new(1);
function new(string name = "idx_data_frontdoor_safe"); super.new(name); endfunction
virtual task body();
pair_lock.get(1);
super.body();
pair_lock.put(1);
endtask
endclassA sensor behind I2C or SPI is the same problem in a different shape: each register access is a serial transaction (start, device address, register number, data, stop) on an agent that is not the APB agent. One option is a separate map with its own adapter on the serial agent's sequencer, if one register access is one serial item. When it is not, for example when a read needs a write of the register number and then a repeated-start read, a frontdoor on the APB-side model can drive the serial agent directly:
// snippet: a frontdoor that reaches a register over I2C (i2c_item and its agent are illustrative)
class i2c_reg_frontdoor extends uvm_reg_frontdoor;
`uvm_object_utils(i2c_reg_frontdoor)
bit [6:0] dev_addr;
bit [7:0] reg_num;
function new(string name = "i2c_reg_frontdoor"); super.new(name); endfunction
virtual task body();
i2c_item it = i2c_item::type_id::create("it");
start_item(it); // runs on fd.sequencer: set it to the I2C sequencer
it.dev_addr = dev_addr;
it.reg_num = reg_num;
it.read = (rw_info.kind == UVM_READ);
it.data = rw_info.value[0];
finish_item(it);
if (it.read) rw_info.value[0] = it.data;
rw_info.status = it.nack ? UVM_NOT_OK : UVM_IS_OK;
endtask
endclass
// fd.sequencer = env.i2c_agt.sqr; rm.sensor_cfg.set_frontdoor(fd, rm.map);Set fd.sequencer explicitly, because the map would otherwise fill in its own (APB) sequencer. And remember the prediction question from the war story: an APB-side predictor never sees I2C traffic, so either predict from an I2C monitor with its own predictor and map, or rely on auto prediction.
body() issues both bus items correctly but never sets rw_info.status. What does a test see when the select write gets PSLVERR?Whatever status the uvm_reg_item held when the frontdoor started, normally UVM_IS_OK. The failed select goes unreported, the data access lands in the wrong entry, and the test's status check passes. A frontdoor owns the status: set it from every transfer it issued.
tbl_b[1].write() update in the scratch model, and why is tbl_b better off than tbl_a?idx (from the IDX write) and tbl_b[1]: the DATA write is predicted through uvm_reg_indirect_data, which uses the IDX mirror to pick the entry. tbl_a entries are unmapped registers with a hand-written frontdoor, and no address leads the predictor to them.
apb_item for each transfer instead of reusing one?Each item is a separate request to the sequencer and driver, and the driver writes read data back into the request. Reusing one handle while the driver or a subscriber still holds it would corrupt the first transfer's results. New items per transfer is the same rule monitors follow.
Next: with access, prediction and checks understood, Registers in Verification puts them to work: the built-in register sequences, register coverage, memories, multiple maps and the RAL interview set.