The APB peripheral capstone

Verify a register-programmed APB peripheral with the whole track's machinery, follow one byte from the virtual sequence to the comparator, and localise a planted design bug from the bench's report alone.

Lesson
U16.4
Time
35 min
Runs on
Verilator with UVM
Check
5 questions

A checksum peripheral adds or xors the bytes software writes to it. Its block bench passes every test that runs in one mode. The first test that switches modes fails, but only after the switch, and CTRL still reads back the new mode. The report has to tell you where to look before you open the RTL.

PhaseCTRL.modeCoverage binBlocksComparator
1addrst->add1, 2Match
2xoradd->xor3, 4RESULT 4a, expected 4; RESULT a4, expected 8
3xorxor->xor5, 6RESULT 8a, expected 4; RESULT 24, expected 8
4addxor->add7, 8Match

Measured in the example below. Every xor block fails, CTRL reads back correctly, and the add blocks after xor pass: the engine never left add.

The peripheral and its test plan

The design has four registers. CTRL.mode (0x00) selects add or xor. Each write to DATA (0x04) feeds one byte, and RESULT (0x08) holds the checksum since CTRL was last written. IRQ.done (0x0C) is set after every fourth byte and cleared by writing 1. Writing CTRL clears the checksum and must take effect from the next byte.

RequirementCheckCoveragePlan step
R1 add mode sums the bytes, mod 256Comparator: RESULT against the reference modelrst->addPhase 1
R2 xor mode xors the bytesComparatorxor->xorPhase 3
R3 a CTRL write takes the new mode from the next byteComparatoradd->xor, xor->addPhases 2 and 4
R4 IRQ.done after every fourth byte, write 1 clears itService routine reads IRQ = 1Blocks servicedEvery block
R5 every result is read, none twiceEnd scan: PENDING, EXTRA-check_phase
R6 CTRL reads back what was writtenmirror(UVM_CHECK)-End of the plan

The integrated test plan. Each requirement has a check that fails loudly and, where it depends on configuration, a coverage bin that proves it was exercised.

R3 is the requirement a single-mode test cannot reach. Its two bins name changes of mode after traffic, so the plan runs four phases: add, xor, xor again and add. The first configuration after reset gets its own rst-> bins, because a mode chosen before any byte is not a reconfiguration. A goal bin at 0 is a hole in the plan, whatever the comparator says.

Who does what in the bench

ComponentJob hereFrom
apb_agentActive: sequencer, driver and monitor on one interfaceU11.1, U13.2
chk_regs + apb_adapterRegister model with the burst-aware adapterU13.1, U13.2
uvm_reg_predictorExplicit prediction from the monitor; auto prediction offU13.3
chk_modelUntimed reference model of the engine, written from the specificationU9.2
chk_predictor, chk_comparatorPredictor turns operations into expectations; comparator checks RESULT and scans for leftoversU8.1, U7.3
chk_coverageBlocks per mode transitionU15.1
chk_vseqConfiguration through the model, traffic through the agent, interrupt serviceU10.1, U14.3

Everything is in chk_bench.svh; main.sv holds only the design and tb.

One choice holds the scoreboard together. The predictor does not listen to the monitor directly; it listens to the register predictor's reg_ap. That port publishes one register operation per observed transfer, in bus order, after the mirror has been updated. So when a DATA write arrives, the CTRL write before it has already set the mirrored mode, and no same-edge ordering question arises (U13.3).

One byte, end to end

The fourth xor byte, 0x14, from sequence to comparatorArrows are items and calls for block 3 in the example: phase 2, mode xor, bytes 11 12 13 14. The monitor feeds everything the scoreboard sees.
The fourth xor byte, 0x14, from sequence to comparatorArrows are items and calls for block 3 in the example: phase 2, mode xor, bytes 11 12 13 14. The monitor feeds everything the scoreboard sees. sequence chk_vseq Phase 2: CTRL = xor,then four DATA bytes component apb_agent driver APB write of 14 to0x04 design chk_csr Engine still adds:RESULT 4a; IRQ.done =1 component apb_monitor Publishes eachcompleted transfer component uvm_reg_predictor Predicts the register,then writes reg_ap component chk_predictor +chk_model Model xors: block 3expects 4 (add->xor) checker chk_comparator RESULT read 4aagainst 4: mismatch coverage chk_coverage add->xor: one moreblock apb_itemAPB writepins sampledap.write(t)reg_ap: DATA write14exp_ap, act_apblock label
  1. Traffic

    The virtual sequence starts chk_bytes_seq on the agent's sequencer. The fourth byte, 14, goes out as an APB write to DATA.

    More context

    Block 3 is phase 2 of the plan: CTRL was written to xor first, then bytes 11, 12 and 13. CTRL reads back correctly in this run: "REG CTRL read-back mismatches: 0".

    Follow the calls, values and waits
    1. data APB write 14 to DATA (0x04): chk_vseq → apb_agent driver
    2. data APB write 14 to DATA (0x04): apb_agent driver → chk_csr
  2. The design

    The engine adds 11 + 12 + 13 + 14 = 4a, because its copy of the mode is still add. It sets IRQ.done.

  3. Observed and predicted

    The monitor publishes the write; the register predictor updates the DATA register and passes the operation on through reg_ap.

    More context

    uvm_reg_predictor updates the mirror first and only then writes reg_ap, so a subscriber always sees the mirror after the operation it is told about.

    Follow the calls, values and waits
    1. data APB write 14 to DATA (0x04): chk_csr → apb_monitor
    2. data APB write 14 to DATA (0x04): apb_monitor → uvm_reg_predictor
  4. Expected

    The predictor feeds 14 to the reference model, which is in xor mode from the earlier CTRL write: 11 ^ 12 ^ 13 ^ 14 = 4. Block 3 expects 4, labelled add->xor.

    More context

    The model took the xor mode from the mirrored CTRL value when the CTRL write came through reg_ap. Nothing here reads the design.

    Follow the calls, values and waits
    1. data reg op: DATA write 14: uvm_reg_predictor → chk_predictor + chk_model
    2. data block 3: expect 4: chk_predictor + chk_model → chk_comparator
    3. data block label add->xor: chk_predictor + chk_model → chk_coverage
    4. Waiting: chk_comparator — expectation 4 queued for block 3
  5. Checked655 ns (illustrative)

    The sequence services the interrupt and reads RESULT. That read reaches the comparator through the same path: 4a, expected 4, a UVM_ERROR [CMP] at 655 ns.

    More context

    The predictor forwards a RESULT read on its second port, act_ap; the arrow stands for both ports. The log line is "[CMP] block 3 (add->xor): RESULT 4a, expected 4".

    Follow the calls, values and waits
    1. data read RESULT (0x08): chk_vseq → apb_agent driver
    2. data read RESULT (0x08): apb_agent driver → chk_csr
    3. data RESULT read = 4a: chk_csr → apb_monitor
    4. data RESULT read = 4a: apb_monitor → uvm_reg_predictor
    5. data RESULT read = 4a: uvm_reg_predictor → chk_predictor + chk_model
    6. data RESULT read = 4a: chk_predictor + chk_model → chk_comparator
    7. Released: chk_comparator
Read the diagram as text
  • chk_vseq: Phase 2: CTRL = xor, then four DATA bytes
  • apb_agent driver: APB write of 14 to 0x04
  • chk_csr: Engine still adds: RESULT 4a; IRQ.done = 1
  • apb_monitor: Publishes each completed transfer
  • uvm_reg_predictor: Predicts the register, then writes reg_ap
  • chk_predictor + chk_model: Model xors: block 3 expects 4 (add->xor)
  • chk_comparator: RESULT read 4a against 4: mismatch
  • chk_coverage: add->xor: one more block
  • chk_vseq → apb_agent driver: apb_item
  • apb_agent driver → chk_csr: APB write
  • chk_csr → apb_monitor: pins sampled
  • apb_monitor → uvm_reg_predictor: ap.write(t)
  • uvm_reg_predictor → chk_predictor + chk_model: reg_ap: DATA write 14
  • chk_predictor + chk_model → chk_comparator: exp_ap, act_ap
  • chk_predictor + chk_model → chk_coverage: block label

The expectation comes only from observed operations and the specification, never from the design's internal signals.

Every expectation therefore depends only on what was observed on the bus and on the specification. If the reference model and the design disagree, the report names the block and the configuration. The bench never looks inside the design to decide which one is right. The comparator checks results in order, because the bus serializes them. At the end, check_phase reports any expectation that was never read as PENDING, and a read with nothing waiting as EXTRA. Before the objection drops, the test waits a 100 ns quiet window and fails if any transfer arrives in it.

Design decisions behind the bench

An active agent with explicit prediction. The agent drives the bus at block level, but the register model learns only from the monitor. At chip level the processor drives APB; the same agent becomes passive and the scoreboard keeps working, because nothing in it depends on who started a transfer.

An untimed model fed by register operations. chk_model knows nothing about APB timing. It sees operations in the order the bus completed them, so it needs no clock. The same model would serve a test that loads the engine through a DMA port.

In-order checking, with accounting at the end. One APB bus completes one transfer at a time, so results come back in the order blocks finish, and a queue is enough. That assumption is stated in the comparator, and the end scan proves no expectation was left behind.

Coverage of transitions, not modes. Counting add and xor blocks would show full coverage for the xor-only plan. The bins name the change from the previous mode, because the requirement does.

Reading the report

The example runs the design as delivered. Its report gives the evidence; localise before you open the RTL.

  1. Which check fired?

    Four [CMP] errors, all on xor blocks. No PENDING, no EXTRA, no IRQ error: every block was signalled, serviced and read once.

  2. Is the register path at fault?

    REG CTRL read-back mismatches: 0. The design stores and returns the mode correctly, so the decoder and CTRL are fine.

  3. Which bins pass and fail?

    rst->add and xor->add pass; add->xor and xor->xor fail. The design always computes add, whatever CTRL says.

  4. What would explain that?

    A copy of the mode taken once and never refreshed. In chk_csr, op is loaded at the first byte while first is 1, and a CTRL write never sets first again.

ComponentWhat it contributed to this diagnosis
ComparatorFour mismatches, each with its block and transition label
End scan and quiet windowNo PENDING or EXTRA: the failure is wrong data, not lost or late data
Interrupt service8 blocks signalled and cleared: the byte counter and IRQ work
Register modelCTRL read back with no mismatch: the configuration path works
CoverageEvery goal bin hit, so the passing bins mean something; failures sit on exactly the xor bins

Each line of the bench report rules out a part of the design. Together they leave the engine's mode select.

A weaker plan would have hidden the bug. Measured on the same design, a plan that runs xor alone from reset passes: the first byte loads xor into op, and both blocks match. Its coverage line says rst->xor 2 and leaves both R3 bins at 0, which is the warning. An earlier version of this bench labelled that first configuration add->xor, after CTRL's reset value. It reported an R3 bin as hit by a plan that never reconfigured. A bin is only as good as its definition.

The fix, and why the mode copy exists at all

The fix is one line in the CTRL branch: first <= 1;, so the next byte reloads op. Loading op <= bus.pwdata[0] directly also works here.

A copy like op is common in real engines: it keeps a mode change from taking effect in the middle of a block. The specification must say when the copy is taken. The reference model encodes R3 as "from the next byte after a CTRL write", and that sentence is what the comparator enforced.

Try it

The four UVM_ERROR [CMP] lines come from the comparator; the bench report after them is printed by the test's report_phase. Read them in the order of the steps above.

Try it: add first <= 1; to the CTRL branch of chk_csr and run again: the report shows 8 matched blocks and PASS. Then put the bug back and add a test class that sets modes = '{1}: the run passes, and the coverage line shows why it should not be trusted.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: UVM_ERROR chk_bench.svh(265) @ 655: uvm_test_top.env.cmp [CMP] block 3 (add->xor): RESULT 4a, expected 4 UVM_ERROR chk_bench.svh(265) @ 865: uvm_test_top.env.cmp [CMP] block 4 (add->xor): RESULT a4, expected 8 UVM_ERROR chk_bench.svh(265) @ 1105: uvm_test_top.env.cmp [CMP] block 5 (xor->xor): RESULT 8a, expected 4 UVM_ERROR chk_bench.svh(265) @ 1315: uvm_test_top.env.cmp [CMP] block 6 (xor->xor): RESULT 24, expected 8 BENCH REPORT at 1930 ns, 61 APB transfers REG CTRL read-back mismatches: 0 IRQ blocks signalled and cleared: 8 SB 8 blocks expected, 4 matched, 4 mismatched, 0 pending, 0 extra COV rst->add 2, rst->xor 0, add->add 0, add->xor 2, xor->xor 2, xor->add 2 FAIL: 4 UVM_ERROR reports UVM_ERROR : 4
Read included file: chk_bench.svh
// chk_bench.svh: the UVM bench for the checksum peripheral. main.sv holds the design and tb.

// ---------------- APB agent (the bus from U13 and U14, packaged as an agent) ----------------
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;
  endfunction
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    apb_item t = apb_item::type_id::create("t");
    t.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
    t.addr  = rw.addr;
    t.data  = rw.data;
    return t;
  endfunction
  virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    apb_item t;
    if (!$cast(t, bus_item)) `uvm_fatal("NOTAPB", "bus2reg needs an apb_item")
    rw.kind   = t.write ? UVM_WRITE : UVM_READ;
    rw.addr   = t.addr;
    rw.data   = t.data;
    rw.status = UVM_IS_OK;          // this APB has no pslverr
  endfunction
endclass

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

// Publishes every completed transfer, whoever started it.
class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)
  virtual apb_if vif;
  uvm_analysis_port #(apb_item) ap;
  int n;                                         // transfers observed
  function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.psel && vif.penable && vif.pready) begin
        apb_item t = apb_item::type_id::create("t");
        t.write = vif.pwrite;
        t.addr  = vif.paddr;
        t.data  = vif.pwrite ? vif.pwdata : vif.prdata;
        n++;
        ap.write(t);
      end
    end
  endtask
endclass

class apb_agent extends uvm_agent;
  `uvm_component_utils(apb_agent)
  uvm_sequencer #(apb_item) sqr;
  apb_driver                drv;
  apb_monitor               mon;
  virtual apb_if            vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(virtual apb_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "no virtual interface")
    mon = apb_monitor::type_id::create("mon", this);
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(apb_item)::type_id::create("sqr", this);
      drv = apb_driver::type_id::create("drv", this);
    end
  endfunction
  function void connect_phase(uvm_phase phase);
    mon.vif = vif;
    if (get_is_active() == UVM_ACTIVE) begin
      drv.vif = vif;
      drv.seq_item_port.connect(sqr.seq_item_export);
    end
  endfunction
endclass

// ---------------- Register model ----------------
class chk_ctrl extends uvm_reg;
  `uvm_object_utils(chk_ctrl)
  rand uvm_reg_field mode;
  function new(string name = "chk_ctrl"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    mode = uvm_reg_field::type_id::create("mode");
    mode.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass
class chk_data extends uvm_reg;
  `uvm_object_utils(chk_data)
  uvm_reg_field din;
  function new(string name = "chk_data"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    din = uvm_reg_field::type_id::create("din");
    din.configure(this, 8, 0, "WO", 0, 8'h00, 1, 0, 0);
  endfunction
endclass
class chk_result extends uvm_reg;
  `uvm_object_utils(chk_result)
  uvm_reg_field acc;
  function new(string name = "chk_result"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    acc = uvm_reg_field::type_id::create("acc");
    acc.configure(this, 8, 0, "RO", 1, 8'h00, 1, 0, 0);      // volatile: the engine changes it
  endfunction
endclass
class chk_irq extends uvm_reg;
  `uvm_object_utils(chk_irq)
  uvm_reg_field done;
  function new(string name = "chk_irq"); 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 engine sets it
  endfunction
endclass
class chk_regs extends uvm_reg_block;
  `uvm_object_utils(chk_regs)
  rand chk_ctrl ctrl;
  chk_data      data;
  chk_result    result;
  chk_irq       irq;
  function new(string name = "chk_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    ctrl   = chk_ctrl::type_id::create("ctrl");     ctrl.configure(this);   ctrl.build();
    data   = chk_data::type_id::create("data");     data.configure(this);   data.build();
    result = chk_result::type_id::create("result"); result.configure(this); result.build();
    irq    = chk_irq::type_id::create("irq");       irq.configure(this);    irq.build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    default_map.add_reg(ctrl,   'h00, "RW");
    default_map.add_reg(data,   'h04, "WO");
    default_map.add_reg(result, 'h08, "RO");
    default_map.add_reg(irq,    'h0C, "RW");
    lock_model();
  endfunction
endclass

// ---------------- Reference model: untimed, from the specification only ----------------
class chk_model;
  bit       mode;           // 0 = add, 1 = xor
  bit       prev;           // the mode before the last CTRL write
  bit       from_reset;     // the last CTRL write was the first since reset
  bit       configured;
  bit [7:0] acc;
  int       nbytes;
  function void configure(bit m);
    from_reset = !configured;
    configured = 1;
    prev = mode; mode = m; acc = 0; nbytes = 0;
  endfunction
  // Returns 1 when this byte completes a block of four.
  function bit feed(bit [7:0] b);
    acc = mode ? (acc ^ b) : (acc + b);
    nbytes++;
    return (nbytes % 4 == 0);
  endfunction
  function string label();
    string from, to;
    if (from_reset) from = "rst"; else if (prev) from = "xor"; else from = "add";
    if (mode) to = "xor"; else to = "add";
    return {from, "->", to};
  endfunction
endclass

class chk_expect extends uvm_object;
  `uvm_object_utils(chk_expect)
  bit [7:0] value;
  int       block;
  string    label;
  function new(string name = "chk_expect"); super.new(name); endfunction
endclass

// ---------------- Scoreboard: predictor and comparator ----------------
// The predictor listens to the register predictor's reg_ap: one register operation per
// observed transfer, in bus order, published after the mirror was updated. CTRL writes
// configure the model with the mirrored mode; DATA writes feed it; every fourth byte yields
// one expectation. RESULT reads go to the comparator as the actual values.
class chk_predictor extends uvm_subscriber #(uvm_reg_item);
  `uvm_component_utils(chk_predictor)
  chk_regs                          regs;
  chk_model                         model = new;
  uvm_analysis_port #(chk_expect)   exp_ap;
  uvm_analysis_port #(uvm_reg_item) act_ap;
  int                               blocks;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_ap = new("exp_ap", this);
    act_ap = new("act_ap", this);
  endfunction
  function void write(uvm_reg_item t);
    uvm_object e    = t.get_element();
    string     name = e.get_name();
    if (t.get_kind() == UVM_WRITE && name == "ctrl")
      model.configure(regs.ctrl.mode.get_mirrored_value());
    else if (t.get_kind() == UVM_WRITE && name == "data") begin
      bit [7:0] b = t.get_value(0);
      if (model.feed(b)) begin
        chk_expect x = chk_expect::type_id::create("x");
        x.value = model.acc;
        x.block = ++blocks;
        x.label = model.label();
        exp_ap.write(x);
      end
    end
    else if (t.get_kind() == UVM_READ && name == "result")
      act_ap.write(t);
  endfunction
endclass

`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)
class chk_comparator extends uvm_scoreboard;
  `uvm_component_utils(chk_comparator)
  uvm_analysis_imp_exp #(chk_expect, chk_comparator)   exp_in;
  uvm_analysis_imp_act #(uvm_reg_item, chk_comparator) act_in;
  chk_expect pending[$];
  int n_exp, n_match, n_mismatch, n_extra;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_in = new("exp_in", this);
    act_in = new("act_in", this);
  endfunction
  function void write_exp(chk_expect x); pending.push_back(x); n_exp++; endfunction
  function void write_act(uvm_reg_item t);
    bit [7:0]  got = t.get_value(0);
    chk_expect x;
    if (pending.size() == 0) begin
      n_extra++;
      `uvm_error("EXTRA", $sformatf("RESULT read %0h with no block waiting", got))
      return;
    end
    x = pending.pop_front();
    if (got == x.value) n_match++;
    else begin
      n_mismatch++;
      `uvm_error("CMP", $sformatf("block %0d (%s): RESULT %0h, expected %0h", x.block, x.label, got, x.value))
    end
  endfunction
  // End scan: every expectation must have been read.
  function void check_phase(uvm_phase phase);
    foreach (pending[i])
      `uvm_error("PENDING", $sformatf("block %0d (%s): expected %0h was never read", pending[i].block, pending[i].label, pending[i].value))
  endfunction
endclass

// ---------------- Coverage: blocks per mode transition (rst = first configuration) ----------------
class chk_coverage extends uvm_subscriber #(chk_expect);
  `uvm_component_utils(chk_coverage)
  int hits[string];
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(chk_expect t);
    if (hits.exists(t.label)) hits[t.label]++; else hits[t.label] = 1;
  endfunction
  function int count(string bin);
    if (hits.exists(bin)) return hits[bin];
    return 0;
  endfunction
endclass

class chk_env extends uvm_env;
  `uvm_component_utils(chk_env)
  apb_agent                     agent;
  chk_regs                      regs;
  apb_adapter                   adapter;
  uvm_reg_predictor #(apb_item) reg_pred;
  chk_predictor                 pred;
  chk_comparator                cmp;
  chk_coverage                  cov;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    agent    = apb_agent::type_id::create("agent", this);
    regs     = chk_regs::type_id::create("regs");
    regs.build();
    adapter  = apb_adapter::type_id::create("adapter");
    reg_pred = uvm_reg_predictor #(apb_item)::type_id::create("reg_pred", this);
    pred     = chk_predictor::type_id::create("pred", this);
    cmp      = chk_comparator::type_id::create("cmp", this);
    cov      = chk_coverage::type_id::create("cov", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    regs.default_map.set_sequencer(agent.sqr, adapter);
    regs.default_map.set_auto_predict(0);       // one prediction path: the monitor
    reg_pred.map     = regs.default_map;
    reg_pred.adapter = adapter;
    agent.mon.ap.connect(reg_pred.bus_in);
    reg_pred.reg_ap.connect(pred.analysis_export);
    pred.regs = regs;
    pred.exp_ap.connect(cmp.exp_in);
    pred.exp_ap.connect(cov.analysis_export);
    pred.act_ap.connect(cmp.act_in);
  endfunction
endclass

// ---------------- Stimulus ----------------
// Traffic: raw DATA writes on the agent's sequencer.
class chk_bytes_seq extends uvm_sequence #(apb_item);
  `uvm_object_utils(chk_bytes_seq)
  bit [7:0] bytes[$];
  function new(string name = "chk_bytes_seq"); super.new(name); endfunction
  task body();
    foreach (bytes[i]) begin
      apb_item t = apb_item::type_id::create("t");
      start_item(t);
      t.write = 1; t.addr = 'h04; t.data = bytes[i];
      finish_item(t);
    end
  endtask
endclass

// The virtual sequence: configuration through the register model, traffic through the agent,
// and interrupt service after every block of four bytes.
class chk_vseq extends uvm_sequence;
  `uvm_object_utils(chk_vseq)
  chk_regs                  regs;
  uvm_sequencer #(apb_item) sqr;
  bit                       modes[$];
  int                       blocks_per_mode = 2;
  bit                       skip_last_read;
  int                       serviced;
  function new(string name = "chk_vseq"); super.new(name); endfunction
  task body();
    uvm_status_e   st;
    uvm_reg_data_t v;
    foreach (modes[p]) begin
      regs.ctrl.write(st, modes[p], UVM_FRONTDOOR, null, this);
      if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL write failed")
      for (int blk = 0; blk < blocks_per_mode; blk++) begin
        chk_bytes_seq traffic = chk_bytes_seq::type_id::create("traffic");
        for (int k = 0; k < 4; k++) traffic.bytes.push_back(8'h10 * p + 4 * blk + k + 1);
        traffic.start(sqr, this);
        // service the interrupt: done must be set; read RESULT; clear done
        regs.irq.read(st, v, UVM_FRONTDOOR, null, this);
        if (st != UVM_IS_OK || v[0] != 1) `uvm_error("IRQ", "done not set after four bytes")
        if (!(skip_last_read && p == modes.size() - 1 && blk == blocks_per_mode - 1)) begin
          regs.result.read(st, v, UVM_FRONTDOOR, null, this);
          if (st != UVM_IS_OK) `uvm_error("RESULT", "RESULT read failed")
        end
        regs.irq.write(st, 1, UVM_FRONTDOOR, null, this);
        if (st != UVM_IS_OK) `uvm_error("IRQ", "clear failed")
        serviced++;
      end
    end
  endtask
endclass

// ---------------- The test: the integrated plan ----------------
class chk_test extends uvm_test;
  `uvm_component_utils(chk_test)
  chk_env  env;
  chk_vseq vseq;
  bit      modes[$] = '{0, 1, 1, 0};        // add, then xor, xor again, add: four transitions
  bit      skip_last_read;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env = chk_env::type_id::create("env", this);
  endfunction
  task run_phase(uvm_phase phase);
    uvm_status_e st;
    int          seen;
    phase.raise_objection(this);
    vseq = chk_vseq::type_id::create("vseq");
    vseq.regs = env.regs;
    vseq.sqr  = env.agent.sqr;
    vseq.modes = modes;
    vseq.skip_last_read = skip_last_read;
    vseq.start(null);
    env.regs.ctrl.mirror(st, UVM_CHECK);      // CTRL must read back what the model predicted
    if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL read failed")
    seen = env.agent.mon.n;                   // quiet window: nothing more may arrive
    #100;
    if (env.agent.mon.n != seen) `uvm_error("QUIET", "transfers after the plan ended")
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    uvm_report_server svr = uvm_report_server::get_server();
    int errors = svr.get_severity_count(UVM_ERROR);
    $display("BENCH REPORT at %0t ns, %0d APB transfers", $time, env.agent.mon.n);
    $display("REG  CTRL read-back mismatches: %0d", svr.get_id_count("RegModel"));
    $display("IRQ  blocks signalled and cleared: %0d", vseq.serviced);
    $display("SB   %0d blocks expected, %0d matched, %0d mismatched, %0d pending, %0d extra",
             env.cmp.n_exp, env.cmp.n_match, env.cmp.n_mismatch, env.cmp.pending.size(), env.cmp.n_extra);
    $display("COV  rst->add %0d, rst->xor %0d, add->add %0d, add->xor %0d, xor->xor %0d, xor->add %0d",
             env.cov.count("rst->add"), env.cov.count("rst->xor"), env.cov.count("add->add"),
             env.cov.count("add->xor"), env.cov.count("xor->xor"), env.cov.count("xor->add"));
    if (errors == 0) $display("PASS: no errors");
    else             $display("FAIL: %0d UVM_ERROR reports", errors);
  endfunction
endclass

Check yourself

ConceptThe report shows [CMP] block 3 (add->xor): RESULT 4a, expected 4 and REG CTRL read-back mismatches: 0. The data bytes were 11 12 13 14. Where is the bug most likely to be?
ConceptCoverage and comparator together: rst->add and xor->add blocks match, add->xor and xor->xor blocks mismatch. What does that pattern say about the design?
ConceptWith the bug still in the design, which plan would pass?
Predict the outputThe design is fixed. This test runs one xor phase of two blocks, and its plan skips the last RESULT read. 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 "chk_bench.svh"

// The checksum peripheral on APB.
//   0x00 CTRL    mode[0]  read-write, reset 0: 0 = add the bytes, 1 = xor them
//   0x04 DATA    [7:0]    write-only: feeds one byte to the engine
//   0x08 RESULT  [7:0]    read-only: checksum of the bytes since CTRL was last written
//   0x0C IRQ     done[0]  set after every fourth byte; write 1 to clear
// Writing CTRL clears RESULT, restarts the byte count and selects the mode for the next bytes.
module chk_csr (apb_if bus);
  logic       mode  = 0;     // CTRL.mode, as software reads it
  logic       op    = 0;     // the engine's copy of the mode, taken at the first byte
  logic       first = 1;     // 1 until the first byte after configuration
  logic [7:0] acc   = 0;
  logic [1:0] count = 0;
  logic       done  = 0;
  wire        wr    = bus.psel && bus.penable && bus.pwrite;
  wire        cur   = first ? mode : op;       // the mode this byte uses
  assign bus.pready = 1;
  assign bus.prdata = (bus.paddr == 32'h00) ? {31'b0, mode}
                    : (bus.paddr == 32'h08) ? {24'b0, acc}
                    : (bus.paddr == 32'h0C) ? {31'b0, done} : 32'b0;
  always @(posedge bus.clk)
    if (wr) case (bus.paddr)
      32'h00: begin
        mode  <= bus.pwdata[0];
        acc   <= 0;
        count <= 0;
        first <= 1;
      end
      32'h04: begin
        if (first) begin op <= mode; first <= 0; end
        acc   <= cur ? (acc ^ bus.pwdata[7:0]) : (acc + bus.pwdata[7:0]);
        count <= count + 1;
        if (count == 3) done <= 1;
      end
      32'h0C: if (bus.pwdata[0]) done <= 0;
      default: ;
    endcase
endmodule

// One mode, two blocks; the plan forgets the last RESULT read.
class short_test extends chk_test;
  `uvm_component_utils(short_test)
  function new(string name, uvm_component parent);
    super.new(name, parent);
    modes = '{1};
    skip_last_read = 1;
  endfunction
  function void report_phase(uvm_phase phase);
    $display("expected=%0d matched=%0d pending=%0d", env.cmp.n_exp, env.cmp.n_match, env.cmp.pending.size());
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  apb_if  bus (clk);
  chk_csr dut (bus);
  initial begin
    uvm_config_db #(virtual apb_if)::set(null, "uvm_test_top.env.agent", "vif", bus);
    run_test("short_test");
  end
endmodule
Read included file: chk_bench.svh
// chk_bench.svh: the UVM bench for the checksum peripheral. main.sv holds the design and tb.

// ---------------- APB agent (the bus from U13 and U14, packaged as an agent) ----------------
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;
  endfunction
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    apb_item t = apb_item::type_id::create("t");
    t.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
    t.addr  = rw.addr;
    t.data  = rw.data;
    return t;
  endfunction
  virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    apb_item t;
    if (!$cast(t, bus_item)) `uvm_fatal("NOTAPB", "bus2reg needs an apb_item")
    rw.kind   = t.write ? UVM_WRITE : UVM_READ;
    rw.addr   = t.addr;
    rw.data   = t.data;
    rw.status = UVM_IS_OK;          // this APB has no pslverr
  endfunction
endclass

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

// Publishes every completed transfer, whoever started it.
class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)
  virtual apb_if vif;
  uvm_analysis_port #(apb_item) ap;
  int n;                                         // transfers observed
  function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.psel && vif.penable && vif.pready) begin
        apb_item t = apb_item::type_id::create("t");
        t.write = vif.pwrite;
        t.addr  = vif.paddr;
        t.data  = vif.pwrite ? vif.pwdata : vif.prdata;
        n++;
        ap.write(t);
      end
    end
  endtask
endclass

class apb_agent extends uvm_agent;
  `uvm_component_utils(apb_agent)
  uvm_sequencer #(apb_item) sqr;
  apb_driver                drv;
  apb_monitor               mon;
  virtual apb_if            vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(virtual apb_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "no virtual interface")
    mon = apb_monitor::type_id::create("mon", this);
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(apb_item)::type_id::create("sqr", this);
      drv = apb_driver::type_id::create("drv", this);
    end
  endfunction
  function void connect_phase(uvm_phase phase);
    mon.vif = vif;
    if (get_is_active() == UVM_ACTIVE) begin
      drv.vif = vif;
      drv.seq_item_port.connect(sqr.seq_item_export);
    end
  endfunction
endclass

// ---------------- Register model ----------------
class chk_ctrl extends uvm_reg;
  `uvm_object_utils(chk_ctrl)
  rand uvm_reg_field mode;
  function new(string name = "chk_ctrl"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    mode = uvm_reg_field::type_id::create("mode");
    mode.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass
class chk_data extends uvm_reg;
  `uvm_object_utils(chk_data)
  uvm_reg_field din;
  function new(string name = "chk_data"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    din = uvm_reg_field::type_id::create("din");
    din.configure(this, 8, 0, "WO", 0, 8'h00, 1, 0, 0);
  endfunction
endclass
class chk_result extends uvm_reg;
  `uvm_object_utils(chk_result)
  uvm_reg_field acc;
  function new(string name = "chk_result"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    acc = uvm_reg_field::type_id::create("acc");
    acc.configure(this, 8, 0, "RO", 1, 8'h00, 1, 0, 0);      // volatile: the engine changes it
  endfunction
endclass
class chk_irq extends uvm_reg;
  `uvm_object_utils(chk_irq)
  uvm_reg_field done;
  function new(string name = "chk_irq"); 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 engine sets it
  endfunction
endclass
class chk_regs extends uvm_reg_block;
  `uvm_object_utils(chk_regs)
  rand chk_ctrl ctrl;
  chk_data      data;
  chk_result    result;
  chk_irq       irq;
  function new(string name = "chk_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    ctrl   = chk_ctrl::type_id::create("ctrl");     ctrl.configure(this);   ctrl.build();
    data   = chk_data::type_id::create("data");     data.configure(this);   data.build();
    result = chk_result::type_id::create("result"); result.configure(this); result.build();
    irq    = chk_irq::type_id::create("irq");       irq.configure(this);    irq.build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    default_map.add_reg(ctrl,   'h00, "RW");
    default_map.add_reg(data,   'h04, "WO");
    default_map.add_reg(result, 'h08, "RO");
    default_map.add_reg(irq,    'h0C, "RW");
    lock_model();
  endfunction
endclass

// ---------------- Reference model: untimed, from the specification only ----------------
class chk_model;
  bit       mode;           // 0 = add, 1 = xor
  bit       prev;           // the mode before the last CTRL write
  bit       from_reset;     // the last CTRL write was the first since reset
  bit       configured;
  bit [7:0] acc;
  int       nbytes;
  function void configure(bit m);
    from_reset = !configured;
    configured = 1;
    prev = mode; mode = m; acc = 0; nbytes = 0;
  endfunction
  // Returns 1 when this byte completes a block of four.
  function bit feed(bit [7:0] b);
    acc = mode ? (acc ^ b) : (acc + b);
    nbytes++;
    return (nbytes % 4 == 0);
  endfunction
  function string label();
    string from, to;
    if (from_reset) from = "rst"; else if (prev) from = "xor"; else from = "add";
    if (mode) to = "xor"; else to = "add";
    return {from, "->", to};
  endfunction
endclass

class chk_expect extends uvm_object;
  `uvm_object_utils(chk_expect)
  bit [7:0] value;
  int       block;
  string    label;
  function new(string name = "chk_expect"); super.new(name); endfunction
endclass

// ---------------- Scoreboard: predictor and comparator ----------------
// The predictor listens to the register predictor's reg_ap: one register operation per
// observed transfer, in bus order, published after the mirror was updated. CTRL writes
// configure the model with the mirrored mode; DATA writes feed it; every fourth byte yields
// one expectation. RESULT reads go to the comparator as the actual values.
class chk_predictor extends uvm_subscriber #(uvm_reg_item);
  `uvm_component_utils(chk_predictor)
  chk_regs                          regs;
  chk_model                         model = new;
  uvm_analysis_port #(chk_expect)   exp_ap;
  uvm_analysis_port #(uvm_reg_item) act_ap;
  int                               blocks;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_ap = new("exp_ap", this);
    act_ap = new("act_ap", this);
  endfunction
  function void write(uvm_reg_item t);
    uvm_object e    = t.get_element();
    string     name = e.get_name();
    if (t.get_kind() == UVM_WRITE && name == "ctrl")
      model.configure(regs.ctrl.mode.get_mirrored_value());
    else if (t.get_kind() == UVM_WRITE && name == "data") begin
      bit [7:0] b = t.get_value(0);
      if (model.feed(b)) begin
        chk_expect x = chk_expect::type_id::create("x");
        x.value = model.acc;
        x.block = ++blocks;
        x.label = model.label();
        exp_ap.write(x);
      end
    end
    else if (t.get_kind() == UVM_READ && name == "result")
      act_ap.write(t);
  endfunction
endclass

`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)
class chk_comparator extends uvm_scoreboard;
  `uvm_component_utils(chk_comparator)
  uvm_analysis_imp_exp #(chk_expect, chk_comparator)   exp_in;
  uvm_analysis_imp_act #(uvm_reg_item, chk_comparator) act_in;
  chk_expect pending[$];
  int n_exp, n_match, n_mismatch, n_extra;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_in = new("exp_in", this);
    act_in = new("act_in", this);
  endfunction
  function void write_exp(chk_expect x); pending.push_back(x); n_exp++; endfunction
  function void write_act(uvm_reg_item t);
    bit [7:0]  got = t.get_value(0);
    chk_expect x;
    if (pending.size() == 0) begin
      n_extra++;
      `uvm_error("EXTRA", $sformatf("RESULT read %0h with no block waiting", got))
      return;
    end
    x = pending.pop_front();
    if (got == x.value) n_match++;
    else begin
      n_mismatch++;
      `uvm_error("CMP", $sformatf("block %0d (%s): RESULT %0h, expected %0h", x.block, x.label, got, x.value))
    end
  endfunction
  // End scan: every expectation must have been read.
  function void check_phase(uvm_phase phase);
    foreach (pending[i])
      `uvm_error("PENDING", $sformatf("block %0d (%s): expected %0h was never read", pending[i].block, pending[i].label, pending[i].value))
  endfunction
endclass

// ---------------- Coverage: blocks per mode transition (rst = first configuration) ----------------
class chk_coverage extends uvm_subscriber #(chk_expect);
  `uvm_component_utils(chk_coverage)
  int hits[string];
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(chk_expect t);
    if (hits.exists(t.label)) hits[t.label]++; else hits[t.label] = 1;
  endfunction
  function int count(string bin);
    if (hits.exists(bin)) return hits[bin];
    return 0;
  endfunction
endclass

class chk_env extends uvm_env;
  `uvm_component_utils(chk_env)
  apb_agent                     agent;
  chk_regs                      regs;
  apb_adapter                   adapter;
  uvm_reg_predictor #(apb_item) reg_pred;
  chk_predictor                 pred;
  chk_comparator                cmp;
  chk_coverage                  cov;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    agent    = apb_agent::type_id::create("agent", this);
    regs     = chk_regs::type_id::create("regs");
    regs.build();
    adapter  = apb_adapter::type_id::create("adapter");
    reg_pred = uvm_reg_predictor #(apb_item)::type_id::create("reg_pred", this);
    pred     = chk_predictor::type_id::create("pred", this);
    cmp      = chk_comparator::type_id::create("cmp", this);
    cov      = chk_coverage::type_id::create("cov", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    regs.default_map.set_sequencer(agent.sqr, adapter);
    regs.default_map.set_auto_predict(0);       // one prediction path: the monitor
    reg_pred.map     = regs.default_map;
    reg_pred.adapter = adapter;
    agent.mon.ap.connect(reg_pred.bus_in);
    reg_pred.reg_ap.connect(pred.analysis_export);
    pred.regs = regs;
    pred.exp_ap.connect(cmp.exp_in);
    pred.exp_ap.connect(cov.analysis_export);
    pred.act_ap.connect(cmp.act_in);
  endfunction
endclass

// ---------------- Stimulus ----------------
// Traffic: raw DATA writes on the agent's sequencer.
class chk_bytes_seq extends uvm_sequence #(apb_item);
  `uvm_object_utils(chk_bytes_seq)
  bit [7:0] bytes[$];
  function new(string name = "chk_bytes_seq"); super.new(name); endfunction
  task body();
    foreach (bytes[i]) begin
      apb_item t = apb_item::type_id::create("t");
      start_item(t);
      t.write = 1; t.addr = 'h04; t.data = bytes[i];
      finish_item(t);
    end
  endtask
endclass

// The virtual sequence: configuration through the register model, traffic through the agent,
// and interrupt service after every block of four bytes.
class chk_vseq extends uvm_sequence;
  `uvm_object_utils(chk_vseq)
  chk_regs                  regs;
  uvm_sequencer #(apb_item) sqr;
  bit                       modes[$];
  int                       blocks_per_mode = 2;
  bit                       skip_last_read;
  int                       serviced;
  function new(string name = "chk_vseq"); super.new(name); endfunction
  task body();
    uvm_status_e   st;
    uvm_reg_data_t v;
    foreach (modes[p]) begin
      regs.ctrl.write(st, modes[p], UVM_FRONTDOOR, null, this);
      if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL write failed")
      for (int blk = 0; blk < blocks_per_mode; blk++) begin
        chk_bytes_seq traffic = chk_bytes_seq::type_id::create("traffic");
        for (int k = 0; k < 4; k++) traffic.bytes.push_back(8'h10 * p + 4 * blk + k + 1);
        traffic.start(sqr, this);
        // service the interrupt: done must be set; read RESULT; clear done
        regs.irq.read(st, v, UVM_FRONTDOOR, null, this);
        if (st != UVM_IS_OK || v[0] != 1) `uvm_error("IRQ", "done not set after four bytes")
        if (!(skip_last_read && p == modes.size() - 1 && blk == blocks_per_mode - 1)) begin
          regs.result.read(st, v, UVM_FRONTDOOR, null, this);
          if (st != UVM_IS_OK) `uvm_error("RESULT", "RESULT read failed")
        end
        regs.irq.write(st, 1, UVM_FRONTDOOR, null, this);
        if (st != UVM_IS_OK) `uvm_error("IRQ", "clear failed")
        serviced++;
      end
    end
  endtask
endclass

// ---------------- The test: the integrated plan ----------------
class chk_test extends uvm_test;
  `uvm_component_utils(chk_test)
  chk_env  env;
  chk_vseq vseq;
  bit      modes[$] = '{0, 1, 1, 0};        // add, then xor, xor again, add: four transitions
  bit      skip_last_read;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env = chk_env::type_id::create("env", this);
  endfunction
  task run_phase(uvm_phase phase);
    uvm_status_e st;
    int          seen;
    phase.raise_objection(this);
    vseq = chk_vseq::type_id::create("vseq");
    vseq.regs = env.regs;
    vseq.sqr  = env.agent.sqr;
    vseq.modes = modes;
    vseq.skip_last_read = skip_last_read;
    vseq.start(null);
    env.regs.ctrl.mirror(st, UVM_CHECK);      // CTRL must read back what the model predicted
    if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL read failed")
    seen = env.agent.mon.n;                   // quiet window: nothing more may arrive
    #100;
    if (env.agent.mon.n != seen) `uvm_error("QUIET", "transfers after the plan ended")
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    uvm_report_server svr = uvm_report_server::get_server();
    int errors = svr.get_severity_count(UVM_ERROR);
    $display("BENCH REPORT at %0t ns, %0d APB transfers", $time, env.agent.mon.n);
    $display("REG  CTRL read-back mismatches: %0d", svr.get_id_count("RegModel"));
    $display("IRQ  blocks signalled and cleared: %0d", vseq.serviced);
    $display("SB   %0d blocks expected, %0d matched, %0d mismatched, %0d pending, %0d extra",
             env.cmp.n_exp, env.cmp.n_match, env.cmp.n_mismatch, env.cmp.pending.size(), env.cmp.n_extra);
    $display("COV  rst->add %0d, rst->xor %0d, add->add %0d, add->xor %0d, xor->xor %0d, xor->add %0d",
             env.cov.count("rst->add"), env.cov.count("rst->xor"), env.cov.count("add->add"),
             env.cov.count("add->xor"), env.cov.count("xor->xor"), env.cov.count("xor->add"));
    if (errors == 0) $display("PASS: no errors");
    else             $display("FAIL: %0d UVM_ERROR reports", errors);
  endfunction
endclass
Spot the bugEach option is a candidate fix added to the CTRL branch of chk_csr. The full four-phase plan runs. Which candidate still fails?
Read included file: chk_bench.svh
// chk_bench.svh: the UVM bench for the checksum peripheral. main.sv holds the design and tb.

// ---------------- APB agent (the bus from U13 and U14, packaged as an agent) ----------------
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;
  endfunction
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    apb_item t = apb_item::type_id::create("t");
    t.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
    t.addr  = rw.addr;
    t.data  = rw.data;
    return t;
  endfunction
  virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    apb_item t;
    if (!$cast(t, bus_item)) `uvm_fatal("NOTAPB", "bus2reg needs an apb_item")
    rw.kind   = t.write ? UVM_WRITE : UVM_READ;
    rw.addr   = t.addr;
    rw.data   = t.data;
    rw.status = UVM_IS_OK;          // this APB has no pslverr
  endfunction
endclass

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

// Publishes every completed transfer, whoever started it.
class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)
  virtual apb_if vif;
  uvm_analysis_port #(apb_item) ap;
  int n;                                         // transfers observed
  function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.psel && vif.penable && vif.pready) begin
        apb_item t = apb_item::type_id::create("t");
        t.write = vif.pwrite;
        t.addr  = vif.paddr;
        t.data  = vif.pwrite ? vif.pwdata : vif.prdata;
        n++;
        ap.write(t);
      end
    end
  endtask
endclass

class apb_agent extends uvm_agent;
  `uvm_component_utils(apb_agent)
  uvm_sequencer #(apb_item) sqr;
  apb_driver                drv;
  apb_monitor               mon;
  virtual apb_if            vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(virtual apb_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "no virtual interface")
    mon = apb_monitor::type_id::create("mon", this);
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(apb_item)::type_id::create("sqr", this);
      drv = apb_driver::type_id::create("drv", this);
    end
  endfunction
  function void connect_phase(uvm_phase phase);
    mon.vif = vif;
    if (get_is_active() == UVM_ACTIVE) begin
      drv.vif = vif;
      drv.seq_item_port.connect(sqr.seq_item_export);
    end
  endfunction
endclass

// ---------------- Register model ----------------
class chk_ctrl extends uvm_reg;
  `uvm_object_utils(chk_ctrl)
  rand uvm_reg_field mode;
  function new(string name = "chk_ctrl"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    mode = uvm_reg_field::type_id::create("mode");
    mode.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
  endfunction
endclass
class chk_data extends uvm_reg;
  `uvm_object_utils(chk_data)
  uvm_reg_field din;
  function new(string name = "chk_data"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    din = uvm_reg_field::type_id::create("din");
    din.configure(this, 8, 0, "WO", 0, 8'h00, 1, 0, 0);
  endfunction
endclass
class chk_result extends uvm_reg;
  `uvm_object_utils(chk_result)
  uvm_reg_field acc;
  function new(string name = "chk_result"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    acc = uvm_reg_field::type_id::create("acc");
    acc.configure(this, 8, 0, "RO", 1, 8'h00, 1, 0, 0);      // volatile: the engine changes it
  endfunction
endclass
class chk_irq extends uvm_reg;
  `uvm_object_utils(chk_irq)
  uvm_reg_field done;
  function new(string name = "chk_irq"); 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 engine sets it
  endfunction
endclass
class chk_regs extends uvm_reg_block;
  `uvm_object_utils(chk_regs)
  rand chk_ctrl ctrl;
  chk_data      data;
  chk_result    result;
  chk_irq       irq;
  function new(string name = "chk_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
  virtual function void build();
    ctrl   = chk_ctrl::type_id::create("ctrl");     ctrl.configure(this);   ctrl.build();
    data   = chk_data::type_id::create("data");     data.configure(this);   data.build();
    result = chk_result::type_id::create("result"); result.configure(this); result.build();
    irq    = chk_irq::type_id::create("irq");       irq.configure(this);    irq.build();
    default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    default_map.add_reg(ctrl,   'h00, "RW");
    default_map.add_reg(data,   'h04, "WO");
    default_map.add_reg(result, 'h08, "RO");
    default_map.add_reg(irq,    'h0C, "RW");
    lock_model();
  endfunction
endclass

// ---------------- Reference model: untimed, from the specification only ----------------
class chk_model;
  bit       mode;           // 0 = add, 1 = xor
  bit       prev;           // the mode before the last CTRL write
  bit       from_reset;     // the last CTRL write was the first since reset
  bit       configured;
  bit [7:0] acc;
  int       nbytes;
  function void configure(bit m);
    from_reset = !configured;
    configured = 1;
    prev = mode; mode = m; acc = 0; nbytes = 0;
  endfunction
  // Returns 1 when this byte completes a block of four.
  function bit feed(bit [7:0] b);
    acc = mode ? (acc ^ b) : (acc + b);
    nbytes++;
    return (nbytes % 4 == 0);
  endfunction
  function string label();
    string from, to;
    if (from_reset) from = "rst"; else if (prev) from = "xor"; else from = "add";
    if (mode) to = "xor"; else to = "add";
    return {from, "->", to};
  endfunction
endclass

class chk_expect extends uvm_object;
  `uvm_object_utils(chk_expect)
  bit [7:0] value;
  int       block;
  string    label;
  function new(string name = "chk_expect"); super.new(name); endfunction
endclass

// ---------------- Scoreboard: predictor and comparator ----------------
// The predictor listens to the register predictor's reg_ap: one register operation per
// observed transfer, in bus order, published after the mirror was updated. CTRL writes
// configure the model with the mirrored mode; DATA writes feed it; every fourth byte yields
// one expectation. RESULT reads go to the comparator as the actual values.
class chk_predictor extends uvm_subscriber #(uvm_reg_item);
  `uvm_component_utils(chk_predictor)
  chk_regs                          regs;
  chk_model                         model = new;
  uvm_analysis_port #(chk_expect)   exp_ap;
  uvm_analysis_port #(uvm_reg_item) act_ap;
  int                               blocks;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_ap = new("exp_ap", this);
    act_ap = new("act_ap", this);
  endfunction
  function void write(uvm_reg_item t);
    uvm_object e    = t.get_element();
    string     name = e.get_name();
    if (t.get_kind() == UVM_WRITE && name == "ctrl")
      model.configure(regs.ctrl.mode.get_mirrored_value());
    else if (t.get_kind() == UVM_WRITE && name == "data") begin
      bit [7:0] b = t.get_value(0);
      if (model.feed(b)) begin
        chk_expect x = chk_expect::type_id::create("x");
        x.value = model.acc;
        x.block = ++blocks;
        x.label = model.label();
        exp_ap.write(x);
      end
    end
    else if (t.get_kind() == UVM_READ && name == "result")
      act_ap.write(t);
  endfunction
endclass

`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)
class chk_comparator extends uvm_scoreboard;
  `uvm_component_utils(chk_comparator)
  uvm_analysis_imp_exp #(chk_expect, chk_comparator)   exp_in;
  uvm_analysis_imp_act #(uvm_reg_item, chk_comparator) act_in;
  chk_expect pending[$];
  int n_exp, n_match, n_mismatch, n_extra;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_in = new("exp_in", this);
    act_in = new("act_in", this);
  endfunction
  function void write_exp(chk_expect x); pending.push_back(x); n_exp++; endfunction
  function void write_act(uvm_reg_item t);
    bit [7:0]  got = t.get_value(0);
    chk_expect x;
    if (pending.size() == 0) begin
      n_extra++;
      `uvm_error("EXTRA", $sformatf("RESULT read %0h with no block waiting", got))
      return;
    end
    x = pending.pop_front();
    if (got == x.value) n_match++;
    else begin
      n_mismatch++;
      `uvm_error("CMP", $sformatf("block %0d (%s): RESULT %0h, expected %0h", x.block, x.label, got, x.value))
    end
  endfunction
  // End scan: every expectation must have been read.
  function void check_phase(uvm_phase phase);
    foreach (pending[i])
      `uvm_error("PENDING", $sformatf("block %0d (%s): expected %0h was never read", pending[i].block, pending[i].label, pending[i].value))
  endfunction
endclass

// ---------------- Coverage: blocks per mode transition (rst = first configuration) ----------------
class chk_coverage extends uvm_subscriber #(chk_expect);
  `uvm_component_utils(chk_coverage)
  int hits[string];
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(chk_expect t);
    if (hits.exists(t.label)) hits[t.label]++; else hits[t.label] = 1;
  endfunction
  function int count(string bin);
    if (hits.exists(bin)) return hits[bin];
    return 0;
  endfunction
endclass

class chk_env extends uvm_env;
  `uvm_component_utils(chk_env)
  apb_agent                     agent;
  chk_regs                      regs;
  apb_adapter                   adapter;
  uvm_reg_predictor #(apb_item) reg_pred;
  chk_predictor                 pred;
  chk_comparator                cmp;
  chk_coverage                  cov;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    agent    = apb_agent::type_id::create("agent", this);
    regs     = chk_regs::type_id::create("regs");
    regs.build();
    adapter  = apb_adapter::type_id::create("adapter");
    reg_pred = uvm_reg_predictor #(apb_item)::type_id::create("reg_pred", this);
    pred     = chk_predictor::type_id::create("pred", this);
    cmp      = chk_comparator::type_id::create("cmp", this);
    cov      = chk_coverage::type_id::create("cov", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    regs.default_map.set_sequencer(agent.sqr, adapter);
    regs.default_map.set_auto_predict(0);       // one prediction path: the monitor
    reg_pred.map     = regs.default_map;
    reg_pred.adapter = adapter;
    agent.mon.ap.connect(reg_pred.bus_in);
    reg_pred.reg_ap.connect(pred.analysis_export);
    pred.regs = regs;
    pred.exp_ap.connect(cmp.exp_in);
    pred.exp_ap.connect(cov.analysis_export);
    pred.act_ap.connect(cmp.act_in);
  endfunction
endclass

// ---------------- Stimulus ----------------
// Traffic: raw DATA writes on the agent's sequencer.
class chk_bytes_seq extends uvm_sequence #(apb_item);
  `uvm_object_utils(chk_bytes_seq)
  bit [7:0] bytes[$];
  function new(string name = "chk_bytes_seq"); super.new(name); endfunction
  task body();
    foreach (bytes[i]) begin
      apb_item t = apb_item::type_id::create("t");
      start_item(t);
      t.write = 1; t.addr = 'h04; t.data = bytes[i];
      finish_item(t);
    end
  endtask
endclass

// The virtual sequence: configuration through the register model, traffic through the agent,
// and interrupt service after every block of four bytes.
class chk_vseq extends uvm_sequence;
  `uvm_object_utils(chk_vseq)
  chk_regs                  regs;
  uvm_sequencer #(apb_item) sqr;
  bit                       modes[$];
  int                       blocks_per_mode = 2;
  bit                       skip_last_read;
  int                       serviced;
  function new(string name = "chk_vseq"); super.new(name); endfunction
  task body();
    uvm_status_e   st;
    uvm_reg_data_t v;
    foreach (modes[p]) begin
      regs.ctrl.write(st, modes[p], UVM_FRONTDOOR, null, this);
      if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL write failed")
      for (int blk = 0; blk < blocks_per_mode; blk++) begin
        chk_bytes_seq traffic = chk_bytes_seq::type_id::create("traffic");
        for (int k = 0; k < 4; k++) traffic.bytes.push_back(8'h10 * p + 4 * blk + k + 1);
        traffic.start(sqr, this);
        // service the interrupt: done must be set; read RESULT; clear done
        regs.irq.read(st, v, UVM_FRONTDOOR, null, this);
        if (st != UVM_IS_OK || v[0] != 1) `uvm_error("IRQ", "done not set after four bytes")
        if (!(skip_last_read && p == modes.size() - 1 && blk == blocks_per_mode - 1)) begin
          regs.result.read(st, v, UVM_FRONTDOOR, null, this);
          if (st != UVM_IS_OK) `uvm_error("RESULT", "RESULT read failed")
        end
        regs.irq.write(st, 1, UVM_FRONTDOOR, null, this);
        if (st != UVM_IS_OK) `uvm_error("IRQ", "clear failed")
        serviced++;
      end
    end
  endtask
endclass

// ---------------- The test: the integrated plan ----------------
class chk_test extends uvm_test;
  `uvm_component_utils(chk_test)
  chk_env  env;
  chk_vseq vseq;
  bit      modes[$] = '{0, 1, 1, 0};        // add, then xor, xor again, add: four transitions
  bit      skip_last_read;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env = chk_env::type_id::create("env", this);
  endfunction
  task run_phase(uvm_phase phase);
    uvm_status_e st;
    int          seen;
    phase.raise_objection(this);
    vseq = chk_vseq::type_id::create("vseq");
    vseq.regs = env.regs;
    vseq.sqr  = env.agent.sqr;
    vseq.modes = modes;
    vseq.skip_last_read = skip_last_read;
    vseq.start(null);
    env.regs.ctrl.mirror(st, UVM_CHECK);      // CTRL must read back what the model predicted
    if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL read failed")
    seen = env.agent.mon.n;                   // quiet window: nothing more may arrive
    #100;
    if (env.agent.mon.n != seen) `uvm_error("QUIET", "transfers after the plan ended")
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    uvm_report_server svr = uvm_report_server::get_server();
    int errors = svr.get_severity_count(UVM_ERROR);
    $display("BENCH REPORT at %0t ns, %0d APB transfers", $time, env.agent.mon.n);
    $display("REG  CTRL read-back mismatches: %0d", svr.get_id_count("RegModel"));
    $display("IRQ  blocks signalled and cleared: %0d", vseq.serviced);
    $display("SB   %0d blocks expected, %0d matched, %0d mismatched, %0d pending, %0d extra",
             env.cmp.n_exp, env.cmp.n_match, env.cmp.n_mismatch, env.cmp.pending.size(), env.cmp.n_extra);
    $display("COV  rst->add %0d, rst->xor %0d, add->add %0d, add->xor %0d, xor->xor %0d, xor->add %0d",
             env.cov.count("rst->add"), env.cov.count("rst->xor"), env.cov.count("add->add"),
             env.cov.count("add->xor"), env.cov.count("xor->xor"), env.cov.count("xor->add"));
    if (errors == 0) $display("PASS: no errors");
    else             $display("FAIL: %0d UVM_ERROR reports", errors);
  endfunction
endclass

Remember

  • Write the test plan as requirement, check, coverage bin and plan step; a requirement about a change needs transition bins.
  • Build expectations from observed operations and the specification: here the register predictor's reg_ap, which arrives in bus order after the mirror is updated.
  • End with an end scan and a quiet window, so a result that is never read, or arrives late, fails the test.
  • Localise from the report first: which check fired, whether the register path reads back, and which coverage bins pass or fail.

Practice