Comparing is not equality

Decide what makes an observed transaction correct: which fields count, which bits are unspecified, which mode the design was in, and how the comparer reports a difference.

Lesson
U8.3
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

The scoreboard gains a time stamp on every transaction to improve its messages, and from that moment every comparison fails. Predictions are stamped when the read is taken, responses when they come back, and compare() dutifully reports that 35 differs from 55. Equality of every field was never the question. The question is whether the design did what the specification says.

Field of mem_rspDecides correctness?Why
tagYesIt is the key; a response with the wrong tag answers the wrong read
dataYes, through maskThe value the specification promises, where it promises one
maskNoIt is the rule, read from the prediction only; the observed object's mask is ignored
atNoBookkeeping for messages and for the latency meter in U8.4
reqNoA handle to the read, kept for the message

Two of five fields decide. The other three ride along and must be ignored by the comparison.

Correct is what the specification says

A comparison is a statement of the specification in code. Three kinds of leniency appear again and again. Some fields never count: time stamps, handles, counters, anything added for debugging. Some bits count only sometimes: a register's reserved bits, or memory that was never written. Some values count within a tolerance: a sample from an analog model, or a latency with a bound.

The place for all three is do_compare, the hook that compare() calls after checking the type. Written there, every caller gets the same verdict: the scoreboard, a stock comparator, a quiz program. Field macros with UVM_ALL_ON compare everything, which is exactly the hook's failure. Give bookkeeping fields UVM_NOCOMPARE, or skip the macros and write do_compare by hand as this lesson does.

The whole rule in two lines, used by every compare() in the bench.
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
  mem_rsp o;
  if (!$cast(o, rhs)) return 0;
  // Directed: the scoreboard calls expected.compare(actual), so this object's mask
  // is the rule. Time stamps, the mask itself and the request handle never take part.
  return comparer.compare_field_int("tag", tag, o.tag, 4) &
         comparer.compare_field_int("data", data & mask, o.data & mask, 8);
endfunction

Bits that are unspecified

This memory's power-up contents are unspecified. A read of a location that was never written returns whatever the silicon held, and the specification promises nothing about it. The model cannot predict it, and must not pretend to.

The prediction says so with a mask: mask = 8'h00 means no data bit is specified. do_compare compares data & mask on both sides, so any value matches, and the tag is still checked. The comparison is directed: the scoreboard always calls e.compare(r) with the prediction on the left, and only the left object's mask applies. A mask on the observed side is ignored, so nothing the monitor produces can weaken the check. In the example the read of address 5 returns 3a. The prediction holds ff, the model's empty location inverted because inv was set, and the pair still matches with the note "unspecified, so any value". The read still counts as checked, because the tag was.

Partial masks are common: the reserved bits of a status register, the low bits of a free-running counter, the lanes of a bus that a narrow transfer does not drive. The mask travels with the prediction because the predictor is the one that knows which bits the specification promises for this transaction.

A prediction that depends on the mode

The memory has a control register with one bit, inv. A read taken while inv is 1 returns the stored byte inverted. The right answer for the same read is 11 in one mode and ee in the other. So the predictor has to know the mode at the edge the read was taken.

It learns it the way the design did: from the bus. The scoreboard keeps a register mirror, a uvm_reg_block with one register and one field, and updates it from observed control writes with regs.ctrl.predict(t.data). When it predicts a read it asks regs.ctrl.inv.get_mirrored_value().

An observed control write becomes the expected byteArrows show values moving inside the scoreboard. The mode comes from the bus traffic, never from the design's state.
An observed control write becomes the expected byteArrows show values moving inside the scoreboard. The mode comes from the bus traffic, never from the design's state. request CTRL write, data1 seen by the monitorat 35 ns state Mirror inv = 1 regs.ctrl.predict(1) request READ tag 2, addr1 seen at 45 ns; modelholds 11 prediction Expected ee get_mirrored_value()is 1, so ~11 check compare: match tag and masked data response Response tag 2 =ee seen at 65 ns predictmode at the read edgestored byte 11er
  1. Remember the mode35 ns (illustrative)

    The monitor reports a write of 1 to CTRL. The scoreboard calls regs.ctrl.predict(1): the mirror now says inv is 1.

    More context

    The value comes from the observed bus write, the same evidence the design acted on. The scoreboard never reads the design's register.

    Follow the calls, values and waits
    1. data inv = 1: CTRL write, data 1 → Mirror inv = 1
  2. Predict with the mode at the read edge45 ns (illustrative)

    The tag 2 read of address 1 is taken. The model holds 11 there, and the mirrored inv is 1, so the prediction is ~11 = ee.

    More context

    The mode is read when the read is taken, not when its response arrives. A CTRL write between the two must not change this prediction.

    Follow the calls, values and waits
    1. data stored byte 11: READ tag 2, addr 1 → Expected ee
    2. data inv = 1: Mirror inv = 1 → Expected ee
    3. Waiting: Expected ee — waiting for the tag 2 response
  3. Compare65 ns (illustrative)

    The tag 2 response carries ee. It is compared with the prediction, ee, under mask ff: match.

    More context

    The log line is "65 ns rsp tag=2 data=ee matches prediction data=ee mask=ff". Without the mirror, the prediction would be 11 and this pair would mismatch.

    Follow the calls, values and waits
    1. data r: tag 2 = ee: Response tag 2 = ee → compare: match
    2. data e: expect ee: Expected ee → compare: match
    3. Released: Expected ee
Read the diagram as text
  • CTRL write, data 1: seen by the monitor at 35 ns
  • Mirror inv = 1: regs.ctrl.predict(1)
  • READ tag 2, addr 1: seen at 45 ns; model holds 11
  • Expected ee: get_mirrored_value() is 1, so ~11
  • compare: match: tag and masked data
  • Response tag 2 = ee: seen at 65 ns
  • CTRL write, data 1 → Mirror inv = 1: predict
  • Mirror inv = 1 → Expected ee: mode at the read edge
  • READ tag 2, addr 1 → Expected ee: stored byte 11
  • Expected ee → compare: match: e
  • Response tag 2 = ee → compare: match: r

Remembered mode plus stored byte gives the expected value. A design that latched the control write wrongly would return 11 and fail here.

The mode at the edge the read is taken
01234567edge (ns)1525354555657595request takenW a1=11R t1 a1CTRL 1R t2 a1R t3 a5CTRL 0R t4 a1-inv mirror afterprediction-11-eeany-11-response seen-t1=11-t2=eet3=3at4=11taken with inv=1taken with inv=0

Each prediction uses the mirror as it stood when the read was taken, which is what the design does too.

Why a mirror and not the design's own inv wire, which a hierarchical reference could read? Because the scoreboard checks the design. If the design latched the control write wrongly, a prediction that read its wire would be wrong in the same way, and the error would pass. The mirror is built from the same bus traffic the design received, and agrees with the design only when the design is right.

The quiz program keeps the mirror but never consults it: two reads taken with inv set mismatch. U13 builds the full register model. There, auto-prediction updates the mirror only for accesses made through the model, such as regs.ctrl.write(). This example's control writes come from a plain sequence on the bus, so the model never sees them, and they need explicit prediction. In U13 that is a uvm_reg_predictor connected to the monitor, which turns each observed bus item into a prediction and replaces the predict call here.

Values within a tolerance

Some outputs are correct within a bound rather than exactly: a filter's sample, a measured latency, a converter's code. The library gives no help here. compare_field_real compares two reals with != and nothing else, so a tolerance is yours to write. Compute the difference and compare it with a bound the prediction carries, in the output's units. On failure call comparer.print_msg() with both values and the bound, so that the miscompare is counted and listed like any other. The bound is part of the specification, so it lives in the item or the prediction, never in the comparator's code.

A tolerance written by hand and reported through the comparer.
// inside do_compare: a sample is right within tol codes of the prediction
if ((sample > o.sample ? sample - o.sample : o.sample - sample) > tol) begin
  comparer.print_msg($sformatf("sample: lhs = %0d : rhs = %0d, tolerance %0d", sample, o.sample, tol));
  ok = 0;
end

What the comparer tells you

compare() runs through a uvm_comparer. With no argument it uses the default one, which it clears before comparing, so each call overwrites the last result and anyone else using the default sees yours. Pass your own, p.compare(a, c), and afterwards c.get_result() is the number of differing fields and c.get_miscompares() the text, one line per field with the lhs and rhs values.

The comparer also prints [MISCMP] messages, at severity UVM_INFO and verbosity UVM_LOW, in two kinds. Each differing field gets a detail line, but only while the count is within show_max, which defaults to 1. Then, once the whole object has been compared, one summary line says how many miscompares there were and how many were shown. So three differing fields print one detail and one summary; get_miscompares() still lists all three, and set_show_max(0) prints every detail. Older code calls set_miscompare_string, which the 2020 library no longer has.

The example starts by comparing two hand-built responses that differ in tag and data. The log shows one detail line for the tag and a summary saying 2 miscompares, 1 shown; the program prints compare=0, result=2 and both fields from get_miscompares(). With equal tags and a mask that hides the low nibble, the same objects compare equal.

A comparator that transforms first

When the prediction is a pure function of the input transaction, the library offers uvm_algorithmic_comparator #(BEFORE, AFTER, TRANSFORMER). The transformer is a plain class with function AFTER transform(BEFORE b), and its transform is the prediction. The comparator calls it on each input and hands the result to an internal uvm_in_order_class_comparator #(AFTER).

Predictor and in-order comparator in one component.
class to_rsp;                       // TRANSFORMER: the prediction as a function
  function mem_rsp transform(mem_req b);
    mem_rsp e = mem_rsp::type_id::create("e");
    e.tag = b.tag; e.data = model_of(b);
    return e;
  endfunction
endclass
to_rsp tr = new();
alg = new("alg", this, tr);         // the constructor takes the transformer, so no factory create
// mon.req_ap -> alg.before_export, mon.rsp_ap -> alg.after_export

It inherits the stock comparator's limits: in-order only, mismatches as warnings, and the inner comparator is local, so the counts are reachable only through the report ids. For a design with a mode or a memory, the transformer would need state, and the separate predictor of U8.1 is the better shape.

Try it

The first two lines are the comparer on its own: two differing fields give result 2 and two lines of text; a mask that hides the differing bits gives a match. Then the bus runs. Tag 2 was taken with inv set and the prediction is ee; tag 3 reads a never-written location and matches with mask 0; tag 4 is back to 11 after the control write cleared inv.

Delete the get_mirrored_value line in the scoreboard and tag 2 mismatches. Add comparer.compare_field_int("at", at, o.at, 64) to do_compare and every pair mismatches.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: comparer: compare=0, result=2, miscompares: predicted.tag: lhs = 'h7 : rhs = 'h8 predicted.data: lhs = 'h11 : rhs = 'h15 comparer with mask f0 and equal tags: compare=1, result=0 45 ns rsp tag=1 data=11 matches prediction data=11 mask=ff 65 ns rsp tag=2 data=ee matches prediction data=ee mask=ff 75 ns rsp tag=3 data=3a matches prediction data=ff mask=00 (unspecified, so any value) 95 ns rsp tag=4 data=11 matches prediction data=11 mask=ff reads 4, checked 4, mismatches 0, orphans 0, waiting 0 UVM_ERROR : 0

Check yourself

Predict the outputThis scoreboard keeps the register mirror up to date but predicts every read as the plain stored byte. The sequence writes 11 to address 1 and 22 to address 2, reads address 1, sets inv, reads addresses 1 and 2, clears inv, and reads address 2. What line does report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

interface mem_if (input logic clk);
  logic       req_valid = 0;
  logic [1:0] req_kind  = 0;    // 0 read, 1 write, 2 control write
  logic [3:0] req_tag   = 0;
  logic       req_bank  = 0;
  logic [3:0] req_addr  = 0;
  logic [7:0] req_data  = 0;
  logic       rsp_valid = 0;
  logic [3:0] rsp_tag   = 0;
  logic [7:0] rsp_data  = 0;
endinterface

// A two-bank memory with tagged reads. Bank 0 is ready to answer one edge after a read is taken,
// bank 1 three edges after; the response is driven at that edge and a monitor sampling at rising
// edges sees it one edge later (two and four edges after the read, when nothing is in the way).
// One response per edge; bank 0 goes first when both are ready. Reads that share a tag
// answer in request order whatever their bank. A control write sets inv; a read taken while inv
// is 1 returns the stored byte inverted. The power-up contents are unspecified. LOSE_TAG is a
// fault switch for the bench: reads with that tag are taken and never answered (16 = off).
module tag_mem #(parameter int LOSE_TAG = 16) (mem_if bus);
  typedef struct { bit [3:0] tag; bit [7:0] data; int due; int seq; } pend_t;
  bit [7:0] mem0 [16], mem1 [16];
  bit       inv = 0;
  pend_t    q0[$], q1[$];
  int       cycle = 0, seqno = 0;
  initial foreach (mem0[i]) begin mem0[i] = 8'hC0 | i[3:0]; mem1[i] = 8'hD0 | i[3:0]; end
  // An older read with the same tag is still waiting in the other bank.
  function automatic bit held(pend_t head, pend_t other[$]);
    foreach (other[i]) if (other[i].tag == head.tag && other[i].seq < head.seq) return 1;
    return 0;
  endfunction
  always @(posedge bus.clk) begin
    pend_t p;
    cycle++;
    if (bus.req_valid) case (bus.req_kind)
      0: begin
        p.tag  = bus.req_tag;
        p.data = (bus.req_bank ? mem1[bus.req_addr] : mem0[bus.req_addr]) ^ {8{inv}};
        p.due  = cycle + (bus.req_bank ? 3 : 1);
        p.seq  = seqno++;
        if (bus.req_tag != LOSE_TAG[3:0] || LOSE_TAG > 15) begin
          if (bus.req_bank) q1.push_back(p); else q0.push_back(p);
        end
      end
      1: if (bus.req_bank) mem1[bus.req_addr] = bus.req_data; else mem0[bus.req_addr] = bus.req_data;
      2: inv = bus.req_data[0];
      default: ;
    endcase
    bus.rsp_valid <= 0;
    if (q0.size() != 0 && q0[0].due <= cycle && !held(q0[0], q1)) begin
      p = q0.pop_front();
      bus.rsp_valid <= 1; bus.rsp_tag <= p.tag; bus.rsp_data <= p.data;
    end else if (q1.size() != 0 && q1[0].due <= cycle && !held(q1[0], q0)) begin
      p = q1.pop_front();
      bus.rsp_valid <= 1; bus.rsp_tag <= p.tag; bus.rsp_data <= p.data;
    end
  end
endmodule

class mem_req extends uvm_sequence_item;
  `uvm_object_utils(mem_req)
  typedef enum bit [1:0] { READ, WRITE, CTRL } kind_e;
  kind_e    kind;
  bit [3:0] tag;
  bit       bank;
  bit [3:0] addr;
  bit [7:0] data;
  time      at;                // when the monitor saw the request taken
  function new(string name = "mem_req"); super.new(name); endfunction
  function string convert2string();
    if (kind == READ) return $sformatf("READ tag=%0d bank=%0d addr=%0d", tag, bank, addr);
    return $sformatf("%s tag=%0d bank=%0d addr=%0d data=%02h", kind.name(), tag, bank, addr, data);
  endfunction
endclass

class mem_rsp extends uvm_sequence_item;
  `uvm_object_utils(mem_rsp)
  bit [3:0] tag;
  bit [7:0] data;
  bit [7:0] mask = 8'hFF;      // 1 = this data bit is checked (a prediction's mask decides)
  bit       timed_out;         // a prediction the watchdog has already reported
  time      at;                // when the monitor saw it (a prediction: when the read was taken)
  mem_req   req;               // a prediction remembers the read it answers
  function new(string name = "mem_rsp"); super.new(name); endfunction
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    mem_rsp o;
    if (!$cast(o, rhs)) return 0;
    // Directed: the scoreboard calls expected.compare(actual), so this object's mask is the rule.
    // The tag and the specified data bits decide. Time stamps, the mask itself and the request
    // handle are bookkeeping and never take part.
    return comparer.compare_field_int("tag", tag, o.tag, 4) &
           comparer.compare_field_int("data", data & mask, o.data & mask, 8);
  endfunction
  function string convert2string(); return $sformatf("tag=%0d data=%02h", tag, data); endfunction
endclass

// A one-register mirror of the memory's control register (U13 builds full register models).
class ctrl_reg extends uvm_reg;
  `uvm_object_utils(ctrl_reg)
  uvm_reg_field inv;
  function new(string name = "ctrl_reg"); super.new(name, 8, UVM_NO_COVERAGE); endfunction
  function void build();
    inv = uvm_reg_field::type_id::create("inv");
    inv.configure(this, 1, 0, "RW", 0, 0, 1, 0, 0);
  endfunction
endclass

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

class script_seq extends uvm_sequence #(mem_req);
  `uvm_object_utils(script_seq)
  function new(string name = "script_seq"); super.new(name); endfunction
  task send(mem_req::kind_e kind, bit [3:0] tag, bit bank, bit [3:0] addr, bit [7:0] data = 0);
    mem_req it = mem_req::type_id::create("it");
    start_item(it);
    it.kind = kind; it.tag = tag; it.bank = bank; it.addr = addr; it.data = data;
    finish_item(it);
  endtask
  task body();
    send(mem_req::WRITE, 0, 0, 1, 8'h11);
    send(mem_req::WRITE, 0, 0, 2, 8'h22);
    send(mem_req::READ,  1, 0, 1);
    send(mem_req::CTRL,  0, 0, 0, 8'h01);   // inv = 1
    send(mem_req::READ,  2, 0, 1);
    send(mem_req::READ,  3, 0, 2);
    send(mem_req::CTRL,  0, 0, 0, 8'h00);   // inv = 0
    send(mem_req::READ,  4, 0, 2);
  endtask
endclass

// One request per cycle: offered at a falling edge, taken at the next rising edge.
class mem_driver extends uvm_driver #(mem_req);
  `uvm_component_utils(mem_driver)
  virtual mem_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(negedge vif.clk);
      seq_item_port.try_next_item(req);
      if (req == null) begin
        vif.req_valid <= 0;
        continue;
      end
      vif.req_valid <= 1;
      vif.req_kind  <= req.kind;
      vif.req_tag   <= req.tag;
      vif.req_bank  <= req.bank;
      vif.req_addr  <= req.addr;
      vif.req_data  <= req.data;
      @(posedge vif.clk);
      seq_item_port.item_done();
    end
  endtask
endclass

// Publishes every request the memory takes and every response it gives, stamped with the time.
class mem_monitor extends uvm_monitor;
  `uvm_component_utils(mem_monitor)
  virtual mem_if vif;
  uvm_analysis_port #(mem_req) req_ap;
  uvm_analysis_port #(mem_rsp) rsp_ap;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    req_ap = new("req_ap", this);
    rsp_ap = new("rsp_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.req_valid) begin
        mem_req t = mem_req::type_id::create("t");
        t.kind = mem_req::kind_e'(vif.req_kind);
        t.tag  = vif.req_tag;
        t.bank = vif.req_bank;
        t.addr = vif.req_addr;
        t.data = vif.req_data;
        t.at   = $time;
        req_ap.write(t);
      end
      if (vif.rsp_valid) begin
        mem_rsp r = mem_rsp::type_id::create("r");
        r.tag  = vif.rsp_tag;
        r.data = vif.rsp_data;
        r.at   = $time;
        rsp_ap.write(r);
      end
    end
  endtask
endclass

`uvm_analysis_imp_decl(_req)
`uvm_analysis_imp_decl(_rsp)

class tag_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(tag_scoreboard)
  uvm_analysis_imp_req #(mem_req, tag_scoreboard) req_imp;
  uvm_analysis_imp_rsp #(mem_rsp, tag_scoreboard) rsp_imp;
  bit [7:0] model0 [16], model1 [16];     // the reference model: what each location holds
  mem_rsp   pending[bit [3:0]][$];        // predictions waiting for a response, by tag, oldest first
  int       reads, checked, mismatches, orphans;
  uvm_event changed = new("changed");     // triggered on every request or response it receives
  mem_regs  regs;                         // the register mirror: what the memory was told
  bit       written0 [16], written1 [16]; // locations with a specified value
  function new(string name, uvm_component parent);
    super.new(name, parent);
    req_imp = new("req_imp", this);
    rsp_imp = new("rsp_imp", this);
  endfunction
  function void build_phase(uvm_phase phase);
    regs = mem_regs::type_id::create("regs");
    regs.build();
  endfunction
  function int waiting();
    int n = 0;
    foreach (pending[t]) n += pending[t].size();
    return n;
  endfunction
  function string waiting_tags();
    string s = "";
    foreach (pending[t]) repeat (pending[t].size()) s = {s, $sformatf(" %0d", t)};
    return s.len() ? s : " none";
  endfunction
  function void write_req(mem_req t);
    changed.trigger();                    // wakes a test that waits for the counts to change
    case (t.kind)
      mem_req::WRITE: begin
        if (t.bank) begin model1[t.addr] = t.data; written1[t.addr] = 1; end
        else        begin model0[t.addr] = t.data; written0[t.addr] = 1; end
      end
      mem_req::CTRL: begin
        void'(regs.ctrl.predict(t.data));   // the mirror follows the bus, never the DUT
      end
      mem_req::READ: begin
        mem_rsp e = mem_rsp::type_id::create("e");
        e.tag = t.tag;
        e.at  = t.at;
        e.req = t;
        e.data = t.bank ? model1[t.addr] : model0[t.addr];
        if (!(t.bank ? written1[t.addr] : written0[t.addr])) e.mask = 8'h00;
        pending[t.tag].push_back(e);
        reads++;
      end
    endcase
  endfunction
  function void write_rsp(mem_rsp r);
    mem_rsp e;
    changed.trigger();
    if (!pending.exists(r.tag) || pending[r.tag].size() == 0) begin
      orphans++;
      `uvm_error("SB", $sformatf("response %s at %0t has no read waiting", r.convert2string(), r.at))
      return;
    end
    e = pending[r.tag].pop_front();
    checked++;
    if (!e.compare(r)) begin
      mismatches++;
      `uvm_error("SB", $sformatf("%s, predicted data=%02h for %s taken at %0t",
                 r.convert2string(), e.data, e.req.convert2string(), e.at))
    end
  endfunction

  // Per tag: every waiting prediction the watchdog has not already reported.
  function void check_phase(uvm_phase phase);
    foreach (pending[t]) begin
      int unreported = 0, oldest = -1;
      foreach (pending[t][i]) if (!pending[t][i].timed_out) begin
        unreported++;
        if (oldest < 0) oldest = i;
      end
      if (unreported != 0)
        `uvm_error("SB", $sformatf("tag %0d: %0d read(s) never answered, first: %s at %0t",
                   t, unreported, pending[t][oldest].req.convert2string(), pending[t][oldest].at))
    end
  endfunction
endclass

class predict_test extends uvm_test;
  `uvm_component_utils(predict_test)
  uvm_sequencer #(mem_req) sqr;
  mem_driver     drv;
  mem_monitor    mon;
  tag_scoreboard sb;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual mem_if vif;
    sqr = uvm_sequencer #(mem_req)::type_id::create("sqr", this);
    drv = mem_driver::type_id::create("drv", this);
    mon = mem_monitor::type_id::create("mon", this);
    sb  = tag_scoreboard::type_id::create("sb", this);
    if (!uvm_config_db #(virtual mem_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface")
    drv.vif = vif;
    mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    mon.req_ap.connect(sb.req_imp);
    mon.rsp_ap.connect(sb.rsp_imp);
  endfunction
  task run_phase(uvm_phase phase);
    script_seq seq = script_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    begin
      bit balanced = 0;
      fork
        begin
          while (sb.waiting() != 0) sb.changed.wait_trigger();
          balanced = 1;
        end
        #100ns;
      join_any
      disable fork;
      if (balanced) #50ns;                   // balanced: a quiet window of this test's 50 ns limit
    end
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    $display("checked %0d, mismatches %0d", sb.checked, sb.mismatches);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  mem_if bus (clk);
  tag_mem #(.LOSE_TAG(16)) dut (bus);
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual mem_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("predict_test");
  end
endmodule
Spot the bugEach option is the body of mem_rsp::do_compare after the $cast. The bench is the example's: four reads, one of a never-written location. Which body fails?
ConceptThe prediction for a read of a never-written location carries mask = 0. What does that mean for the comparison?
ConceptWhy does the scoreboard read the mode from a register mirror instead of the design's inv signal through a hierarchical reference?
Conceptp.compare(a, c) returns 0 and three fields differ. The log shows one [MISCMP] detail line naming a field, then one summary line. Why only one detail?

Remember

  • A comparison is the specification in code: decide which fields count, which bits are specified, and within what tolerance.
  • Write the rule once in do_compare, so every compare() in the bench gives the same verdict; never compare time stamps or handles.
  • A mask carried by the prediction says which data bits the specification promises; mask 0 means any value matches while the tag is still checked.
  • A mode-dependent prediction reads the mode from a register mirror updated by the bus traffic, never from the design's own state.
  • Pass your own uvm_comparer to read get_result() and get_miscompares(); the log prints one detail line and a summary by default.