A team swaps their in-house APB agent for a purchased one. Register writes still work, but every register read now returns 0, even from registers whose reset value is not 0. Nothing in the register model changed. The new driver sends read data back in a separate response object, and the adapter was still configured for a driver that writes the data into the request. One bit in the adapter's constructor decided which object the model read the data from.
The register map speaks in abstract operations: write 0xCAFEF00D to address 0x0C, 32 bits. The APB agent speaks in apb_items. The adapter is the interpreter between them, and like a court interpreter it works under a contract: it must translate both ways without adding or dropping meaning, and it must say in advance how the other side answers. The abstract operation is a small struct, uvm_reg_bus_op, with six members: kind (UVM_READ or UVM_WRITE), addr, data, n_bits, byte_en and status.
The contract has two functions and two flags. reg2bus() turns an operation into a bus item. bus2reg() turns a completed bus item back into an operation, filling in read data and status. provides_responses tells the map whether the completed item is the request itself or a separate response. supports_byte_enable tells the model whether a single field can be written without touching its neighbours.
Read the alt box carefully: the map calls bus2reg() on exactly one object. If the flag points at the wrong one, the model reads stale data or waits for an object that never comes.
// 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
endclassLine | What it promises | Why it is right for this bench |
|---|---|---|
| No bus access can touch only some byte lanes. | APB3 has no PSTRB. Field writes become whole-register writes (lesson 1). |
| Read data and status come back inside the request. |
|
| The map's address goes on the bus unchanged. | One map at base 0; the map has already added the register offset. |
| Write data passes through. | For reads the driver ignores |
| Only APB items are translated. | A wrong item type is a wiring bug in the testbench, not a DUT behaviour. |
| Reads return what the slave drove. | The predictor also calls |
| A bus error is reported as a status. | The test decides what an error means; the adapter only reports it. |
Notice that bus2reg() serves two callers. The map calls it on the driver's completed request, and the explicit predictor calls it on every item the monitor publishes. That is why it must decode writes as well as reads: for the predictor, the write data is the whole point.
Some drivers answer with a second object: item_done(rsp) or put_response(rsp), with rsp.set_id_info(req). Pipelined drivers and most purchased VIP work this way. For those, set provides_responses = 1, and the map calls get_base_response() after finish_item() returns, matching on the transaction ID. Get the flag wrong and you see one of two failures:
Flag 1, driver never responds: the register task blocks forever in get_base_response(). It looks like a hang in the test, not in the adapter.
Flag 0, driver does respond: bus2reg() reads the request, whose rdata the driver never filled, so reads return 0 or old data. Worse, the unread responses pile up in the sequence's response queue (8 deep by default). UVM 1.2 reports an overflow error on the ninth; the IEEE 1800.2-2020 reference library drops them silently unless you turn the report on. That is the opening story.
Match the flag to the driver, and check it the first time you integrate any agent: one frontdoor read of a register with a known non-zero reset value is enough.
A field write can stay a field write only if the bus can write a subset of bytes. With supports_byte_enable = 1, the map fills rw.byte_en with one bit per byte lane, and the adapter must carry it to the bus. On APB4 that is PSTRB. The library still falls back to a whole-register write if the field is not individually_accessible, if it shares a byte lane with another field, or if the register has a user frontdoor. The bench's APB3 item has no strobe, so this is a sketch:
// snippet: an APB4 adapter that carries byte enables to PSTRB
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb4_item it = apb4_item::type_id::create("it");
it.addr = rw.addr;
it.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE}); // bursts arrive as UVM_BURST_WRITE
it.data = rw.data;
it.strb = it.write ? rw.byte_en[3:0] : 4'h0; // PSTRB is low for reads
return it;
endfunctionIn bus2reg(), copy the strobe back into rw.byte_en. The predictor passes it to do_predict, so a monitored partial write updates only the bytes that were really written. The predictor sets byte_en to all ones before calling bus2reg(), so an adapter that ignores it is treated as full-word.
reg2bus() receives a flat struct, but sometimes the bus item needs more: a protection level, a secure bit, a master ID. Every register task takes an extension argument (any uvm_object), and inside reg2bus() the adapter can call get_item() to reach the whole uvm_reg_item for the current access: the register (element), the extension, the parent sequence, the priority. The map sets that item just before calling reg2bus() and clears it just after, so get_item() is valid only there. The adapter also has a parent_sequence member: if you set it, the map clones that sequence to start the bus items under it.
// snippet: verified in a scratch run with the DMA bench
class apb_tag_ext extends uvm_object;
`uvm_object_utils(apb_tag_ext)
bit [2:0] pprot;
function new(string name = "apb_tag_ext"); super.new(name); endfunction
endclass
class apb_reg_adapter_ext extends apb_reg_adapter;
`uvm_object_utils(apb_reg_adapter_ext)
function new(string name = "apb_reg_adapter_ext"); super.new(name); endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
uvm_reg_item item = get_item(); // the abstract access behind this bus op
apb_tag_ext ext;
reg2bus = super.reg2bus(rw);
if (item != null && item.extension != null && $cast(ext, item.extension))
`uvm_info("EXT", $sformatf("addr %02h: %s carries pprot=%0d", rw.addr,
item.element.get_full_name(), ext.pprot), UVM_LOW)
endfunction
endclass
// In the test: a factory override, then an access that carries the extension.
// apb_reg_adapter::type_id::set_type_override(apb_reg_adapter_ext::get_type());
// ext.pprot = 3'b010;
// renv.regmodel.key.write(st, 32'h1234_5678, .extension(ext));The adapter turns PSLVERR into UVM_NOT_OK. What the model then does with the mirror depends on the library version. The stock uvm_reg_predictor passes every observed transfer to uvm_reg::do_predict, skipping only its read check on UVM_NOT_OK. In IEEE 1800.2, do_predict itself then skips the update and warns PREDICT_NOK, whichever path called it: the explicit predictor, auto-prediction, peek() or poke(). In UVM 1.2, do_predict has no status check, so the mirror is updated even after a slave error. To keep the mirror unchanged there, filter in the predictor's pre_predict() or in a derived predictor. The DMA answers PSLVERR for any unmapped address, so a scratch run adds a model register at 0x14 and accesses it:
scratch run: +UVM_TESTNAME=s_slverr_test (a model register at 0x14, where the DMA has none)
UVM_WARNING reg/uvm_reg.svh(1664) @ 75000: reporter [PREDICT_NOK] status UVM_NOT_OK; skip prediction.
UVM_INFO sacc_tb.sv(183) @ 76000: uvm_test_top [NOK] write: status=UVM_NOT_OK desired=a5a5a5a5 mirrored=00000000
UVM_WARNING reg/uvm_reg.svh(1664) @ 115000: reporter [PREDICT_NOK] status UVM_NOT_OK; skip prediction.
UVM_INFO sacc_tb.sv(187) @ 116000: uvm_test_top [NOK] read: status=UVM_NOT_OK value=00000000 mirrored=00000000Both accesses return UVM_NOT_OK and the mirror stays 0, with one PREDICT_NOK warning per transfer from the explicit predictor. The desired value, though, already holds A5A5A5A5: write() sets it before the bus access, so needs_update() is now true and a later update() would retry the write.
If bus2reg() receives the wrong item type, the predictor is connected to the wrong monitor or the map to the wrong sequencer. That is a testbench wiring bug, so the bench stops with uvm_fatal. Returning UVM_NOT_OK instead would disguise the wiring bug as a DUT bus error, and a test that expects bus errors could pass on it. The library's own uvm_reg_tlm_adapter reports uvm_error and returns; either is defensible, but pick one policy for every adapter in the bench and never map a wiring bug to a DUT status.
With the extended adapter swapped in by a factory override, a KEY write carries pprot=2 and a LEN write carries no extension:
scratch run: +UVM_TESTNAME=s_ext_test (with the null check)
UVM_INFO sacc_tb.sv(462) @ 0: reporter [EXT] addr 0c: regmodel.key carries pprot=2
UVM_INFO sacc_tb.sv(480) @ 115000: uvm_test_top [EXT] adapter type: apb_reg_adapter_ext
UVM_ERROR : 0
UVM_FATAL : 0The first line is printed at time 0, before any bus activity: reg2bus() runs when the map builds the item, not when the driver drives it. The LEN write printed nothing, which is correct.
The first version of that adapter checked only $cast(ext, item.extension). The KEY write worked. The LEN write, which passed no extension, crashed the simulation:
scratch run: +UVM_TESTNAME=s_ext_test (first version)
UVM_INFO sacc_tb.sv(462) @ 0: reporter [EXT] addr 0c: regmodel.key carries pprot=2
%Error: sacc_tb.sv:462: Null pointer dereferenced
Aborting...SystemVerilog's $cast succeeds when the source handle is null: it assigns null and returns 1. So the condition was true, ext was null, and ext.pprot dereferenced it. Most accesses in a real bench carry no extension, so the bug appears on the first ordinary register access after someone adds the feature. The fix is the explicit item.extension != null in the listing above. The same rule applies anywhere you $cast an optional object: test for null first.
item_done(rsp) with a cloned response holding rdata, and the adapter has provides_responses = 0. The first eight register reads return 0. What happens on the ninth, and how do you fix it?Each item_done(rsp) queues a response the sequence never takes. On the ninth, UVM 1.2 reports a response queue overflow error; the IEEE 1800.2-2020 reference library drops it silently by default. The reads return 0 because bus2reg() decodes the request. Set provides_responses = 1, or change the driver to fill the request and call item_done() with no argument.
supports_byte_enable = 1, but a write to one 4-bit field still writes the whole register. Name two reasons the library might fall back.Any of: the field was configured with individually_accessible = 0; the field shares a byte lane with another field, so a byte strobe would also hit the neighbour; the register has a user-defined frontdoor. The library prints a RegModel warning naming the reason.
bus2reg() returns UVM_NOT_OK when the cast fails, instead of a fatal. Why can that hide a bug?The cast fails only when the testbench is wired wrong (the predictor on the wrong monitor, or the map on the wrong sequencer). Reported as UVM_NOT_OK, it looks like a DUT bus error. A test that expects or tolerates bus errors would then pass while no register access was ever translated correctly.
Next: the adapter told the model what happened on the bus. Prediction: Auto, Explicit and Passive is about when the model hears it, and why a correct DMA failed a bit-bash on 94 bits.