An APB adapter and frontdoor access
Connect a register model to an APB driver through an adapter, and predict which bus transfers write, read, update and mirror produce.
- Lesson
- U13.2
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 4 questions
A new APB adapter sets provides_responses = 1 because "the bus returns read data". Its driver writes the read data into the request and calls item_done without a response item, so no response ever reaches the sequence. The first regs.ctrl.write hangs: the transfer finishes on the pins, but the register layer waits for a response item that never comes.
| Step | Register layer | Driver |
|---|---|---|
| 1 | reg2bus builds an apb_item, sends it, waits for item_done | Takes the item |
| 2 | Waiting | Drives setup and access phases, calls item_done |
| 3 | provides_responses is 1: waits for a response item | Waits for the next item |
| 4 | Still waiting, until the test times out | Still waiting |
The adapter promised responses that the driver never sends. Measured: the transfer completes at 25 ns, write never returns, and a 1000 ns test timeout ends the run with UVM_FATAL PH_TIMEOUT.
From a register call to a bus item
In U13.1 the model only kept books. Now its calls reach the design. A frontdoor access uses the real bus, through the same driver a test would use. When a test calls regs.ctrl.write(status, 'h5a), the call travels through four layers and comes back.
regs.ctrl.write(status, 'h5a) in the example. The desired value changes when write() starts, the design at 25 ns and the mirror when the access is predicted at 30 ns.- Register to map0 ns (illustrative)
At 0 ns
writefinds CTRL's address in the map and fills auvm_reg_bus_op: kind, address 0, data 5a, size.More context
Before the operation leaves the register,
writesets the desired value to 5a. The mirror still says 01.Follow the calls, values and waits
- call write(status, 5a): ral_test → regs.ctrl
- data bus op: write, addr 0, data 5a: regs.ctrl → default_map
- Waiting: ral_test — inside write() until 30 ns
- Map to adapter
reg2busturns the operation into anapb_item, the only thing the APB driver understands. The map starts it on the sequencer.More context
From here on the operation is a bus item. Only the adapter knew how to make it.
Follow the calls, values and waits
- data bus op: write, addr 0, data 5a: default_map → apb_adapter
- data apb_item: write 0 <- 5a: apb_adapter → APB sequencer + driver
- Waiting: default_map — waiting for the item to finish
- On the pins25 ns (illustrative)
The driver runs the setup phase at 10 ns and the access phase; the design takes 5a at the 25 ns edge.
More context
The log line "25 ns APB write addr=0 data=5a" is this edge. The design now holds 5a; the model's mirror still says 01.
Follow the calls, values and waits
- data apb_item: write 0 <- 5a: APB sequencer + driver → incr_csr.ctrl
- The item comes back30 ns (illustrative)
At 30 ns the driver calls
item_done. The map callsbus2regon the finished item for its status.More context
bus2regreads the status and data out of the item. A driver that never callsitem_doneleaves the map, and the test, waiting here.Follow the calls, values and waits
- return item_done: finished apb_item: APB sequencer + driver → apb_adapter
- return item_done: finished apb_item: apb_adapter → default_map
- Released: default_map
- Back to the register30 ns (illustrative)
With auto prediction the mirror becomes 5a; the desired value already was, because
writeset it before the access began.writereturnsUVM_IS_OKat 30 ns.More context
The log line "30 ns write(5a) status=UVM_IS_OK mirrored=5a" is printed right after this return. Desired, design and mirror all hold 5a.
Follow the calls, values and waits
- data mirror = 5a: default_map → regs.ctrl
- return status UVM_IS_OK: regs.ctrl → ral_test
- Released: ral_test
Read the diagram as text
- ral_test: Calls regs.ctrl.write(status, 5a) at 0 ns
- regs.ctrl: Desired 5a from 0 ns; mirror 01 until the access is predicted
- default_map: Address 0; fills a uvm_reg_bus_op
- apb_adapter: reg2bus builds an apb_item; bus2reg reads back data and status
- APB sequencer + driver: Setup at 10 ns, access completes at 25 ns, item_done at 30 ns
- incr_csr.ctrl: Holds 5a after the 25 ns edge
- ral_test → regs.ctrl: write() call
- regs.ctrl → default_map: kind, addr 0, data 5a
- default_map → apb_adapter: reg2bus(rw)
- apb_adapter → APB sequencer + driver: apb_item, started on the sequencer
- APB sequencer + driver → incr_csr.ctrl: APB write at 25 ns
- default_map → regs.ctrl: status; predict 5a (auto prediction)
The register ledger with a bus: desired 5a at 0 ns, design 5a at 25 ns, mirror 5a at 30 ns.
None of this is visible to the test. It calls one task and gets back a status, and the same call works whether the design sits on APB, AXI or a custom bus. That is the main reason to use a register model: tests talk about registers and fields, and only the adapter knows the protocol.
On the way back, the adapter's bus2reg turns the finished item into a uvm_reg_bus_op again. The map reads its data and its status, and write returns that status to the test.
The adapter in two functions
An adapter extends uvm_reg_adapter and overrides two functions. reg2bus(rw) creates a bus item from a register operation. bus2reg(item, rw) fills a register operation from a bus item. Everything protocol-specific stays in the adapter, so the same register model can run on APB today and on another bus later.
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_item t = apb_item::type_id::create("t");
t.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
t.addr = rw.addr;
t.data = rw.data;
return t;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_item t;
if (!$cast(t, bus_item)) `uvm_fatal("NOTAPB", "bus2reg needs an apb_item")
rw.kind = t.write ? UVM_WRITE : UVM_READ;
rw.addr = t.addr;
rw.data = t.data; // for a read, the value the driver captured
rw.status = UVM_IS_OK;
endfunctionThe adapter is a uvm_object, created once by the environment and shared by every register access on that bus. It keeps no state between calls, so one adapter can serve any number of maps that use the same driver.
Two flags in the constructor describe the driver. supports_byte_enable says whether the bus can write single bytes; this APB slice cannot. provides_responses says how the driver returns results. With 0, the driver writes read data into the request object, and the map calls bus2reg on that same object. With 1, the map waits for a separate response item, as in U4.2.
Which one applies is a property of the driver, not of the protocol. The same APB bus can have a driver of either kind, so read the driver before setting the flag.
The flag must match the driver. Set it to 1 for a driver that never sends a response item, and every access hangs, as the hook showed. On this bench the APB write completed at 25 ns and write never returned. Set it to 0 for a driver that does send responses, and those responses pile up unread.
Connecting the model to the sequencer
One call in connect_phase joins the layers: regs.default_map.set_sequencer(sqr, adapter). From then on, every frontdoor access through default_map goes to sqr and is translated by adapter.
A second call chooses how the mirror is kept up to date. set_auto_predict(1) tells the map to update the mirror itself after each access it makes. It is the simplest choice, and the example uses it. Lesson U13.3 shows the other choice, a predictor fed by a monitor, and why it matters when other code also uses the bus.
Four frontdoor operations
| Call | Bus transfers | Afterwards (auto prediction on) |
|---|---|---|
write(status, v) | One write of v | Desired and mirrored are v |
read(status, v) | One read; v is the read data | Desired and mirrored are the read data |
update(status) | For CTRL: one write of the desired value, or none if needs_update() is 0 | Mirrored equals desired |
mirror(status, UVM_CHECK) | One read | Mirror compared with the read data first (an error on mismatch), then set to it |
For the nonvolatile read-write CTRL register. Each call blocks until its transfers finish and returns a status. Fields with other access policies, such as RC or W1C, update the mirror differently.
update is where U13.1's two values pay off. A test can set several fields and then call update once. For a nonvolatile read-write register like CTRL, update writes only when the desired value differs from the mirror. So calling update again sends nothing. A volatile writable field always counts as needing an update.
mirror with UVM_CHECK is the register model's checker. It reads the register and compares the result with what the model expected. A mismatch is reported as a UVM_ERROR, so a design that loses a write fails the test.
Always check the status
Every frontdoor call returns a status through its first argument. UVM_IS_OK means the access completed. UVM_NOT_OK means the bus reported an error or the map could not perform it, for example because the register has no address in that map. UVM_HAS_X means a read returned unknown bits.
A test that ignores the status can pass while the design refuses every access. The read data then holds whatever the adapter copied, often 0, and a check against 0 may even succeed.
Ignores the status
regs.ctrl.read(status, value);
if (value != 'h5a) `uvm_error("CTRL", "wrong value")
A failed read goes unnoticed unless its data happens to differ from 0x5a.
Checks it first
regs.ctrl.read(status, value);
if (status != UVM_IS_OK) `uvm_error("CTRL", status.name())
else if (value != 'h5a) `uvm_error("CTRL", "wrong value")
A refused access is reported as such, before its data is trusted.
The status is only as good as the adapter. If bus2reg always writes UVM_IS_OK, as this small example does, errors on the bus never reach the test.
Reading the transfers
The example writes 0x5a, reads it back, sets 0x33 and updates, updates again, and finally mirrors with a check. The driver prints one line per transfer, at the edge where the design accepts it.
One APB write: a setup cycle with psel, then an access cycle with penable. pready is always 1 in this design.
Count the transfers in the log: four, not five. The second update sent nothing because the mirror already matched. Every call returned UVM_IS_OK, which the adapter set in bus2reg. A real APB adapter would copy the bus's error signal, pslverr, into that status, so a refused access is reported as UVM_NOT_OK.
Register sequences and the model handle
Tests rarely call the model from run_phase for long. Register stimulus goes into a sequence that extends uvm_reg_sequence, which adds a model handle of type uvm_reg_block and helpers such as write_reg, read_reg and update_reg. The helpers pass the sequence as the parent of each access, so the accesses belong to it.
class ctrl_setup_seq extends uvm_reg_sequence;
`uvm_object_utils(ctrl_setup_seq)
function new(string name = "ctrl_setup_seq"); super.new(name); endfunction
virtual task body();
incr_regs r;
uvm_status_e status;
if (!$cast(r, model)) `uvm_fatal("SEQ", "model is not an incr_regs")
write_reg(r.ctrl, status, 'h5a);
if (status != UVM_IS_OK) `uvm_error("SEQ", status.name())
endtask
endclass
seq.model = regs;
seq.start(null); // the map supplies the APB sequencer for each accessBecause the sequence holds only model, the same sequence runs on any bench that builds an incr_regs, at block or chip level. It finds the model through its model handle, set by the test or read from the configuration database.
Writing one field of a register
regs.ctrl.mode.write(status, 'h44) writes one field. It goes out alone only when UVM finds the field individually accessible with this map, adapter and layout. That needs the field configured that way, an adapter that sets supports_byte_enable, and no user frontdoor on the register. Otherwise the model writes the whole register.
The whole-register value keeps each neighbour as safe as its policy allows. An ordinary field contributes its mirrored value. A W1C, W1S or W1T neighbour gets 0, so nothing is cleared or set, and a W0C-type neighbour gets 1. A neighbour with an unconditional side effect, such as WC or WS, cannot be protected, and the model warns. Measured on this bench, whose CTRL has one field: one 32-bit APB write of 44, status UVM_IS_OK, and a UVM_WARNING that individual field access is not available.
Try it
Each APB line is a transfer the driver completed; the other lines are the test reporting what each register call returned. Four calls produce transfers; the second update produces none.
Change the design so it stores only the low four bits (ctrl <= {4'b0, bus.pwdata[3:0]};) and run again. The read now returns 0a, and mirror with UVM_CHECK reports the mismatch as a UVM_ERROR.
25 ns APB write addr=0 data=5a 30 ns write(5a) status=UVM_IS_OK mirrored=5a 55 ns APB read addr=0 data=5a 60 ns read status=UVM_IS_OK value=5a 85 ns APB write addr=0 data=33 90 ns set(33), update: mirrored=33 90 ns update again: nothing to send 115 ns APB read addr=0 data=33 120 ns mirror(check) status=UVM_IS_OK UVM_ERROR : 0Check yourself
Remember
- A frontdoor access goes register, map, adapter, sequencer, driver, and back through the adapter; the mirror changes only when the access is predicted.
reg2busmust handle burst kinds too;bus2regcopies kind, address, data and status back, for reads as well as writes.provides_responsesmust match the driver: 0 when it writes results into the request, 1 when it sends separate responses.default_map.set_sequencer(sqr, adapter)connects the model; register stimulus belongs in auvm_reg_sequencethat reaches the block throughmodel.- For a nonvolatile RW register like CTRL,
updatewrites only when desired and mirrored differ;mirror(status, UVM_CHECK)reads and reports a mismatch as an error.
Practice
- Registers that all read zero Fix · Easy