Register-driven configuration and coverage

Program a design from a randomized register model in the order the design needs, reconfigure it safely while it runs, protect registers that share a window from concurrent threads, and collect register coverage.

Lesson
U14.3
Time
30 min
Runs on
Verilator with UVM
Check
4 questions

The design is running, and a test wants a new mode. It randomizes the model and calls regs.update(). The call returns UVM_IS_OK, but the design still runs the old mode, and a pending interrupt has disappeared. The update wrote CTRL while it was locked, and it wrote 1 to the write-1-to-clear IRQ, because that field is volatile.

StepGOModel asks for modeDesign modeIRQ.doneWrites
Configured in order1fefe011
An event arrives1fefe1-
Randomize, then block update()182Still fe0: cleared10
Reconfigured in order10d0d011

Measured in the example below (default seed). After the block update the model's desired value is fe again: the request for 82 is gone. The update cost ten writes: it left the mode unchanged, rewrote the table entries and cleared the pending interrupt.

Configuration lives in the model

A register model is a natural home for a design's configuration. Its fields are rand, so regs.randomize() picks a legal configuration, and constraints in the register or block classes keep it legal. The desired values then hold the plan, and update writes every register whose desired value differs from its mirror.

Randomizing fills each rand field's value, and each field's post_randomize copies it into the desired value. regs.randomize() covers every register declared rand in the block; registers that should not change, like GO here, are simply not declared rand. The example randomizes CTRL and the four table entries one by one.

One update for the whole block

void'(regs.randomize());
regs.update(status);

Writes in the order the registers were created, including every volatile writable field, whatever state the design is in.

A sequence that knows the order

write_reg(r.go, st, 0);       // stop
update_reg(r.ctrl, st);       // configure
foreach (r.tbl[i]) update_reg(r.tbl[i], st);
write_reg(r.go, st, 1);       // start

Stops the design first, writes only the configuration registers, then starts it.

Block update visits registers in creation order, the order of their instance IDs, so it is an implementation detail rather than a promise. Some flows shuffle the configuration registers on purpose to show that order does not matter. That is valid only when it really does not: here GO must come last, and IRQ must not be written at all.

Why the block update went wrong

  1. CTRL is written while locked

    The design ignores it. The lock callback from U14.2 keeps the mirror at the old mode, and prediction copies that old value into the desired value too. The request for the new mode is gone from the model.

  2. IRQ is written although nobody asked

    IRQ.done's desired value is still 0 from reset, while the design holds 1 from the event. A volatile writable field always reports needs_update(), and for a W1C field update writes the inverse of the desired value: 1, which clears the pending event.

  3. Every TBL entry is rewritten

    Each entry's desired value changed, so each costs two transfers through the window.

The fix is a uvm_reg_sequence that encodes the design's rules. It carries a model handle, typed as uvm_reg_block, and helpers such as write_reg and update_reg that pass the sequence as the parent of each access. Start it with cfg.model = regs; cfg.start(null): register calls find the sequencer through the map, so the configuration sequence needs none.

Reconfiguring mid-flight follows the same sequence: stop, configure, start. If the design had traffic in flight, the stop must also wait for it to drain, using the completion rules from U10.

Two threads, one window

The model can be called from several threads at once. Each register has a semaphore: a second access to the same register waits until the first finishes. That keeps a register's mirror consistent, but it protects nothing shared between registers.

Separate semaphores, one shared INDEXTwo threads write different table entries at once. Arrows are APB items in the order the sequencer grants them, without a lock around each pair.
Separate semaphores, one shared INDEXTwo threads write different table entries at once. Arrows are APB items in the order the sequencer grants them, without a lock around each pair. component Thread A:tbl[1].write(11) Holds tbl[1]'ssemaphore component Thread B:tbl[2].write(22) Holds tbl[2]'ssemaphore component APB sequencer Grants waiting itemsin arrival order design INDEX 1, then 2 design DATA ->TBL[INDEX] Both values land inentry 2 1: INDEX = 12: INDEX = 23: DATA = 114: DATA = 22last write wins
  1. Both start

    Each thread holds only its own register's semaphore, so both frontdoors run. A sends INDEX = 1 first.

    More context

    Neither semaphore stops the other thread: tbl[1] and tbl[2] are different registers. Without a lock, nothing reserves the sequencer for A's pair.

    Follow the calls, values and waits
    1. data INDEX = 1: Thread A: tbl[1].write(11) → APB sequencer
    2. data INDEX = 1: APB sequencer → INDEX
    3. Waiting: Thread B: tbl[2].write(22) — INDEX = 2 waiting for a grant
  2. B moves the index

    B's INDEX = 2 was already waiting, so the sequencer grants it before A's DATA item.

    More context

    The sequencer grants waiting items in arrival order. B's INDEX request has been queued since both threads started; A asks for its DATA item only after its INDEX write finishes.

    Follow the calls, values and waits
    1. data INDEX = 2: Thread B: tbl[2].write(22) → APB sequencer
    2. data INDEX = 2: APB sequencer → INDEX
    3. Waiting: Thread A: tbl[1].write(11) — DATA = 11 queued behind B
    4. Released: Thread B: tbl[2].write(22)
  3. A's data lands in entry 2

    A's DATA = 11 is written to TBL[INDEX], and INDEX is now 2.

    More context

    A's frontdoor finishes normally, and auto prediction sets tbl[1]'s mirror to 11. The design's TBL[1] was never written.

    Follow the calls, values and waits
    1. data DATA = 11: Thread A: tbl[1].write(11) → DATA -> TBL[INDEX]
    2. Released: Thread A: tbl[1].write(11)
  4. B's data overwrites it

    B's 22 also goes to TBL[2]. TBL[1] keeps its old value, while the mirror says 11.

    More context

    The spot item's no-lock option ends with tbl[1] = 0 and tbl[2] = 22. Only a later read of tbl[1] would show the damage.

    Follow the calls, values and waits
    1. data DATA = 22: Thread B: tbl[2].write(22) → DATA -> TBL[INDEX]
Read the diagram as text
  • Thread A: tbl[1].write(11): Holds tbl[1]'s semaphore
  • Thread B: tbl[2].write(22): Holds tbl[2]'s semaphore
  • APB sequencer: Grants waiting items in arrival order
  • INDEX: 1, then 2
  • DATA -> TBL[INDEX]: Both values land in entry 2
  • Thread A: tbl[1].write(11) → APB sequencer: 1: INDEX = 1
  • Thread B: tbl[2].write(22) → APB sequencer: 2: INDEX = 2
  • Thread A: tbl[1].write(11) → DATA -> TBL[INDEX]: 3: DATA = 11
  • Thread B: tbl[2].write(22) → DATA -> TBL[INDEX]: 4: DATA = 22
  • APB sequencer → INDEX: last write wins

Measured in the spot item below: without a lock, TBL[1] stays 0 and TBL[2] ends at 22.

The bench's frontdoor calls lock(m_sequencer) before INDEX and unlock after DATA, so the pair is atomic on the bus. grab or a semaphore shared by all entries' frontdoors works too. The example forks two table writes, and the APB trace shows each pair together.

Register coverage

A register model can carry its own coverage. The test enables it before the model is built with uvm_reg::include_coverage("*", UVM_CVR_FIELD_VALS). Each register's constructor asks build_coverage whether to build its coverage model, and the test turns it on afterwards with set_coverage.

HookCalled whenSees
sample(data, byte_en, is_read, map)After a successful frontdoor access under auto prediction, or a transfer the predictor processes; before the mirror changes. Not for backdoor accesses or a direct predictThe value of that access, even one the design ignored
sample_values()Only when the bench calls itThe mirrored values at that moment

Override them in the register class, guard them with get_coverage, and sample a covergroup inside. The example counted 3 access samples and 1 value sample on CTRL.

Choose the hook by the question. Which values were written is sample. Which configurations the design actually ran is sample_values, called after each configuration. It reads the mirror, so it skips the ignored write.

Try it

The first four lines follow the ledger in the table above: configuration in order, the tempting block update, and the safe reconfiguration. The 82 that the block update requested appears only in the coverage. Then the APB trace shows two table writes from two threads, each INDEX and DATA pair together. The last line is the coverage the model collected: three written modes (fe, 82, 0d) by sample, and the final mirror (0d) by sample_values, so the 82 the design never ran is counted once.

Try it: delete the lock and unlock lines in tbl_frontdoor inside incr_bench.svh and run again. Then move include_coverage after super.build_phase and watch the counts drop to 0. Then think it through: after the bad block update, why would stopping GO and calling ctrl.update() not restore 82, and where must the requested configuration be kept?

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 330 ns configured, 11 writes ctrl desired=fe mirror=fe dut=fe | go=1 irq.done=0 650 ns block update asked for mode 82: 10 writes 650 ns after block update ctrl desired=fe mirror=fe dut=fe | go=1 irq.done=0 980 ns reconfigured, 11 writes ctrl desired=d mirror=d dut=d | go=1 irq.done=0 1005 ns APB write 010 <- 1 1035 ns APB write 014 <- 11 1065 ns APB write 010 <- 3 1095 ns APB write 014 <- 33 1100 ns design tbl[1]=11 tbl[3]=33 1100 ns ctrl.mode coverage: 3 access samples, 1 value sample, bins 2 0 1 1 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 outputThe test enables coverage with the scope "regs.*" before the model is built. 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

  virtual function void build_phase(uvm_phase phase);
    uvm_reg::include_coverage("regs.*", UVM_CVR_FIELD_VALS);   // every register under regs?
    super.build_phase(phase);
  endfunction
  virtual function void end_of_elaboration_phase(uvm_phase phase);
    $display("%s coverage=%0d", regs.ctrl.get_full_name(), regs.ctrl.has_coverage(UVM_CVR_FIELD_VALS));
  endfunction
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 threads write tbl[1] and tbl[2] at the same time. Each option is the body of the table-entry frontdoor, which sends INDEX and then DATA with send. Which one lets the threads corrupt the table?
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
ConceptThe design is running (GO.en = 1). A new random configuration changes CTRL.mode and some TBL entries, then the test calls regs.update(). IRQ.done is a volatile W1C field, COUNT is RC, and GO is not random. Which registers does the update write?
ConceptCoverage must record the CTRL modes the design actually ran. CTRL writes are sometimes ignored while GO.en = 1, and a callback keeps the mirror right. Where should the covergroup be sampled?

Remember

  • Randomize the model and let a uvm_reg_sequence program it in the design's order: stop, configure, start.
  • Block update() writes in creation order and writes every volatile writable field; use it only when order and side effects do not matter.
  • A write the design ignores also erases the request from the model once the prediction is corrected.
  • Each register's semaphore serializes that register only; registers sharing a window need a lock, grab or shared semaphore.
  • Enable coverage before the model is built; sample sees each frontdoor or observed access, sample_values sees the mirror when you ask.