Built-in register tests and quirky registers

Run UVM's built-in register and memory tests knowing what each assumes and costs, exclude what does not fit, and model registers whose behaviour no access string describes.

Lesson
U14.2
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

A block's first regression runs uvm_reg_bit_bash_seq and fails 48 times on CTRL, all on bits 8 to 31. The model and the design agree on the 8-bit mode field. They disagree on the 24 bits CTRL does not implement: the test expects them to read 0, and this design stores whatever is written there.

TestWhat it checksAssumesCost on this slice
uvm_reg_hw_reset_seqEach register reads its reset valueDesign just reset; volatile fields skipped4 writes, 6 reads
uvm_reg_bit_bash_seqEvery bit can be set and cleared, aloneUnimplemented bits read 0238 writes, 238 reads
uvm_mem_walk_seqEach memory word holds its own valueRW memory31 writes, 16 reads
uvm_reg_access_seqFrontdoor and backdoor agreeA backdoor or HDL path16 writes, 8 reads
uvm_mem_access_seqSame, for each memory wordA backdoor or HDL path16 writes, 16 reads

Measured in the example. The table entries are excluded from bit_bash; the other counts include them. bit_bash skips the volatile fields IRQ.done[0] and COUNT.n[7:0] but still bashes the unimplemented bits around them: 2 x (32 + 32 + 31 + 24) = 238.

Running a built-in test

Each built-in test is a uvm_reg_sequence. You give it the block and start it on no sequencer: seq.model = regs; seq.start(null);. It walks the block and its sub-blocks, and its register calls go through each register's map to the map's sequencer. So the map needs set_sequencer and a way to predict, exactly as for your own accesses.

Run them in an order that matches their assumptions. hw_reset comes first, straight after reset, because every later test leaves values behind. uvm_reg_mem_built_in_seq runs the whole set in one call; its tests field selects members, such as UVM_DO_REG_HW_RESET | UVM_DO_MEM_WALK.

These tests are the cheapest check that the model and the decoder agree. They find a wrong offset, a wrong reset value, a field that does not stick, or two registers that alias. They say nothing about what the registers do, so they complement functional tests rather than replace them.

What each test assumes

  1. Reset values

    hw_reset calls mirror(UVM_CHECK) on each register after model.reset(). Fields without a reset, or with checking off, are skipped; a register whose fields are all volatile is skipped entirely, like IRQ and COUNT here.

  2. Bit bashing

    For each of the register's 32 bits, bit_bash writes it flipped and reads back, twice. Unimplemented bits are treated as read-only zeros. Volatile fields and write-only fields are skipped, because their read-back is unpredictable.

  3. Memory walk

    mem_walk writes ~k to word k, reads word k-1 back, then writes k-1 there. Two adjacent words that alias each other show up as a mismatch.

  4. Access through both doors

    reg_access writes through the frontdoor and checks through the backdoor, then the reverse. Without a backdoor or HDL path it reports an error, so it runs here only because of incr_backdoor.

Exclude what a test cannot check with a resource attribute. The key is "REG::" plus the register's full name; the attribute name says which test to skip. NO_REG_TESTS skips all of them, and the more specific ones skip one each.

The example skips the table entries in bit_bash: each bit would cost four transfers through the window, and the window itself is covered by hw_reset and reg_access.
foreach (regs.tbl[i])
  uvm_resource_db #(bit)::set({"REG::", regs.tbl[i].get_full_name()},
                              "NO_REG_BIT_BASH_TEST", 1, this);

Excluding is a decision, not a fix. Record why each exclusion exists. Marking a field volatile to quiet bit_bash also turns off its mirror checks everywhere else.

A quirk the access string cannot describe

CTRL ignores bus writes while GO.en is 1. No access string says that, because an access policy describes a field on its own, and this rule depends on another register. Left alone, the model predicts every CTRL write, and the first mirror(UVM_CHECK) after a write while running reports a mismatch that looks like a design bug.

A post_predict callback keeps the mirror rightArrows show one CTRL write while GO.en = 1: the transfer goes to the design, the prediction goes through the field's callbacks before it reaches the mirror.
A post_predict callback keeps the mirror rightArrows show one CTRL write while GO.en = 1: the transfer goes to the design, the prediction goes through the field's callbacks before it reaches the mirror. component regs.ctrl.write(5a) Frontdoor, GO.en = 1 component APB write 000 <- 5a The design keeps mode = 01 design tb.dut.ctrl Locked while running model Field prediction RW policy: value = 5a component ctrl_lock_cb.post_predict GO.en mirror is 1, path is notbackdoor: value = previous model mode mirror Stays 01, as in the design frontdoorignoredauto predictionpost_predict(previous,value)value = previous
  1. The transfer17725 ns (illustrative)

    The write reaches the design, which keeps mode = 01 because GO.en is 1.

    More context

    The log line "17725 ns APB write 000 <- 5a". The transfer is legal on the bus; the design's lock rule drops it.

    Follow the calls, values and waits
    1. data APB write 000 <- 5a: regs.ctrl.write(5a) → APB write 000 <- 5a
    2. data APB write 000 <- 5a: APB write 000 <- 5a → tb.dut.ctrl
    3. Waiting: regs.ctrl.write(5a) — inside write() until the access is predicted
  2. The prediction17730 ns (illustrative)

    The RW policy predicts 5a, then each callback registered on the field may change the value.

    More context

    Auto prediction runs when the transfer completes, before write returns. Without the callback, 5a would go straight into the mirror.

    Follow the calls, values and waits
    1. data predicted value = 5a: regs.ctrl.write(5a) → Field prediction
    2. call post_predict(01, 5a): Field prediction → ctrl_lock_cb.post_predict
  3. The correction17730 ns (illustrative)

    The callback sees GO.en = 1 and a frontdoor path, so it restores the previous value. The mirror stays 01, and the check passes.

    More context

    The log line "17730 ns ctrl written while running: mirror=1 dut=1": model and design agree, so the mirror(UVM_CHECK) that follows reads 1 at 17755 ns and matches.

    Follow the calls, values and waits
    1. data value = previous 01: ctrl_lock_cb.post_predict → mode mirror
    2. Released: regs.ctrl.write(5a)
Read the diagram as text
  • regs.ctrl.write(5a): Frontdoor, GO.en = 1
  • APB write 000 <- 5a: The design keeps mode = 01
  • tb.dut.ctrl: Locked while running
  • Field prediction: RW policy: value = 5a
  • ctrl_lock_cb.post_predict: GO.en mirror is 1, path is not backdoor: value = previous
  • mode mirror: Stays 01, as in the design
  • regs.ctrl.write(5a) → APB write 000 <- 5a: frontdoor
  • APB write 000 <- 5a → tb.dut.ctrl: ignored
  • regs.ctrl.write(5a) → Field prediction: auto prediction
  • Field prediction → ctrl_lock_cb.post_predict: post_predict(previous, value)
  • ctrl_lock_cb.post_predict → mode mirror: value = previous

The callback changes only the prediction. A backdoor write still lands in the design, so the callback leaves it alone.

post_predict receives the field, its previous mirror, the predicted value by reference, the kind of prediction and the path. Register it with uvm_reg_field_cb::add(regs.ctrl.mode, cb). It runs for the read and write predictions of auto prediction, of an explicit predictor and of backdoor accesses, so test path when the quirk applies only to the bus. A direct predict(v) does not call it.

When the rule is a field on its own, prefer the right access string: write-1-to-clear is "W1C", not "RW" plus a callback. The callback is for rules that cross registers or depend on state the field cannot see.

A register with no address

The four table entries share one window: write INDEX, then read or write DATA. In the model they are ordinary registers added to the map as unmapped with a user frontdoor: add_reg(tbl[i], 0, "RW", 1, fd). Any access to tbl[2] then runs fd.body() instead of the map's built-in frontdoor.

One model access, two bus transfersArrows are calls and transfers for regs.tbl[2].write(st, 'h3c). The register hands its operation to its frontdoor as rw_info.
One model access, two bus transfersArrows are calls and transfers for `regs.tbl[2].write(st, 'h3c)`. The register hands its operation to its frontdoor as `rw_info`. model regs.tbl[2].write(3c) An unmapped register component tbl_frontdoor (k =2) A uvm_reg_frontdoorsequence; reads rw_info component APB sequencer lock() held for both items design INDEX at 0x10 Selects entry 2 design DATA at 0x14 Writes TBL[INDEX] model tbl[2] mirror Auto prediction afterbody() returns: 3c fd.start(sequencer)two apb_itemswrite 2write 3cpredict 3c
  1. Start

    The register sees a user frontdoor in its map, fills fd.rw_info with the operation and starts fd on the map's sequencer.

    More context

    The register has no address in the map, so there is no uvm_reg_bus_op to send. The frontdoor sequence decides what the bus sees.

    Follow the calls, values and waits
    1. call rw_info: write tbl[2] <- 3c: regs.tbl[2].write(3c) → tbl_frontdoor (k = 2)
    2. Waiting: regs.tbl[2].write(3c) — inside write() until body() returns
  2. Index17815 ns (illustrative)

    body() locks the sequencer and sends INDEX = 2.

    More context

    The log line "17815 ns APB write 010 <- 2". The lock keeps any other sequence's item from landing between this write and the next.

    Follow the calls, values and waits
    1. data apb_item: INDEX <- 2: tbl_frontdoor (k = 2) → APB sequencer
    2. data apb_item: INDEX <- 2: APB sequencer → INDEX at 0x10
    3. Waiting: APB sequencer — locked by tbl_frontdoor
  3. Then data17845 ns (illustrative)

    Still under the lock, body() sends DATA = 3c, which the design writes into TBL[2]. A read sends INDEX, reads DATA and copies the data into rw_info.

    More context

    The log line "17845 ns APB write 014 <- 3c". One model access has cost two bus transfers.

    Follow the calls, values and waits
    1. data apb_item: DATA <- 3c: tbl_frontdoor (k = 2) → APB sequencer
    2. data apb_item: DATA <- 3c: APB sequencer → DATA at 0x14
  4. Predict

    Back in the register, auto prediction updates tbl[2]'s mirror as for any frontdoor access.

    More context

    body() unlocks the sequencer before it returns. The later read of tbl[2] prints "17910 ns tbl[2] read -> 3c, mirror=3c".

    Follow the calls, values and waits
    1. return body() returned: tbl_frontdoor (k = 2) → regs.tbl[2].write(3c)
    2. data mirror = 3c: regs.tbl[2].write(3c) → tbl[2] mirror
    3. Released: APB sequencer
    4. Released: regs.tbl[2].write(3c)
Read the diagram as text
  • regs.tbl[2].write(3c): An unmapped register
  • tbl_frontdoor (k = 2): A uvm_reg_frontdoor sequence; reads rw_info
  • APB sequencer: lock() held for both items
  • INDEX at 0x10: Selects entry 2
  • DATA at 0x14: Writes TBL[INDEX]
  • tbl[2] mirror: Auto prediction after body() returns: 3c
  • regs.tbl[2].write(3c) → tbl_frontdoor (k = 2): fd.start(sequencer)
  • tbl_frontdoor (k = 2) → APB sequencer: two apb_items
  • APB sequencer → INDEX at 0x10: write 2
  • APB sequencer → DATA at 0x14: write 3c
  • regs.tbl[2].write(3c) → tbl[2] mirror: predict 3c

Measured: the write costs two APB writes and the read costs a write and a read. U14.3 shows why the lock matters.

The library's own quirky registers: uvm_reg_indirect_data and uvm_reg_fifo

uvm_reg_indirect_data packages exactly this pattern. You give it the index register and the array of table registers. Its configure installs one frontdoor per entry and adds each entry to the map as unmapped, and its own predictions follow the index register's value.

uvm_reg_fifo models a register where writes push and reads pop. It keeps a queue of expected values, set pushes onto it, update writes the pending entries, and reads predict from the head. Backdoor access is not supported, because a FIFO's read and write pointers cannot be deposited.

Other quirks follow the same recipe. Use an access policy when the rule is local, a callback when it depends on other state, and a user frontdoor when the bus path is unusual. Aliased registers, for instance, use a post_predict on one that predicts the other.

Try it

The first five lines are the built-in tests, with the transfers each one cost and the errors so far. Then the two quirks run with the APB trace on: a CTRL write while running, checked by mirror, and one table entry written and read through its window.

Try it: remove install_quirks()'s effect by overriding it with an empty function in lesson_test. The CTRL mirror check then reports a mismatch. Then delete the NO_REG_BIT_BASH_TEST loop and compare the bit_bash count.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: hw_reset writes=4 reads=6 errors=0 bit_bash writes=238 reads=238 errors=0 mem_walk writes=31 reads=16 errors=0 reg_access writes=16 reads=8 errors=0 mem_access writes=16 reads=16 errors=0 17725 ns APB write 000 <- 5a 17730 ns ctrl written while running: mirror=1 dut=1 17755 ns APB read 000 -> 1 17815 ns APB write 010 <- 2 17845 ns APB write 014 <- 3c 17905 ns APB read 014 -> 3c 17910 ns tbl[2] read -> 3c, mirror=3c UVM_ERROR : 0
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 outputGO is a 32-bit register with one read-write bit, en. uvm_reg_single_bit_bash_seq bashes only GO. 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_reg_single_bit_bash_seq bash = uvm_reg_single_bit_bash_seq::type_id::create("bash");
    phase.raise_objection(this);
    drv.trace = 0;
    regs.reset();
    bash.rg = regs.go;          // GO: one implemented bit in a 32-bit register
    bash.start(null);
    $display("writes=%0d reads=%0d", drv.n_wr, drv.n_rd);
    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 bugCTRL ignores bus writes while GO.en = 1, but a backdoor poke always lands. Each option is the body of the post_predict callback installed on CTRL.mode. The test writes and pokes CTRL while running and checks the mirror after each. Which callback fails?
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
ConceptA new design fails uvm_reg_bit_bash_seq on bits 8 to 31 of CTRL, whose model has one 8-bit RW field. uvm_reg_hw_reset_seq passes. What is the most likely explanation?
ConceptWhich built-in sequences report an error, rather than skip, when a register or memory has no backdoor and no HDL path?
ConceptWhy does CTRL's lock need a uvm_reg_cbs callback, when IRQ's write-1-to-clear needs only an access string?

Remember

  • Built-in tests are register sequences: set model and start(null); the map must have a sequencer and a way to predict.
  • Each test has assumptions: hw_reset needs a fresh reset, bit_bash expects unimplemented bits to read 0, the access tests need a backdoor.
  • Exclude a register with a REG:: resource attribute such as NO_REG_BIT_BASH_TEST, and record why.
  • Use the access string for a field's own rule and a post_predict callback for a rule that depends on other state.
  • A register without an address gets a uvm_reg_frontdoor: one model access, several bus transfers, normal prediction.

Practice