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.

StepRegister layerDriver
1reg2bus builds an apb_item, sends it, waits for item_doneTakes the item
2WaitingDrives setup and access phases, calls item_done
3provides_responses is 1: waits for a response itemWaits for the next item
4Still waiting, until the test times outStill 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.

One frontdoor write, out and backArrows are calls and items in the order they happen for 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.
One frontdoor write, out and backArrows are calls and items in the order they happen for `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. component ral_test Callsregs.ctrl.write(status,5a) at 0 ns model regs.ctrl Desired 5a from 0 ns;mirror 01 until theaccess is predicted component default_map Address 0; fills auvm_reg_bus_op component apb_adapter reg2bus builds anapb_item; bus2regreads back data andstatus component APB sequencer +driver Setup at 10 ns, accesscompletes at 25 ns,item_done at 30 ns design incr_csr.ctrl Holds 5a after the 25ns edge write() callkind, addr 0, data 5areg2bus(rw)apb_item, started onthe sequencerAPB write at 25 nsstatus; predict 5a(auto prediction)
  1. Register to map0 ns (illustrative)

    At 0 ns write finds CTRL's address in the map and fills a uvm_reg_bus_op: kind, address 0, data 5a, size.

    More context

    Before the operation leaves the register, write sets the desired value to 5a. The mirror still says 01.

    Follow the calls, values and waits
    1. call write(status, 5a): ral_test → regs.ctrl
    2. data bus op: write, addr 0, data 5a: regs.ctrl → default_map
    3. Waiting: ral_test — inside write() until 30 ns
  2. Map to adapter

    reg2bus turns the operation into an apb_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
    1. data bus op: write, addr 0, data 5a: default_map → apb_adapter
    2. data apb_item: write 0 <- 5a: apb_adapter → APB sequencer + driver
    3. Waiting: default_map — waiting for the item to finish
  3. 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
    1. data apb_item: write 0 <- 5a: APB sequencer + driver → incr_csr.ctrl
  4. The item comes back30 ns (illustrative)

    At 30 ns the driver calls item_done. The map calls bus2reg on the finished item for its status.

    More context

    bus2reg reads the status and data out of the item. A driver that never calls item_done leaves the map, and the test, waiting here.

    Follow the calls, values and waits
    1. return item_done: finished apb_item: APB sequencer + driver → apb_adapter
    2. return item_done: finished apb_item: apb_adapter → default_map
    3. Released: default_map
  5. Back to the register30 ns (illustrative)

    With auto prediction the mirror becomes 5a; the desired value already was, because write set it before the access began. write returns UVM_IS_OK at 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
    1. data mirror = 5a: default_map → regs.ctrl
    2. return status UVM_IS_OK: regs.ctrl → ral_test
    3. 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.

The example's adapter. bus2reg must copy the data for reads as well as writes.
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;
endfunction

The 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

CallBus transfersAfterwards (auto prediction on)
write(status, v)One write of vDesired and mirrored are v
read(status, v)One read; v is the read dataDesired and mirrored are the read data
update(status)For CTRL: one write of the desired value, or none if needs_update() is 0Mirrored equals desired
mirror(status, UVM_CHECK)One readMirror 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.

The first write on the APB pins (10 ns clock)
0123time (ns)5152535pselpenablepwritepwdata05asetupaccess: design accepts

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.

A register sequence needs no sequencer of its own: each register access goes to the map's sequencer. U14.3 builds a configuration sequence this way.
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 access

Because 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputThe driver counts the transfers it completes. What line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The incrementer's register slice on a small APB bus.
interface apb_if (input logic clk);
  logic        psel = 0, penable = 0, pwrite = 0;
  logic [31:0] paddr = 0, pwdata = 0;
  logic [31:0] prdata;
  logic        pready;
endinterface

// CTRL at address 0 (8 bits, read-write); every access completes at once.
module incr_csr (apb_if bus);
  logic [7:0] ctrl = 8'h01;
  assign bus.pready = 1;
  assign bus.prdata = (bus.paddr == 0) ? {24'b0, ctrl} : 32'b0;
  always @(posedge bus.clk)
    if (bus.psel && bus.penable && bus.pwrite && bus.paddr == 0)
      ctrl <= bus.pwdata[7:0];
endmodule

class apb_item extends uvm_sequence_item;
  `uvm_object_utils(apb_item)
  bit          write;
  bit   [31:0] addr, data;
  function new(string name = "apb_item"); super.new(name); endfunction
endclass

class ctrl_reg extends uvm_reg;
  `uvm_object_utils(ctrl_reg)
  rand uvm_reg_field mode;
  function new(string name = "ctrl_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  function void build();
    mode = uvm_reg_field::type_id::create("mode");
    mode.configure(this, 8, 0, "RW", 0, 8'h01, 1, 1, 0);
  endfunction
endclass

class incr_regs extends uvm_reg_block;
  `uvm_object_utils(incr_regs)
  rand ctrl_reg ctrl;
  function new(string name = "incr_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  function void build();
    ctrl = ctrl_reg::type_id::create("ctrl");
    ctrl.configure(this);
    ctrl.build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    default_map.add_reg(ctrl, 'h0, "RW");
    lock_model();
  endfunction
endclass

// Translates between a register operation and an APB item.
class apb_adapter extends uvm_reg_adapter;
  `uvm_object_utils(apb_adapter)
  function new(string name = "apb_adapter");
    super.new(name);
    supports_byte_enable = 0;
    provides_responses   = 0;   // the driver writes read data into the request itself
  endfunction
  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;
    rw.status = UVM_IS_OK;
  endfunction
endclass

class apb_driver extends uvm_driver #(apb_item);
  `uvm_component_utils(apb_driver)
  virtual apb_if vif;
  int transfers;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      @(negedge vif.clk);                       // setup phase
      vif.psel <= 1; vif.penable <= 0;
      vif.pwrite <= req.write; vif.paddr <= req.addr; vif.pwdata <= req.data;
      @(negedge vif.clk);                       // access phase
      vif.penable <= 1;
      do @(posedge vif.clk); while (!vif.pready);
      if (!req.write) req.data = vif.prdata;    // read data goes back in the request
      transfers++;
      @(negedge vif.clk);
      vif.psel <= 0; vif.penable <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

class ral_test extends uvm_test;
  `uvm_component_utils(ral_test)
  incr_regs                 regs;
  apb_adapter               adapter;
  apb_driver                drv;
  uvm_sequencer #(apb_item) sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    regs = incr_regs::type_id::create("regs");
    regs.build();
    adapter = apb_adapter::type_id::create("adapter");
    sqr = uvm_sequencer #(apb_item)::type_id::create("sqr", this);
    drv = apb_driver::type_id::create("drv", this);
    if (!uvm_config_db #(virtual apb_if)::get(this, "", "vif", drv.vif))
      `uvm_fatal("NOVIF", "no virtual interface")
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    regs.default_map.set_sequencer(sqr, adapter);   // register operations go through sqr
    regs.default_map.set_auto_predict(1);           // update the mirror after each access
  endfunction
  task run_phase(uvm_phase phase);
    uvm_status_e status;
    phase.raise_objection(this);
    regs.reset();
    regs.ctrl.write(status, 'h11);
    regs.ctrl.set('h11);
    regs.ctrl.update(status);
    regs.ctrl.set('h22);
    regs.ctrl.update(status);
    regs.ctrl.mirror(status, UVM_NO_CHECK);
    $display("transfers=%0d mirrored=%0h", drv.transfers, regs.ctrl.get_mirrored_value());
    phase.drop_objection(this);
  endtask
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  apb_if   bus (clk);
  incr_csr dut (bus);
  initial begin
    uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("ral_test");
  end
endmodule
Spot the bugThe program writes 0x5a, reads it back, and checks both statuses, the read value and the mirror. Each option is the body of the adapter's bus2reg. Which one fails?
ConceptWhat does provides_responses = 0 tell the register layer about the APB driver?
ConceptA test calls regs.ctrl.set('h40) and then regs.ctrl.update(status) twice. The mirror held 0x01 before. How many APB transfers result?

Remember

  • A frontdoor access goes register, map, adapter, sequencer, driver, and back through the adapter; the mirror changes only when the access is predicted.
  • reg2bus must handle burst kinds too; bus2reg copies kind, address, data and status back, for reads as well as writes.
  • provides_responses must 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 a uvm_reg_sequence that reaches the block through model.
  • For a nonvolatile RW register like CTRL, update writes only when desired and mirrored differ; mirror(status, UVM_CHECK) reads and reports a mismatch as an error.

Practice