Memories and a backdoor without VPI

Reach the design's storage through the frontdoor and through a hand-written backdoor, predict which path triggers a register's side effects, and manage a memory that the model does not mirror.

Lesson
U14.1
Time
30 min
Runs on
Verilator with UVM
Check
6 questions

A test wants to know how many events have happened, so it reads COUNT. The count is gone: COUNT clears itself when the bus reads it, and a later check expects the old total. The tempting fix, a backdoor read, also clears it, because the register model imitates the bus. Only peek leaves the design alone.

The COUNT ledger through three kinds of access
0123time (ns)6090130call3 eventspeekreadbackdoor readbus transfer-noneAPB read 00cnonevalue returned-32design COUNT30model mirror0302the bus read clears itthe model clears it too

Measured in the example below. Two more events arrive between the read and the backdoor read. Only peek returns the value and leaves COUNT unchanged.

Two routes into the same storage

The slice from U13 has grown. Besides CTRL it has GO, a write-1-to-clear IRQ, a read-to-clear COUNT, a table behind an index register and a 16-word RAM. Every register access now takes a path: UVM_FRONTDOOR sends a bus transfer through the map, as in U13.2; UVM_BACKDOOR goes through the register's backdoor object.

Frontdoor and backdoor reach the same storageArrows are calls or transfers, labelled with what crosses them. The frontdoor goes through the map, the adapter and the APB driver; the backdoor goes from the register straight to the design's variables.
Frontdoor and backdoor reach the same storageArrows are calls or transfers, labelled with what crosses them. The frontdoor goes through the map, the adapter and the APB driver; the backdoor goes from the register straight to the design's variables. component lesson_test Calls read, peek, pokeor write onregs.count model regs.count Register model: mirrorand access policy RC component map +apb_adapter Address 0x0C; buildsan apb_item component APB driver Setup and accessphase, 30 ns component incr_backdoor read_func and writeuse hierarchicalreferences design tb.dut.count The design's storage;a bus read clears it register callUVM_FRONTDOORapb_itemAPB transferUVM_BACKDOOR,peek, pokereads or assigns thevariable
  1. 60 ns: peek

    peek calls the backdoor's read_func, which returns tb.dut.count = 3. No transfer and no side effect; the mirror records 3.

    More context

    The log line "60 ns peek -> 3 mirror=3 dut=3". All of it happens in zero time: this backdoor has no timed waits.

    Follow the calls, values and waits
    1. call peek(st, v): lesson_test → regs.count
    2. call peek(st, v): regs.count → incr_backdoor
    3. return count = 3 (variable read): tb.dut.count → incr_backdoor
    4. return count = 3 (variable read): incr_backdoor → regs.count
    5. return count = 3 (variable read): regs.count → lesson_test
  2. 70 ns: frontdoor read

    The map sends an APB read of 0x0C to the driver, which puts it on the pins.

    More context

    Unlike peek, this route takes bus time: the test stays inside read() while the transfer runs.

    Follow the calls, values and waits
    1. call read(st, v), frontdoor: lesson_test → regs.count
    2. call read(st, v), frontdoor: regs.count → map + apb_adapter
    3. data apb_item: read 0x0C: map + apb_adapter → APB driver
    4. data apb_item: read 0x0C: APB driver → tb.dut.count
    5. Waiting: lesson_test — inside read() until 90 ns
  3. 85 ns: the design answers

    The design returns 3 and clears COUNT at 85 ns.

    More context

    The log line "85 ns APB read 00c -> 3". The read itself is the side effect: the design clears COUNT on that edge.

    Follow the calls, values and waits
    1. return prdata = 3: tb.dut.count → APB driver
  4. 90 ns: the read returns

    The 3 travels back through the driver and the map. The access policy is RC, so the mirror predicts 0.

    More context

    The log line "90 ns read -> 3 mirror=0 dut=0": the test gets 3, while mirror and design both hold 0.

    Follow the calls, values and waits
    1. return prdata = 3: APB driver → map + apb_adapter
    2. return v = 3, mirror predicts 0: map + apb_adapter → regs.count
    3. return v = 3, mirror predicts 0: regs.count → lesson_test
    4. Released: lesson_test
  5. 130 ns: backdoor read

    The model reads 2 through the backdoor, sees the RC policy and writes 0 back through the same backdoor, imitating the bus. The mirror keeps the 2 it read.

    More context

    The log line "130 ns backdoor read -> 2 mirror=2 dut=0". The read and the write-back both finish at 130 ns, without a bus cycle.

    Follow the calls, values and waits
    1. call read(st, v, UVM_BACKDOOR): lesson_test → regs.count
    2. call read(st, v, UVM_BACKDOOR): regs.count → incr_backdoor
    3. return count = 2 (variable read): tb.dut.count → incr_backdoor
    4. data count = 0 (RC write-back): incr_backdoor → tb.dut.count
    5. return count = 2 (variable read): incr_backdoor → regs.count
    6. return count = 2 (variable read): regs.count → lesson_test
  6. Both routes update one mirror

    Each route ends by predicting the same register's mirror, but not always to the value now stored. After the frontdoor read the mirror is 0, like the design; after the backdoor read it is the 2 that was read, while the design holds 0.

Read the diagram as text
  • lesson_test: Calls read, peek, poke or write on regs.count
  • regs.count: Register model: mirror and access policy RC
  • map + apb_adapter: Address 0x0C; builds an apb_item
  • APB driver: Setup and access phase, 30 ns
  • incr_backdoor: read_func and write use hierarchical references
  • tb.dut.count: The design's storage; a bus read clears it
  • lesson_test → regs.count: register call
  • regs.count → map + apb_adapter: UVM_FRONTDOOR
  • map + apb_adapter → APB driver: apb_item
  • APB driver → tb.dut.count: APB transfer
  • regs.count → incr_backdoor: UVM_BACKDOOR, peek, poke
  • incr_backdoor → tb.dut.count: reads or assigns the variable

The route decides whether the design sees an access, and which value the mirror records. The register object is the same either way.

The path is the third argument of read and write. Leave it out and the block's default door applies, which is the frontdoor here. A frontdoor access costs bus time and exercises the decoder, the protocol and every side effect. This bench's backdoor uses no bus cycles and no timed waits, so its accesses finish in zero time. Use it to load a memory image, check a value the bus cannot reach, or inspect state between transfers. A backdoor's read and write methods are tasks (only read_func is a function), so another implementation may wait.

Which access leaves side effects

CallPathWhat the design seesMirror afterwards
read(st, v)FrontdoorA bus read: COUNT clearsPredicted from the policy: 0
read(st, v, UVM_BACKDOOR)BackdoorThe model deposits the cleared valueThe value read (2, while the design holds 0)
peek(st, v)BackdoorNothingThe value read
write(st, 1, UVM_BACKDOOR) on IRQBackdoorThe model computes 1 & ~1 = 0 and deposits it0
poke(st, 1) on IRQBackdoorThe raw 1 is deposited: done sets1

Measured for COUNT (RC) and IRQ.done (W1C). read and write imitate bus side effects on either path; peek and poke never do.

Read the last two rows together. Writing 1 to a write-1-to-clear bit through the backdoor clears it, as a bus write would. Poking 1 sets it, which raises an interrupt that no hardware event caused. Use poke to build a state the bus cannot create, and a backdoor write for bus behaviour without bus time.

The imitation comes from the register model, not from your backdoor class. For a backdoor write, uvm_reg reads the current value, applies each field's access policy, then deposits the result. For a backdoor read of an RC field it deposits the cleared value. Your backdoor only moves raw bits.

A backdoor in twenty lines

incr_backdoor extends uvm_reg_backdoor and overrides two methods: read_func fills in the value and write assigns it. Each receives a uvm_reg_item. Its get_element() is the register or memory being accessed, and its get_offset() is the word offset for a memory.

The frontdoor splits a memory burst into one transfer per word, but a backdoor burst arrives as a single item: get_value_size() words starting at the offset. uvm_mem checks only that the first word exists, so the backdoor must check the end. Handling only get_value(0) would load one word and still report success.

Three of the cases in incr_bench.svh. An unknown element, or a burst past the last word, returns UVM_NOT_OK so the caller sees the failure.
virtual task write(uvm_reg_item rw);
  uvm_object     e = rw.get_element();
  uvm_reg_data_t v = rw.get_value(0);
  rw.set_status(UVM_IS_OK);
  case (e.get_name())
    "count": tb.dut.count = v[7:0];
    "ram":                         // a burst is one item: every word, or none
      if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
      else for (int i = 0; i < rw.get_value_size(); i++)
        tb.dut.ram[rw.get_offset() + i] = rw.get_value(i);
    // ... one line per register
    default: rw.set_status(UVM_NOT_OK);
  endcase
endtask

regs.set_backdoor(bkdr);   // in build_phase: every register and memory inherits it

A hierarchical reference such as tb.dut.count is resolved when the bench is compiled, so this backdoor works on any simulator. The cost is coupling: the class names the design's hierarchy, and a renamed signal breaks the build. Keep one backdoor class per block, next to the model.

One condition comes from the design. Verilator rejects a variable that the RTL updates with nonblocking assignments and the backdoor assigns with blocking ones. So the slice updates its registers with blocking assignments and captures read data in the setup phase.

The HDL-path backdoor, and what it needs from the simulator

Without a backdoor object, uvm_reg uses HDL paths: add_hdl_path_slice("count_q", 0, 8) on the register and add_hdl_path("tb.dut") on the block. Access then goes through uvm_hdl_read and uvm_hdl_deposit, which look the signal up by name at run time. Generated models usually carry these paths, so no class per block is needed.

A lookup by name needs the simulator's programming interface (VPI) and read and write visibility of those signals, often enabled by a compile option. uvm_reg_mem_hdl_paths_seq checks that every declared path resolves.

A backdoor can also watch the design. Override is_auto_updated and wait_for_change, and the model updates a volatile field's mirror whenever the signal changes, without any read.

Memories have no mirror

A uvm_mem describes a range of words: new("ram", 16, 32, "RW") is 16 words of 32 bits, placed at 0x100 with add_mem. Unlike a register it keeps no desired or mirrored value. A memory may hold megabytes, and a mirror would cost the test as much again. So read and write take a word offset, and the model remembers nothing afterwards.

Call in the exampleBus transfersResult
ram.write(st, 2, 'h1234)APB write at 0x108peek of word 2 returns 1234
ram.poke(st, 5, 'hcafe)NoneA frontdoor read of word 5 returns cafe
ram.burst_write(st, 8, words)Four APB writes, 0x120-0x12cThe adapter has no burst, so the map sends one transfer per word
ram.burst_write(st, 12, words, UVM_BACKDOOR)NoneOne backdoor item with four words; a burst at 14 is refused with UVM_NOT_OK

Word k sits at 0x100 + 4k because create_map's byte_addressing argument defaults to 1. With 0, words would sit at 0x100, 0x101 and so on.

Without a mirror, a memory check is a comparison you write. Keep the expected words yourself or in a reference model, then peek or read them back. peek checks a large region cheaply; read also proves the bus path.

Sharing a memory without collisions

Two DMA engines need a 4-word buffer each. If each test picks its own offsets, the second buffer can land on part of the first, and the fault surfaces much later as corrupt data. The memory allocation manager, uvm_mem_mam, hands out regions that never overlap. Every uvm_mem already has one, ram.mam, covering all its words.

  1. request_region(16)

    Returns a uvm_mem_region of 4 words at a random free offset. The example got words 4-7 for A and 0-3 for B.

  2. reserve_region(offset, bytes)

    Claims a specific range. One that overlaps an allocated region is refused with an error and returns null.

  3. region.write(st, offset, v)

    Writes relative to the region's start. Call release_region() when the engine is done with it.

The allocator only protects code that asks it. A sequence that writes ram at a fixed offset bypasses it, so route every buffer through the same mam.

Try it

Each ledger line shows the mirror beside the design's value after one call; the APB lines are the only bus transfers. The backdoor burst carries four words in one item without a bus write, and the backdoor refuses a burst that would run past word 15. The one UVM_ERROR is the allocator refusing the overlapping region, which is what the last part demonstrates.

Try it: replace the frontdoor read of COUNT with mirror(st, UVM_CHECK). It also reads over the bus, so COUNT still clears. Then poke IRQ.done with 0 and read IRQ through the frontdoor.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 60 ns 3 events mirror=0 dut=3 60 ns peek -> 3 mirror=3 dut=3 85 ns APB read 00c -> 3 90 ns read -> 3 mirror=0 dut=0 130 ns backdoor read -> 2 mirror=2 dut=0 130 ns backdoor write(1) mirror=0 dut=0 130 ns poke(1) mirror=1 dut=1 155 ns APB write 108 <- 1234 160 ns ram[2] peek -> 1234 185 ns APB read 114 -> cafe 190 ns ram[5] after poke, read -> cafe 215 ns APB write 120 <- 11 305 ns APB write 12c <- 44 310 ns burst_write done: 5 bus writes in total 310 ns backdoor burst at 12 -> 11 22 33 44, still 5 bus writes 310 ns backdoor burst at 14 -> UVM_NOT_OK 310 ns mam: a = words 4-7, b = words 0-3 310 ns mam: a region overlapping a is refused 340 ns a.write(offset 4) -> UVM_IS_OK; ram[8] 11 -> ee UVM_ERROR : 1
Read included file: incr_bench.svh
// incr_bench.svh: the U14 register slice, its APB bus, its register model and the base test.
// main.sv holds only the lesson's test, which extends incr_base_test.

// ---------------- APB bus (from U13; the adapter also accepts memory bursts) ----------------
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

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 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});   // a memory burst arrives as 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;      // this small APB has no pslverr
  endfunction
endclass

// One APB transfer per item. Counts transfers; prints each one while trace is 1.
class apb_driver extends uvm_driver #(apb_item);
  `uvm_component_utils(apb_driver)
  virtual apb_if vif;
  string port  = "APB";
  bit    trace = 1;
  int    n_wr, n_rd;
  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) n_wr++;
      else begin req.data = vif.prdata; n_rd++; end
      if (trace && req.write) $display("%4d ns  %s write %h <- %0h", $time, port, req.addr[11:0], req.data);
      if (trace && !req.write) $display("%4d ns  %s read  %h -> %0h", $time, port, req.addr[11:0], req.data);
      @(negedge vif.clk);
      vif.psel <= 0; vif.penable <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

// ---------------- The register slice (design) ----------------
//   0x00  CTRL   mode[7:0]  read-write, reset 01; writes are ignored while GO.en = 1
//   0x04  GO     en[0]      read-write, reset 0
//   0x08  IRQ    done[0]    an event sets it; writing 1 clears it
//   0x0C  COUNT  n[7:0]     events since the last read; a bus read clears it
//   0x10  INDEX  [1:0]      selects a TBL entry
//   0x14  DATA   [7:0]      reads and writes TBL[INDEX]
//   0x100-0x13C  RAM        16 words of 32 bits
// Bits a register does not implement read as 0. The storage uses blocking assignments, so the
// backdoor below can also assign it; read data is captured in the setup phase, so a read that
// clears COUNT cannot change its own read data.
module incr_csr (apb_if bus, input logic evt);
  logic [7:0]  ctrl  = 8'h01;
  logic        go    = 0;
  logic        done  = 0;
  logic [7:0]  count = 0;
  logic [1:0]  index = 0;
  logic [7:0]  tbl [4];
  logic [31:0] ram [16];
  initial begin
    foreach (tbl[i]) tbl[i] = 0;
    foreach (ram[i]) ram[i] = 0;
  end
  assign bus.pready = 1;

  function automatic logic [31:0] read_value(logic [31:0] a);
    if (a >= 32'h100 && a < 32'h140) return ram[a[5:2]];
    case (a)
      32'h00:  return {24'b0, ctrl};
      32'h04:  return {31'b0, go};
      32'h08:  return {31'b0, done};
      32'h0C:  return {24'b0, count};
      32'h10:  return {30'b0, index};
      32'h14:  return {24'b0, tbl[index]};
      default: return 0;
    endcase
  endfunction

  function automatic void write_value(logic [31:0] a, logic [31:0] d);
    if (a >= 32'h100 && a < 32'h140) ram[a[5:2]] = d;
    else case (a)
      32'h00:  if (!go) ctrl = d[7:0];
      32'h04:  go = d[0];
      32'h08:  if (d[0]) done = 0;
      32'h10:  index = d[1:0];
      32'h14:  tbl[index] = d[7:0];
      default: ;
    endcase
  endfunction

  always @(negedge bus.clk)
    if (bus.psel && !bus.penable) bus.prdata = read_value(bus.paddr);
  always @(posedge bus.clk) begin
    if (evt) begin
      if (count != 8'hff) count = count + 1;
      done = 1;
    end
    if (bus.psel && bus.penable && bus.pwrite) write_value(bus.paddr, bus.pwdata);
    if (bus.psel && bus.penable && !bus.pwrite && bus.paddr == 32'h0C) count = 0;
  end
endmodule

// ---------------- The register model ----------------
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);
    add_coverage(build_coverage(UVM_CVR_FIELD_VALS));   // only if a test asked for it (U14.3)
  endfunction
  virtual 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 go_reg extends uvm_reg;
  `uvm_object_utils(go_reg)
  rand uvm_reg_field en;
  function new(string name = "go_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    en = uvm_reg_field::type_id::create("en");
    en.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass

class irq_reg extends uvm_reg;
  `uvm_object_utils(irq_reg)
  uvm_reg_field done;
  function new(string name = "irq_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    done = uvm_reg_field::type_id::create("done");
    done.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0);   // volatile: the design sets it
  endfunction
endclass

class count_reg extends uvm_reg;
  `uvm_object_utils(count_reg)
  uvm_reg_field n;
  function new(string name = "count_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    n = uvm_reg_field::type_id::create("n");
    n.configure(this, 8, 0, "RC", 1, 8'h00, 1, 0, 0);      // volatile: events change it
  endfunction
endclass

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

// A TBL entry has no address of its own. Its frontdoor reaches entry k through the window:
// write INDEX = k, then write or read DATA. lock() keeps the two transfers together.
class tbl_frontdoor extends uvm_reg_frontdoor;
  `uvm_object_utils(tbl_frontdoor)
  int unsigned k;
  function new(string name = "tbl_frontdoor"); super.new(name); endfunction
  virtual task body();
    apb_item t;
    lock(m_sequencer);
    t = apb_item::type_id::create("index");
    start_item(t);
    t.write = 1; t.addr = 'h10; t.data = k;
    finish_item(t);
    t = apb_item::type_id::create("data");
    start_item(t);
    t.write = (rw_info.get_kind() == UVM_WRITE); t.addr = 'h14; t.data = rw_info.get_value(0);
    finish_item(t);
    unlock(m_sequencer);
    if (rw_info.get_kind() == UVM_READ) rw_info.set_value(t.data, 0);
    rw_info.set_status(UVM_IS_OK);
  endtask
endclass

class incr_regs extends uvm_reg_block;
  `uvm_object_utils(incr_regs)
  rand ctrl_reg  ctrl;
  go_reg         go;
  irq_reg        irq;
  count_reg      count;
  rand tbl_reg   tbl[4];
  uvm_mem        ram;
  function new(string name = "incr_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    ctrl  = ctrl_reg::type_id::create("ctrl");    ctrl.configure(this);  ctrl.build();
    go    = go_reg::type_id::create("go");        go.configure(this);    go.build();
    irq   = irq_reg::type_id::create("irq");      irq.configure(this);   irq.build();
    count = count_reg::type_id::create("count");  count.configure(this); count.build();
    default_map.add_reg(ctrl,  'h00, "RW");
    default_map.add_reg(go,    'h04, "RW");
    default_map.add_reg(irq,   'h08, "RW");
    default_map.add_reg(count, 'h0C, "RW");
    foreach (tbl[i]) begin
      tbl_frontdoor fd = new($sformatf("tbl%0d_fd", i));
      fd.k = i;
      tbl[i] = tbl_reg::type_id::create($sformatf("tbl%0d", i));
      tbl[i].configure(this);
      tbl[i].build();
      default_map.add_reg(tbl[i], 'h0, "RW", 1, fd);    // unmapped: reached through its frontdoor
    end
    //        name   words bits access
    ram = new("ram", 16,   32,  "RW", UVM_NO_COVERAGE);
    ram.configure(this);
    default_map.add_mem(ram, 'h100, "RW");
    lock_model();
  endfunction
endclass

// ---------------- A backdoor without VPI ----------------
// It reads and writes the design's storage through hierarchical references, with no bus
// cycles and no timed waits. One instance, set on the block, serves every register and the
// memory. A memory burst arrives as one item holding every word.
class incr_backdoor extends uvm_reg_backdoor;
  `uvm_object_utils(incr_backdoor)
  function new(string name = "incr_backdoor"); super.new(name); endfunction
  virtual task write(uvm_reg_item rw);
    uvm_object     e = rw.get_element();
    uvm_reg_data_t v = rw.get_value(0);
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  tb.dut.ctrl   = v[7:0];
      "go":    tb.dut.go     = v[0];
      "irq":   tb.dut.done   = v[0];
      "count": tb.dut.count  = v[7:0];
      "tbl0":  tb.dut.tbl[0] = v[7:0];
      "tbl1":  tb.dut.tbl[1] = v[7:0];
      "tbl2":  tb.dut.tbl[2] = v[7:0];
      "tbl3":  tb.dut.tbl[3] = v[7:0];
      "ram":                                   // every word of a burst, or none
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          tb.dut.ram[rw.get_offset() + i] = rw.get_value(i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endtask
  virtual function void read_func(uvm_reg_item rw);
    uvm_object e = rw.get_element();
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  rw.set_value(tb.dut.ctrl, 0);
      "go":    rw.set_value(tb.dut.go, 0);
      "irq":   rw.set_value(tb.dut.done, 0);
      "count": rw.set_value(tb.dut.count, 0);
      "tbl0":  rw.set_value(tb.dut.tbl[0], 0);
      "tbl1":  rw.set_value(tb.dut.tbl[1], 0);
      "tbl2":  rw.set_value(tb.dut.tbl[2], 0);
      "tbl3":  rw.set_value(tb.dut.tbl[3], 0);
      "ram":
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          rw.set_value(tb.dut.ram[rw.get_offset() + i], i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endfunction
endclass

// ---------------- A quirk no access string describes ----------------
// CTRL ignores bus writes while GO.en = 1. After such a write, put the old value back into the
// prediction. A backdoor write still lands, so it is left alone.
class ctrl_lock_cb extends uvm_reg_cbs;
  `uvm_object_utils(ctrl_lock_cb)
  uvm_reg_field en;
  function new(string name = "ctrl_lock_cb"); super.new(name); endfunction
  virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                     inout uvm_reg_data_t value, input uvm_predict_e kind,
                                     input uvm_door_e path, input uvm_reg_map map);
    if (kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && en.get_mirrored_value() == 1)
      value = previous;
  endfunction
endclass

// ---------------- The base test every U14.1-U14.3 program extends ----------------
class incr_base_test extends uvm_test;
  `uvm_component_utils(incr_base_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

  virtual function void build_phase(uvm_phase phase);
    incr_backdoor bkdr = incr_backdoor::type_id::create("bkdr");
    regs = incr_regs::type_id::create("regs");
    regs.build();
    regs.set_backdoor(bkdr);                // inherited by every register and the memory
    install_quirks();
    // Reading the mirror of a volatile field warns that the design may have changed it since.
    // The ledger lines below read those mirrors on purpose, so the warning is switched off.
    uvm_root::get().set_report_id_action("UVM/FLD/GET_MIRRORED_VAL/VOL", UVM_NO_ACTION);
    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

  virtual function void install_quirks();
    ctrl_lock_cb cb = ctrl_lock_cb::type_id::create("ctrl_lock");
    cb.en = regs.go.en;
    uvm_reg_field_cb::add(regs.ctrl.mode, cb);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    regs.default_map.set_sequencer(sqr, adapter);
    regs.default_map.set_auto_predict(1);
  endfunction

  // Every register call returns a status; a refused access must not pass silently.
  function void ok(uvm_status_e st, string what);
    if (st != UVM_IS_OK) `uvm_error("STATUS", {what, " returned ", st.name()})
  endfunction
endclass

module tb;
  bit   clk;
  logic evt = 0;
  always #5 clk = ~clk;
  apb_if   bus (clk);
  incr_csr dut (bus, evt);
  // n hardware events, one every 20 ns: each adds 1 to COUNT and sets IRQ.done
  task automatic events(int n);
    repeat (n) begin @(negedge clk); evt = 1; @(negedge clk); evt = 0; end
  endtask
  initial begin
    uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("lesson_test");
  end
endmodule

Check yourself

Predict the outputThe bench is the example's incr_bench.svh. Four events raise COUNT, a read-to-clear register, to 4. 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::*;
`include "incr_bench.svh"

class lesson_test extends incr_base_test;
  `uvm_component_utils(lesson_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction

  task run_phase(uvm_phase phase);
    uvm_status_e   st;
    uvm_reg_data_t a, b, c;
    phase.raise_objection(this);
    drv.trace = 0;
    regs.reset();
    tb.events(4);
    regs.count.peek(st, a);  ok(st, "peek");
    regs.count.read(st, b);  ok(st, "read");
    regs.count.peek(st, c);  ok(st, "peek");
    $display("peek=%0h read=%0h peek=%0h", a, b, c);
    phase.drop_objection(this);
  endtask
endclass
Read included file: incr_bench.svh
// incr_bench.svh: the U14 register slice, its APB bus, its register model and the base test.
// main.sv holds only the lesson's test, which extends incr_base_test.

// ---------------- APB bus (from U13; the adapter also accepts memory bursts) ----------------
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

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 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});   // a memory burst arrives as 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;      // this small APB has no pslverr
  endfunction
endclass

// One APB transfer per item. Counts transfers; prints each one while trace is 1.
class apb_driver extends uvm_driver #(apb_item);
  `uvm_component_utils(apb_driver)
  virtual apb_if vif;
  string port  = "APB";
  bit    trace = 1;
  int    n_wr, n_rd;
  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) n_wr++;
      else begin req.data = vif.prdata; n_rd++; end
      if (trace && req.write) $display("%4d ns  %s write %h <- %0h", $time, port, req.addr[11:0], req.data);
      if (trace && !req.write) $display("%4d ns  %s read  %h -> %0h", $time, port, req.addr[11:0], req.data);
      @(negedge vif.clk);
      vif.psel <= 0; vif.penable <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

// ---------------- The register slice (design) ----------------
//   0x00  CTRL   mode[7:0]  read-write, reset 01; writes are ignored while GO.en = 1
//   0x04  GO     en[0]      read-write, reset 0
//   0x08  IRQ    done[0]    an event sets it; writing 1 clears it
//   0x0C  COUNT  n[7:0]     events since the last read; a bus read clears it
//   0x10  INDEX  [1:0]      selects a TBL entry
//   0x14  DATA   [7:0]      reads and writes TBL[INDEX]
//   0x100-0x13C  RAM        16 words of 32 bits
// Bits a register does not implement read as 0. The storage uses blocking assignments, so the
// backdoor below can also assign it; read data is captured in the setup phase, so a read that
// clears COUNT cannot change its own read data.
module incr_csr (apb_if bus, input logic evt);
  logic [7:0]  ctrl  = 8'h01;
  logic        go    = 0;
  logic        done  = 0;
  logic [7:0]  count = 0;
  logic [1:0]  index = 0;
  logic [7:0]  tbl [4];
  logic [31:0] ram [16];
  initial begin
    foreach (tbl[i]) tbl[i] = 0;
    foreach (ram[i]) ram[i] = 0;
  end
  assign bus.pready = 1;

  function automatic logic [31:0] read_value(logic [31:0] a);
    if (a >= 32'h100 && a < 32'h140) return ram[a[5:2]];
    case (a)
      32'h00:  return {24'b0, ctrl};
      32'h04:  return {31'b0, go};
      32'h08:  return {31'b0, done};
      32'h0C:  return {24'b0, count};
      32'h10:  return {30'b0, index};
      32'h14:  return {24'b0, tbl[index]};
      default: return 0;
    endcase
  endfunction

  function automatic void write_value(logic [31:0] a, logic [31:0] d);
    if (a >= 32'h100 && a < 32'h140) ram[a[5:2]] = d;
    else case (a)
      32'h00:  if (!go) ctrl = d[7:0];
      32'h04:  go = d[0];
      32'h08:  if (d[0]) done = 0;
      32'h10:  index = d[1:0];
      32'h14:  tbl[index] = d[7:0];
      default: ;
    endcase
  endfunction

  always @(negedge bus.clk)
    if (bus.psel && !bus.penable) bus.prdata = read_value(bus.paddr);
  always @(posedge bus.clk) begin
    if (evt) begin
      if (count != 8'hff) count = count + 1;
      done = 1;
    end
    if (bus.psel && bus.penable && bus.pwrite) write_value(bus.paddr, bus.pwdata);
    if (bus.psel && bus.penable && !bus.pwrite && bus.paddr == 32'h0C) count = 0;
  end
endmodule

// ---------------- The register model ----------------
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);
    add_coverage(build_coverage(UVM_CVR_FIELD_VALS));   // only if a test asked for it (U14.3)
  endfunction
  virtual 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 go_reg extends uvm_reg;
  `uvm_object_utils(go_reg)
  rand uvm_reg_field en;
  function new(string name = "go_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    en = uvm_reg_field::type_id::create("en");
    en.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass

class irq_reg extends uvm_reg;
  `uvm_object_utils(irq_reg)
  uvm_reg_field done;
  function new(string name = "irq_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    done = uvm_reg_field::type_id::create("done");
    done.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0);   // volatile: the design sets it
  endfunction
endclass

class count_reg extends uvm_reg;
  `uvm_object_utils(count_reg)
  uvm_reg_field n;
  function new(string name = "count_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    n = uvm_reg_field::type_id::create("n");
    n.configure(this, 8, 0, "RC", 1, 8'h00, 1, 0, 0);      // volatile: events change it
  endfunction
endclass

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

// A TBL entry has no address of its own. Its frontdoor reaches entry k through the window:
// write INDEX = k, then write or read DATA. lock() keeps the two transfers together.
class tbl_frontdoor extends uvm_reg_frontdoor;
  `uvm_object_utils(tbl_frontdoor)
  int unsigned k;
  function new(string name = "tbl_frontdoor"); super.new(name); endfunction
  virtual task body();
    apb_item t;
    lock(m_sequencer);
    t = apb_item::type_id::create("index");
    start_item(t);
    t.write = 1; t.addr = 'h10; t.data = k;
    finish_item(t);
    t = apb_item::type_id::create("data");
    start_item(t);
    t.write = (rw_info.get_kind() == UVM_WRITE); t.addr = 'h14; t.data = rw_info.get_value(0);
    finish_item(t);
    unlock(m_sequencer);
    if (rw_info.get_kind() == UVM_READ) rw_info.set_value(t.data, 0);
    rw_info.set_status(UVM_IS_OK);
  endtask
endclass

class incr_regs extends uvm_reg_block;
  `uvm_object_utils(incr_regs)
  rand ctrl_reg  ctrl;
  go_reg         go;
  irq_reg        irq;
  count_reg      count;
  rand tbl_reg   tbl[4];
  uvm_mem        ram;
  function new(string name = "incr_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    ctrl  = ctrl_reg::type_id::create("ctrl");    ctrl.configure(this);  ctrl.build();
    go    = go_reg::type_id::create("go");        go.configure(this);    go.build();
    irq   = irq_reg::type_id::create("irq");      irq.configure(this);   irq.build();
    count = count_reg::type_id::create("count");  count.configure(this); count.build();
    default_map.add_reg(ctrl,  'h00, "RW");
    default_map.add_reg(go,    'h04, "RW");
    default_map.add_reg(irq,   'h08, "RW");
    default_map.add_reg(count, 'h0C, "RW");
    foreach (tbl[i]) begin
      tbl_frontdoor fd = new($sformatf("tbl%0d_fd", i));
      fd.k = i;
      tbl[i] = tbl_reg::type_id::create($sformatf("tbl%0d", i));
      tbl[i].configure(this);
      tbl[i].build();
      default_map.add_reg(tbl[i], 'h0, "RW", 1, fd);    // unmapped: reached through its frontdoor
    end
    //        name   words bits access
    ram = new("ram", 16,   32,  "RW", UVM_NO_COVERAGE);
    ram.configure(this);
    default_map.add_mem(ram, 'h100, "RW");
    lock_model();
  endfunction
endclass

// ---------------- A backdoor without VPI ----------------
// It reads and writes the design's storage through hierarchical references, with no bus
// cycles and no timed waits. One instance, set on the block, serves every register and the
// memory. A memory burst arrives as one item holding every word.
class incr_backdoor extends uvm_reg_backdoor;
  `uvm_object_utils(incr_backdoor)
  function new(string name = "incr_backdoor"); super.new(name); endfunction
  virtual task write(uvm_reg_item rw);
    uvm_object     e = rw.get_element();
    uvm_reg_data_t v = rw.get_value(0);
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  tb.dut.ctrl   = v[7:0];
      "go":    tb.dut.go     = v[0];
      "irq":   tb.dut.done   = v[0];
      "count": tb.dut.count  = v[7:0];
      "tbl0":  tb.dut.tbl[0] = v[7:0];
      "tbl1":  tb.dut.tbl[1] = v[7:0];
      "tbl2":  tb.dut.tbl[2] = v[7:0];
      "tbl3":  tb.dut.tbl[3] = v[7:0];
      "ram":                                   // every word of a burst, or none
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          tb.dut.ram[rw.get_offset() + i] = rw.get_value(i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endtask
  virtual function void read_func(uvm_reg_item rw);
    uvm_object e = rw.get_element();
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  rw.set_value(tb.dut.ctrl, 0);
      "go":    rw.set_value(tb.dut.go, 0);
      "irq":   rw.set_value(tb.dut.done, 0);
      "count": rw.set_value(tb.dut.count, 0);
      "tbl0":  rw.set_value(tb.dut.tbl[0], 0);
      "tbl1":  rw.set_value(tb.dut.tbl[1], 0);
      "tbl2":  rw.set_value(tb.dut.tbl[2], 0);
      "tbl3":  rw.set_value(tb.dut.tbl[3], 0);
      "ram":
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          rw.set_value(tb.dut.ram[rw.get_offset() + i], i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endfunction
endclass

// ---------------- A quirk no access string describes ----------------
// CTRL ignores bus writes while GO.en = 1. After such a write, put the old value back into the
// prediction. A backdoor write still lands, so it is left alone.
class ctrl_lock_cb extends uvm_reg_cbs;
  `uvm_object_utils(ctrl_lock_cb)
  uvm_reg_field en;
  function new(string name = "ctrl_lock_cb"); super.new(name); endfunction
  virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                     inout uvm_reg_data_t value, input uvm_predict_e kind,
                                     input uvm_door_e path, input uvm_reg_map map);
    if (kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && en.get_mirrored_value() == 1)
      value = previous;
  endfunction
endclass

// ---------------- The base test every U14.1-U14.3 program extends ----------------
class incr_base_test extends uvm_test;
  `uvm_component_utils(incr_base_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

  virtual function void build_phase(uvm_phase phase);
    incr_backdoor bkdr = incr_backdoor::type_id::create("bkdr");
    regs = incr_regs::type_id::create("regs");
    regs.build();
    regs.set_backdoor(bkdr);                // inherited by every register and the memory
    install_quirks();
    // Reading the mirror of a volatile field warns that the design may have changed it since.
    // The ledger lines below read those mirrors on purpose, so the warning is switched off.
    uvm_root::get().set_report_id_action("UVM/FLD/GET_MIRRORED_VAL/VOL", UVM_NO_ACTION);
    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

  virtual function void install_quirks();
    ctrl_lock_cb cb = ctrl_lock_cb::type_id::create("ctrl_lock");
    cb.en = regs.go.en;
    uvm_reg_field_cb::add(regs.ctrl.mode, cb);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    regs.default_map.set_sequencer(sqr, adapter);
    regs.default_map.set_auto_predict(1);
  endfunction

  // Every register call returns a status; a refused access must not pass silently.
  function void ok(uvm_status_e st, string what);
    if (st != UVM_IS_OK) `uvm_error("STATUS", {what, " returned ", st.name()})
  endfunction
endclass

module tb;
  bit   clk;
  logic evt = 0;
  always #5 clk = ~clk;
  apb_if   bus (clk);
  incr_csr dut (bus, evt);
  // n hardware events, one every 20 ns: each adds 1 to COUNT and sets IRQ.done
  task automatic events(int n);
    repeat (n) begin @(negedge clk); evt = 1; @(negedge clk); evt = 0; end
  endtask
  initial begin
    uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("lesson_test");
  end
endmodule
Spot the bugTwo DMA engines each fill a 4-word buffer, A at off_a and then B at off_b. The test then checks buffer A. Each option is the code that chooses the offsets. Which one corrupts buffer A?
Read included file: incr_bench.svh
// incr_bench.svh: the U14 register slice, its APB bus, its register model and the base test.
// main.sv holds only the lesson's test, which extends incr_base_test.

// ---------------- APB bus (from U13; the adapter also accepts memory bursts) ----------------
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

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 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});   // a memory burst arrives as 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;      // this small APB has no pslverr
  endfunction
endclass

// One APB transfer per item. Counts transfers; prints each one while trace is 1.
class apb_driver extends uvm_driver #(apb_item);
  `uvm_component_utils(apb_driver)
  virtual apb_if vif;
  string port  = "APB";
  bit    trace = 1;
  int    n_wr, n_rd;
  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) n_wr++;
      else begin req.data = vif.prdata; n_rd++; end
      if (trace && req.write) $display("%4d ns  %s write %h <- %0h", $time, port, req.addr[11:0], req.data);
      if (trace && !req.write) $display("%4d ns  %s read  %h -> %0h", $time, port, req.addr[11:0], req.data);
      @(negedge vif.clk);
      vif.psel <= 0; vif.penable <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

// ---------------- The register slice (design) ----------------
//   0x00  CTRL   mode[7:0]  read-write, reset 01; writes are ignored while GO.en = 1
//   0x04  GO     en[0]      read-write, reset 0
//   0x08  IRQ    done[0]    an event sets it; writing 1 clears it
//   0x0C  COUNT  n[7:0]     events since the last read; a bus read clears it
//   0x10  INDEX  [1:0]      selects a TBL entry
//   0x14  DATA   [7:0]      reads and writes TBL[INDEX]
//   0x100-0x13C  RAM        16 words of 32 bits
// Bits a register does not implement read as 0. The storage uses blocking assignments, so the
// backdoor below can also assign it; read data is captured in the setup phase, so a read that
// clears COUNT cannot change its own read data.
module incr_csr (apb_if bus, input logic evt);
  logic [7:0]  ctrl  = 8'h01;
  logic        go    = 0;
  logic        done  = 0;
  logic [7:0]  count = 0;
  logic [1:0]  index = 0;
  logic [7:0]  tbl [4];
  logic [31:0] ram [16];
  initial begin
    foreach (tbl[i]) tbl[i] = 0;
    foreach (ram[i]) ram[i] = 0;
  end
  assign bus.pready = 1;

  function automatic logic [31:0] read_value(logic [31:0] a);
    if (a >= 32'h100 && a < 32'h140) return ram[a[5:2]];
    case (a)
      32'h00:  return {24'b0, ctrl};
      32'h04:  return {31'b0, go};
      32'h08:  return {31'b0, done};
      32'h0C:  return {24'b0, count};
      32'h10:  return {30'b0, index};
      32'h14:  return {24'b0, tbl[index]};
      default: return 0;
    endcase
  endfunction

  function automatic void write_value(logic [31:0] a, logic [31:0] d);
    if (a >= 32'h100 && a < 32'h140) ram[a[5:2]] = d;
    else case (a)
      32'h00:  if (!go) ctrl = d[7:0];
      32'h04:  go = d[0];
      32'h08:  if (d[0]) done = 0;
      32'h10:  index = d[1:0];
      32'h14:  tbl[index] = d[7:0];
      default: ;
    endcase
  endfunction

  always @(negedge bus.clk)
    if (bus.psel && !bus.penable) bus.prdata = read_value(bus.paddr);
  always @(posedge bus.clk) begin
    if (evt) begin
      if (count != 8'hff) count = count + 1;
      done = 1;
    end
    if (bus.psel && bus.penable && bus.pwrite) write_value(bus.paddr, bus.pwdata);
    if (bus.psel && bus.penable && !bus.pwrite && bus.paddr == 32'h0C) count = 0;
  end
endmodule

// ---------------- The register model ----------------
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);
    add_coverage(build_coverage(UVM_CVR_FIELD_VALS));   // only if a test asked for it (U14.3)
  endfunction
  virtual 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 go_reg extends uvm_reg;
  `uvm_object_utils(go_reg)
  rand uvm_reg_field en;
  function new(string name = "go_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    en = uvm_reg_field::type_id::create("en");
    en.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass

class irq_reg extends uvm_reg;
  `uvm_object_utils(irq_reg)
  uvm_reg_field done;
  function new(string name = "irq_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    done = uvm_reg_field::type_id::create("done");
    done.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0);   // volatile: the design sets it
  endfunction
endclass

class count_reg extends uvm_reg;
  `uvm_object_utils(count_reg)
  uvm_reg_field n;
  function new(string name = "count_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    n = uvm_reg_field::type_id::create("n");
    n.configure(this, 8, 0, "RC", 1, 8'h00, 1, 0, 0);      // volatile: events change it
  endfunction
endclass

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

// A TBL entry has no address of its own. Its frontdoor reaches entry k through the window:
// write INDEX = k, then write or read DATA. lock() keeps the two transfers together.
class tbl_frontdoor extends uvm_reg_frontdoor;
  `uvm_object_utils(tbl_frontdoor)
  int unsigned k;
  function new(string name = "tbl_frontdoor"); super.new(name); endfunction
  virtual task body();
    apb_item t;
    lock(m_sequencer);
    t = apb_item::type_id::create("index");
    start_item(t);
    t.write = 1; t.addr = 'h10; t.data = k;
    finish_item(t);
    t = apb_item::type_id::create("data");
    start_item(t);
    t.write = (rw_info.get_kind() == UVM_WRITE); t.addr = 'h14; t.data = rw_info.get_value(0);
    finish_item(t);
    unlock(m_sequencer);
    if (rw_info.get_kind() == UVM_READ) rw_info.set_value(t.data, 0);
    rw_info.set_status(UVM_IS_OK);
  endtask
endclass

class incr_regs extends uvm_reg_block;
  `uvm_object_utils(incr_regs)
  rand ctrl_reg  ctrl;
  go_reg         go;
  irq_reg        irq;
  count_reg      count;
  rand tbl_reg   tbl[4];
  uvm_mem        ram;
  function new(string name = "incr_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    ctrl  = ctrl_reg::type_id::create("ctrl");    ctrl.configure(this);  ctrl.build();
    go    = go_reg::type_id::create("go");        go.configure(this);    go.build();
    irq   = irq_reg::type_id::create("irq");      irq.configure(this);   irq.build();
    count = count_reg::type_id::create("count");  count.configure(this); count.build();
    default_map.add_reg(ctrl,  'h00, "RW");
    default_map.add_reg(go,    'h04, "RW");
    default_map.add_reg(irq,   'h08, "RW");
    default_map.add_reg(count, 'h0C, "RW");
    foreach (tbl[i]) begin
      tbl_frontdoor fd = new($sformatf("tbl%0d_fd", i));
      fd.k = i;
      tbl[i] = tbl_reg::type_id::create($sformatf("tbl%0d", i));
      tbl[i].configure(this);
      tbl[i].build();
      default_map.add_reg(tbl[i], 'h0, "RW", 1, fd);    // unmapped: reached through its frontdoor
    end
    //        name   words bits access
    ram = new("ram", 16,   32,  "RW", UVM_NO_COVERAGE);
    ram.configure(this);
    default_map.add_mem(ram, 'h100, "RW");
    lock_model();
  endfunction
endclass

// ---------------- A backdoor without VPI ----------------
// It reads and writes the design's storage through hierarchical references, with no bus
// cycles and no timed waits. One instance, set on the block, serves every register and the
// memory. A memory burst arrives as one item holding every word.
class incr_backdoor extends uvm_reg_backdoor;
  `uvm_object_utils(incr_backdoor)
  function new(string name = "incr_backdoor"); super.new(name); endfunction
  virtual task write(uvm_reg_item rw);
    uvm_object     e = rw.get_element();
    uvm_reg_data_t v = rw.get_value(0);
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  tb.dut.ctrl   = v[7:0];
      "go":    tb.dut.go     = v[0];
      "irq":   tb.dut.done   = v[0];
      "count": tb.dut.count  = v[7:0];
      "tbl0":  tb.dut.tbl[0] = v[7:0];
      "tbl1":  tb.dut.tbl[1] = v[7:0];
      "tbl2":  tb.dut.tbl[2] = v[7:0];
      "tbl3":  tb.dut.tbl[3] = v[7:0];
      "ram":                                   // every word of a burst, or none
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          tb.dut.ram[rw.get_offset() + i] = rw.get_value(i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endtask
  virtual function void read_func(uvm_reg_item rw);
    uvm_object e = rw.get_element();
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  rw.set_value(tb.dut.ctrl, 0);
      "go":    rw.set_value(tb.dut.go, 0);
      "irq":   rw.set_value(tb.dut.done, 0);
      "count": rw.set_value(tb.dut.count, 0);
      "tbl0":  rw.set_value(tb.dut.tbl[0], 0);
      "tbl1":  rw.set_value(tb.dut.tbl[1], 0);
      "tbl2":  rw.set_value(tb.dut.tbl[2], 0);
      "tbl3":  rw.set_value(tb.dut.tbl[3], 0);
      "ram":
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          rw.set_value(tb.dut.ram[rw.get_offset() + i], i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endfunction
endclass

// ---------------- A quirk no access string describes ----------------
// CTRL ignores bus writes while GO.en = 1. After such a write, put the old value back into the
// prediction. A backdoor write still lands, so it is left alone.
class ctrl_lock_cb extends uvm_reg_cbs;
  `uvm_object_utils(ctrl_lock_cb)
  uvm_reg_field en;
  function new(string name = "ctrl_lock_cb"); super.new(name); endfunction
  virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                     inout uvm_reg_data_t value, input uvm_predict_e kind,
                                     input uvm_door_e path, input uvm_reg_map map);
    if (kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && en.get_mirrored_value() == 1)
      value = previous;
  endfunction
endclass

// ---------------- The base test every U14.1-U14.3 program extends ----------------
class incr_base_test extends uvm_test;
  `uvm_component_utils(incr_base_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

  virtual function void build_phase(uvm_phase phase);
    incr_backdoor bkdr = incr_backdoor::type_id::create("bkdr");
    regs = incr_regs::type_id::create("regs");
    regs.build();
    regs.set_backdoor(bkdr);                // inherited by every register and the memory
    install_quirks();
    // Reading the mirror of a volatile field warns that the design may have changed it since.
    // The ledger lines below read those mirrors on purpose, so the warning is switched off.
    uvm_root::get().set_report_id_action("UVM/FLD/GET_MIRRORED_VAL/VOL", UVM_NO_ACTION);
    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

  virtual function void install_quirks();
    ctrl_lock_cb cb = ctrl_lock_cb::type_id::create("ctrl_lock");
    cb.en = regs.go.en;
    uvm_reg_field_cb::add(regs.ctrl.mode, cb);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    regs.default_map.set_sequencer(sqr, adapter);
    regs.default_map.set_auto_predict(1);
  endfunction

  // Every register call returns a status; a refused access must not pass silently.
  function void ok(uvm_status_e st, string what);
    if (st != UVM_IS_OK) `uvm_error("STATUS", {what, " returned ", st.name()})
  endfunction
endclass

module tb;
  bit   clk;
  logic evt = 0;
  always #5 clk = ~clk;
  apb_if   bus (clk);
  incr_csr dut (bus, evt);
  // n hardware events, one every 20 ns: each adds 1 to COUNT and sets IRQ.done
  task automatic events(int n);
    repeat (n) begin @(negedge clk); evt = 1; @(negedge clk); evt = 0; end
  endtask
  initial begin
    uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("lesson_test");
  end
endmodule
Spot the bugThe test loads four RAM words with one burst_write, then checks them with peek. Each option is the line in reg2bus that decides whether the APB item writes. Which adapter loses the burst?
Read included file: incr_bench.svh
// incr_bench.svh: the U14 register slice, its APB bus, its register model and the base test.
// main.sv holds only the lesson's test, which extends incr_base_test.

// ---------------- APB bus (from U13; the adapter also accepts memory bursts) ----------------
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

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 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});   // a memory burst arrives as 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;      // this small APB has no pslverr
  endfunction
endclass

// One APB transfer per item. Counts transfers; prints each one while trace is 1.
class apb_driver extends uvm_driver #(apb_item);
  `uvm_component_utils(apb_driver)
  virtual apb_if vif;
  string port  = "APB";
  bit    trace = 1;
  int    n_wr, n_rd;
  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) n_wr++;
      else begin req.data = vif.prdata; n_rd++; end
      if (trace && req.write) $display("%4d ns  %s write %h <- %0h", $time, port, req.addr[11:0], req.data);
      if (trace && !req.write) $display("%4d ns  %s read  %h -> %0h", $time, port, req.addr[11:0], req.data);
      @(negedge vif.clk);
      vif.psel <= 0; vif.penable <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

// ---------------- The register slice (design) ----------------
//   0x00  CTRL   mode[7:0]  read-write, reset 01; writes are ignored while GO.en = 1
//   0x04  GO     en[0]      read-write, reset 0
//   0x08  IRQ    done[0]    an event sets it; writing 1 clears it
//   0x0C  COUNT  n[7:0]     events since the last read; a bus read clears it
//   0x10  INDEX  [1:0]      selects a TBL entry
//   0x14  DATA   [7:0]      reads and writes TBL[INDEX]
//   0x100-0x13C  RAM        16 words of 32 bits
// Bits a register does not implement read as 0. The storage uses blocking assignments, so the
// backdoor below can also assign it; read data is captured in the setup phase, so a read that
// clears COUNT cannot change its own read data.
module incr_csr (apb_if bus, input logic evt);
  logic [7:0]  ctrl  = 8'h01;
  logic        go    = 0;
  logic        done  = 0;
  logic [7:0]  count = 0;
  logic [1:0]  index = 0;
  logic [7:0]  tbl [4];
  logic [31:0] ram [16];
  initial begin
    foreach (tbl[i]) tbl[i] = 0;
    foreach (ram[i]) ram[i] = 0;
  end
  assign bus.pready = 1;

  function automatic logic [31:0] read_value(logic [31:0] a);
    if (a >= 32'h100 && a < 32'h140) return ram[a[5:2]];
    case (a)
      32'h00:  return {24'b0, ctrl};
      32'h04:  return {31'b0, go};
      32'h08:  return {31'b0, done};
      32'h0C:  return {24'b0, count};
      32'h10:  return {30'b0, index};
      32'h14:  return {24'b0, tbl[index]};
      default: return 0;
    endcase
  endfunction

  function automatic void write_value(logic [31:0] a, logic [31:0] d);
    if (a >= 32'h100 && a < 32'h140) ram[a[5:2]] = d;
    else case (a)
      32'h00:  if (!go) ctrl = d[7:0];
      32'h04:  go = d[0];
      32'h08:  if (d[0]) done = 0;
      32'h10:  index = d[1:0];
      32'h14:  tbl[index] = d[7:0];
      default: ;
    endcase
  endfunction

  always @(negedge bus.clk)
    if (bus.psel && !bus.penable) bus.prdata = read_value(bus.paddr);
  always @(posedge bus.clk) begin
    if (evt) begin
      if (count != 8'hff) count = count + 1;
      done = 1;
    end
    if (bus.psel && bus.penable && bus.pwrite) write_value(bus.paddr, bus.pwdata);
    if (bus.psel && bus.penable && !bus.pwrite && bus.paddr == 32'h0C) count = 0;
  end
endmodule

// ---------------- The register model ----------------
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);
    add_coverage(build_coverage(UVM_CVR_FIELD_VALS));   // only if a test asked for it (U14.3)
  endfunction
  virtual 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 go_reg extends uvm_reg;
  `uvm_object_utils(go_reg)
  rand uvm_reg_field en;
  function new(string name = "go_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    en = uvm_reg_field::type_id::create("en");
    en.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass

class irq_reg extends uvm_reg;
  `uvm_object_utils(irq_reg)
  uvm_reg_field done;
  function new(string name = "irq_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    done = uvm_reg_field::type_id::create("done");
    done.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0);   // volatile: the design sets it
  endfunction
endclass

class count_reg extends uvm_reg;
  `uvm_object_utils(count_reg)
  uvm_reg_field n;
  function new(string name = "count_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    n = uvm_reg_field::type_id::create("n");
    n.configure(this, 8, 0, "RC", 1, 8'h00, 1, 0, 0);      // volatile: events change it
  endfunction
endclass

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

// A TBL entry has no address of its own. Its frontdoor reaches entry k through the window:
// write INDEX = k, then write or read DATA. lock() keeps the two transfers together.
class tbl_frontdoor extends uvm_reg_frontdoor;
  `uvm_object_utils(tbl_frontdoor)
  int unsigned k;
  function new(string name = "tbl_frontdoor"); super.new(name); endfunction
  virtual task body();
    apb_item t;
    lock(m_sequencer);
    t = apb_item::type_id::create("index");
    start_item(t);
    t.write = 1; t.addr = 'h10; t.data = k;
    finish_item(t);
    t = apb_item::type_id::create("data");
    start_item(t);
    t.write = (rw_info.get_kind() == UVM_WRITE); t.addr = 'h14; t.data = rw_info.get_value(0);
    finish_item(t);
    unlock(m_sequencer);
    if (rw_info.get_kind() == UVM_READ) rw_info.set_value(t.data, 0);
    rw_info.set_status(UVM_IS_OK);
  endtask
endclass

class incr_regs extends uvm_reg_block;
  `uvm_object_utils(incr_regs)
  rand ctrl_reg  ctrl;
  go_reg         go;
  irq_reg        irq;
  count_reg      count;
  rand tbl_reg   tbl[4];
  uvm_mem        ram;
  function new(string name = "incr_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    ctrl  = ctrl_reg::type_id::create("ctrl");    ctrl.configure(this);  ctrl.build();
    go    = go_reg::type_id::create("go");        go.configure(this);    go.build();
    irq   = irq_reg::type_id::create("irq");      irq.configure(this);   irq.build();
    count = count_reg::type_id::create("count");  count.configure(this); count.build();
    default_map.add_reg(ctrl,  'h00, "RW");
    default_map.add_reg(go,    'h04, "RW");
    default_map.add_reg(irq,   'h08, "RW");
    default_map.add_reg(count, 'h0C, "RW");
    foreach (tbl[i]) begin
      tbl_frontdoor fd = new($sformatf("tbl%0d_fd", i));
      fd.k = i;
      tbl[i] = tbl_reg::type_id::create($sformatf("tbl%0d", i));
      tbl[i].configure(this);
      tbl[i].build();
      default_map.add_reg(tbl[i], 'h0, "RW", 1, fd);    // unmapped: reached through its frontdoor
    end
    //        name   words bits access
    ram = new("ram", 16,   32,  "RW", UVM_NO_COVERAGE);
    ram.configure(this);
    default_map.add_mem(ram, 'h100, "RW");
    lock_model();
  endfunction
endclass

// ---------------- A backdoor without VPI ----------------
// It reads and writes the design's storage through hierarchical references, with no bus
// cycles and no timed waits. One instance, set on the block, serves every register and the
// memory. A memory burst arrives as one item holding every word.
class incr_backdoor extends uvm_reg_backdoor;
  `uvm_object_utils(incr_backdoor)
  function new(string name = "incr_backdoor"); super.new(name); endfunction
  virtual task write(uvm_reg_item rw);
    uvm_object     e = rw.get_element();
    uvm_reg_data_t v = rw.get_value(0);
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  tb.dut.ctrl   = v[7:0];
      "go":    tb.dut.go     = v[0];
      "irq":   tb.dut.done   = v[0];
      "count": tb.dut.count  = v[7:0];
      "tbl0":  tb.dut.tbl[0] = v[7:0];
      "tbl1":  tb.dut.tbl[1] = v[7:0];
      "tbl2":  tb.dut.tbl[2] = v[7:0];
      "tbl3":  tb.dut.tbl[3] = v[7:0];
      "ram":                                   // every word of a burst, or none
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          tb.dut.ram[rw.get_offset() + i] = rw.get_value(i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endtask
  virtual function void read_func(uvm_reg_item rw);
    uvm_object e = rw.get_element();
    rw.set_status(UVM_IS_OK);
    case (e.get_name())
      "ctrl":  rw.set_value(tb.dut.ctrl, 0);
      "go":    rw.set_value(tb.dut.go, 0);
      "irq":   rw.set_value(tb.dut.done, 0);
      "count": rw.set_value(tb.dut.count, 0);
      "tbl0":  rw.set_value(tb.dut.tbl[0], 0);
      "tbl1":  rw.set_value(tb.dut.tbl[1], 0);
      "tbl2":  rw.set_value(tb.dut.tbl[2], 0);
      "tbl3":  rw.set_value(tb.dut.tbl[3], 0);
      "ram":
        if (rw.get_offset() + rw.get_value_size() > 16) rw.set_status(UVM_NOT_OK);
        else for (int i = 0; i < rw.get_value_size(); i++)
          rw.set_value(tb.dut.ram[rw.get_offset() + i], i);
      default: rw.set_status(UVM_NOT_OK);
    endcase
  endfunction
endclass

// ---------------- A quirk no access string describes ----------------
// CTRL ignores bus writes while GO.en = 1. After such a write, put the old value back into the
// prediction. A backdoor write still lands, so it is left alone.
class ctrl_lock_cb extends uvm_reg_cbs;
  `uvm_object_utils(ctrl_lock_cb)
  uvm_reg_field en;
  function new(string name = "ctrl_lock_cb"); super.new(name); endfunction
  virtual function void post_predict(input uvm_reg_field fld, input uvm_reg_data_t previous,
                                     inout uvm_reg_data_t value, input uvm_predict_e kind,
                                     input uvm_door_e path, input uvm_reg_map map);
    if (kind == UVM_PREDICT_WRITE && path != UVM_BACKDOOR && en.get_mirrored_value() == 1)
      value = previous;
  endfunction
endclass

// ---------------- The base test every U14.1-U14.3 program extends ----------------
class incr_base_test extends uvm_test;
  `uvm_component_utils(incr_base_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

  virtual function void build_phase(uvm_phase phase);
    incr_backdoor bkdr = incr_backdoor::type_id::create("bkdr");
    regs = incr_regs::type_id::create("regs");
    regs.build();
    regs.set_backdoor(bkdr);                // inherited by every register and the memory
    install_quirks();
    // Reading the mirror of a volatile field warns that the design may have changed it since.
    // The ledger lines below read those mirrors on purpose, so the warning is switched off.
    uvm_root::get().set_report_id_action("UVM/FLD/GET_MIRRORED_VAL/VOL", UVM_NO_ACTION);
    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

  virtual function void install_quirks();
    ctrl_lock_cb cb = ctrl_lock_cb::type_id::create("ctrl_lock");
    cb.en = regs.go.en;
    uvm_reg_field_cb::add(regs.ctrl.mode, cb);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    regs.default_map.set_sequencer(sqr, adapter);
    regs.default_map.set_auto_predict(1);
  endfunction

  // Every register call returns a status; a refused access must not pass silently.
  function void ok(uvm_status_e st, string what);
    if (st != UVM_IS_OK) `uvm_error("STATUS", {what, " returned ", st.name()})
  endfunction
endclass

module tb;
  bit   clk;
  logic evt = 0;
  always #5 clk = ~clk;
  apb_if   bus (clk);
  incr_csr dut (bus, evt);
  // n hardware events, one every 20 ns: each adds 1 to COUNT and sets IRQ.done
  task automatic events(int n);
    repeat (n) begin @(negedge clk); evt = 1; @(negedge clk); evt = 0; end
  endtask
  initial begin
    uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("lesson_test");
  end
endmodule
ConceptCOUNT clears when the bus reads it. Which call returns the current count and leaves COUNT unchanged in the design?
ConceptAfter regs.ram.write(st, 2, 'h1234) returns UVM_IS_OK, what does the register model know about word 2?
ConceptA team replaces its hand-written backdoor with add_hdl_path and add_hdl_path_slice calls. What must the simulator now provide?

Remember

  • The path argument picks the route: UVM_FRONTDOOR is a bus transfer, UVM_BACKDOOR goes through the backdoor object, here in zero time.
  • read and write imitate bus side effects on either path; peek and poke move raw bits and never do.
  • A hand-written uvm_reg_backdoor with hierarchical references needs no VPI; add_hdl_path and uvm_hdl_* do.
  • A uvm_mem has no mirror: access it by offset and check it against values you keep, and make the adapter accept burst kinds.
  • Take buffers from mem.mam so concurrent users never share words, and keep offsets inside the region yourself.