Callbacks: extend a driver from the test

Add error injection and a coverage hook to a driver from the test, decide where each hook runs and in what order, and catch a callback that was registered but never called.

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

Lane B of a chip shipped with a broken parity checker, although its error-injection test passed with no errors. The test had registered its parity-flipping callback on port_a.drv, whose lane checks parity correctly, so lane B never received a bad beat. Nothing in the log said the injector had never touched lane B.

Injector registered onBad beats lane B tookLane B scoreboardUVM errors
port_a.drv (the shipped test)03 matched0
port_b.drv (the example)12 matched, 1 mismatch1

Measured on the example bench, with lane B's design ignoring parity in both runs.

The hook points belong to the driver

U3.6 built the pattern: a component offers hook methods, and a facade call walks a list of registered callback objects and calls the method on each. This lesson is about the decisions around it: where the hooks sit, what they receive, and who is on the list.

One driver, a list of callbacks, two hooksThe example's port_b.drv. Solid arrows are calls; the list holds handles in calling order.
One driver, a list of callbacks, two hooksThe example's `port_b.drv`. Solid arrows are calls; the list holds handles in calling order. registers Test add(port_b.drv,perr); add(null, cov) facade port_b.drv pre_drive andpost_drive hooks queue Callback list perr, then cov callback perr pre_drive: flips paron item 2 callback cov post_drive: countsbeats taken interface lane_if pins valid, x, par additeratecall 1call 2drives the beat
  1. Register

    In connect_phase the test adds perr to this driver only and cov to every lane_driver. The driver's code does not change.

    More context

    The run prints the lists first: "port_a.drv calls: cov" and "port_b.drv calls: perr cov". Adding with null puts cov on every instance of the type.

    Follow the calls, values and waits
    1. data perr handle: Test → Callback list
    2. data cov handle: Test → Callback list
  2. pre_drive20 ns (illustrative)

    At 20 ns the driver has built the beat for item 2 (x = 32, par = 1). uvm_do_callbacks walks the list; perr flips par to 0. cov has no pre_drive, so the base method does nothing.

    More context

    The log line "20 ns port_b parity flipped on item 2 (x=32)" comes from this call. perr edits the driver's own beat object; nothing has reached the pins yet.

    Follow the calls, values and waits
    1. call pre_drive(item 2): port_b.drv → Callback list
    2. call pre_drive(item 2): Callback list → perr
    3. return beat x=32, par=0: perr → Callback list
  3. Drive20 ns (illustrative)

    The pre_drive walk returns, and the driver itself puts the edited beat on the pins and waits for the lane to take it.

    More context

    The driver copies the edited beat to valid, x and par at 20 ns, then waits clock by clock until ready is high. Lane B ignores parity, so it takes the bad beat.

    Follow the calls, values and waits
    1. data beat x=32, par=0: port_b.drv → lane_if pins
    2. Waiting: port_b.drv — waiting for an edge with ready
  4. post_drive45 ns (illustrative)

    The lane takes the beat at 45 ns. After that edge with ready, the same walk calls cov.post_drive, which records one bad-parity beat for port_b.

    More context

    The final count reads "port_b 2 good 1 bad". cov counts what the driver believes it sent; the 65 ns mismatch on y=33 is the scoreboard's view of the same beat.

    Follow the calls, values and waits
    1. call post_drive(item 2): port_b.drv → Callback list
    2. call post_drive(item 2): Callback list → cov
    3. Released: port_b.drv
Read the diagram as text
  • Test: add(port_b.drv, perr); add(null, cov)
  • port_b.drv: pre_drive and post_drive hooks
  • Callback list: perr, then cov
  • perr: pre_drive: flips par on item 2
  • cov: post_drive: counts beats taken
  • lane_if pins: valid, x, par
  • Test → Callback list: add
  • port_b.drv → Callback list: iterate
  • Callback list → perr: call 1
  • Callback list → cov: call 2
  • port_b.drv → lane_if pins: drives the beat

The driver owns the hook points and their arguments; tests own the list. Calls run in list order.

The VIP author writes three things. A callback class, lane_driver_cb extends uvm_callback, declares the hooks as empty virtual methods. ` uvm_register_cb(lane_driver, lane_driver_cb) ` in the driver records that this driver type accepts that callback type. Each uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat)) `` line is one hook point.

The test writes the rest: a subclass that overrides a hook, an object of it, and uvm_callbacks #(lane_driver, lane_driver_cb)::add(...). The driver's source never changes.

Placing the hooks and choosing their arguments

The two hook points in the lane driver.
beat.x   = req.x;
beat.par = ^req.x;                       // the driver's own translation
`uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat))
vif.valid <= 1; vif.x <= beat.x; vif.par <= beat.par;
do @(posedge vif.clk); while (!vif.ready);
`uvm_do_callbacks(lane_driver, lane_driver_cb, post_drive(this, req, beat))
seq_item_port.item_done();

Each placement answers a question for the callback writer. pre_drive runs after the driver has translated the item into pin values and before any pin moves, so an edit there reaches the bus. post_drive runs after the edge where the lane took the beat, so it reports what really transferred.

The arguments decide what a callback may change. The hooks receive the beat, the pin values for this transfer, as an object they may edit. They also receive the sequence's item, for reading only. An injector that edited the item would change what the sequence and any intent coverage believe was asked for.

Every hook is a promise to users. Once tests override pre_drive(drv, it, beat), changing its arguments breaks all of them. Offer few hooks, at points where the driver's state is complete and easy to describe.

An injected error the scoreboard must catch

parity_error_cb flips beat.par for item 2 on lane B. The scoreboard predicts from the monitor's view of the pins, so it needs no knowledge of the callback. A beat with bad parity predicts a parity error. The lane's specification leaves y unspecified when err = 1, so the scoreboard compares err alone there, as U8.3 masks fields that carry no meaning.

Lane B in the example (design ignores parity)
012345678edge (ns)152535455565758595beat taken16-32-48-par on pins1-0-0-predicted-17-error-49lane B showed-17-33-49bad parity takenmismatch

32 needs par = 1; the injector drove 0. A correct lane shows a parity error at 65 ns; lane B shows y = 33.

Without the injector, every beat has good parity and both lanes pass whether or not they check it. The error path is only exercised when something puts a bad beat on the pins. The check that catches the bug is the ordinary scoreboard.

The coverage hook cov runs at post_drive on every lane driver. Its report, port_a 3 good 0 bad, port_b 2 good 1 bad, shows a hole as well: lane A's parity checker was never exercised in this run. It counts what the driver believes it sent; a count that must prove what reached the design comes from the monitor.

One instance or every instance, and in what order

add(env.port_b.drv, perr) stores perr for that one object. add(null, cov) is the typewide form: cov applies to every lane_driver, including ones created later. A null handle always means typewide, so registering through a variable that is still empty silently injects on every lane.

Callbacks run in the order they were added. UVM_PREPEND as a third argument puts one at the front. Order matters when one callback reads what another writes.

Coverage in pre_drive

add(drv, sample);   // pre_drive: reads beat.par
add(drv, perr);     // pre_drive: flips beat.par

sample runs first and records good parity for the beat that went out bad.

Coverage in post_drive

add(drv, perr);     // pre_drive: flips beat.par
add(drv, cov);      // post_drive: reads beat.par

Every pre_drive edit has happened before any post_drive runs, so the order of the two adds no longer matters.

So modify in pre_drive and observe in post_drive. When two editors must both run, register them in the order you want and print the order before the run. The example prints each driver's list in end_of_elaboration_phase with a uvm_callback_iter: port_b.drv calls: perr cov.

Switching a callback off

perr.callback_mode(0) keeps perr on every list but makes each walk skip it; callback_mode(1) turns it back on. uvm_callbacks #(lane_driver, lane_driver_cb)::delete(drv, perr) removes it from that driver's list.

Use the mode for a window, such as no errors during reset. Use delete when the callback's job is done. Deleting a callback that is not on the list gives a CBUNREG warning, and the object's counters survive either way.

The callback that never runs

Three registrations that inject nothing on lane BEach injector is registered, and none of them reaches port_b.drv's pre_drive walk.
Three registrations that inject nothing on lane BEach injector is registered, and none of them reaches `port_b.drv`'s pre_drive walk. registration add(port_a.drv,perr) Right type, wronginstance: no message registration add(port_b.mon,perr_b) The monitor neverwalks a list: oneCBUNREG warning registration functionpre_drv(...) A new method, not anoverride: no message facade port_a.drv Calls perr; lane Arejects the beat component port_b.mon Holds perr_b; nevercalls it facade port_b.drv Calls only the emptybase pre_drive stored onstored onnever called by
  1. Wrong instance

    add(port_a.drv, perr) stores the injector on lane A's driver. It runs there, flips parity, and lane A rejects the beat, as its scoreboard expects.

    More context

    The type pair is registered, so the library has nothing to warn about. With perr_b also on the monitor, the predict program prints "perr hits 1, perr_b hits 0".

    Follow the calls, values and waits
    1. data perr handle: add(port_a.drv, perr) → port_a.drv
  2. Wrong type

    add(port_b.mon, perr_b) names lane B, but the monitor type never declared the callback pair. The library warns once and stores the callback anyway.

    More context

    The measured warning is "[CBUNREG] Callback perr_b cannot be registered with object uvm_test_top.env.port_b.mon". It counts as a warning, not an error, and the monitor has no hook point that would call it.

    Follow the calls, values and waits
    1. data perr_b handle: add(port_b.mon, perr_b) → port_b.mon
  3. Misspelled hook

    An injector that defines pre_drv instead of pre_drive is stored on port_b.drv, the right instance. Its walk calls pre_drive, which this class does not override.

    More context

    A misspelled name compiles as a new method of the subclass. The walk runs the empty base pre_drive, with no message.

    Follow the calls, values and waits
    1. data injector with pre_drv: function pre_drv(...) → port_b.drv
  4. Lane B takes good beats

    In all three cases port_b.drv sends three beats with correct parity. Lane B's scoreboard matches them all, and its parity bug goes unseen.

Read the diagram as text
  • add(port_a.drv, perr): Right type, wrong instance: no message
  • add(port_b.mon, perr_b): The monitor never walks a list: one CBUNREG warning
  • function pre_drv(...): A new method, not an override: no message
  • port_a.drv: Calls perr; lane A rejects the beat
  • port_b.mon: Holds perr_b; never calls it
  • port_b.drv: Calls only the empty base pre_drive
  • add(port_a.drv, perr) → port_a.drv: stored on
  • add(port_b.mon, perr_b) → port_b.mon: stored on
  • function pre_drv(...) → port_b.drv: never called by

Lane B's parity bug survives all three. Checking that lane B's monitor saw a bad beat turns each one into an error.

A callback on the wrong instance produces no message at all. Registering on a type that never declared the pair, here the monitor, gives one CBUNREG warning and still stores the callback, which nobody calls. A misspelled hook name compiles as a new method and is never called.

The cure is a check on the target, made in check_phase from what the monitor saw on the pins. A hit counter inside the injector catches the never-called cases, but on the wrong instance it still counts hits on the other lane. A count kept by another callback is no better: a hook sees the beat the driver holds, not the pins. The example asks lane B's scoreboard how many bad-parity beats its monitor saw and reports CBIDLE at zero. Printing each list at the end of elaboration shows the wrong-instance mistake before time 0.

Callback, factory override or config knob

Config knobFactory overrideCallback
A test can changeA value the VIP already readsAny virtual method of a class the VIP creates through the factoryOnly what the VIP passes to its hooks
ScopeOne instance, through its config objectA type, or instances by pathOne instance, or every instance of a type
Takes effectWhen the VIP reads it, often once in buildOnly for components created after itAny time: add, delete or switch off mid-run
Several at onceYes, one knob eachOne class per instance; combining two means one class with bothYes, in list order
VIP author maintainsA field and its defaultVirtual methods and factory creationHook calls and their arguments, unchanged for users
Silent failureSet at the wrong path, or read too earlyA path that matches nothing (U11.3)The wrong instance or type

Three extension points seen from the VIP author's side.

Choose a knob when the variation is a value the VIP author can foresee, such as an idle value or a delay range. Choose an override to replace behaviour wholesale, such as a driver with different timing. Choose a callback when test-specific additions must stack, switch on and off during the run, or touch one point inside a long method.

Callbacks have a run-time price. Each hook point creates an iterator object every time it runs, even when the list is empty. Keep hooks out of per-cycle loops.

Try it

The lists come first, then one line per comparison. Lane B shows y = 33 for the beat with bad parity, and its scoreboard reports the mismatch.

Change #(.CHECK_PARITY(0)) on dut_b to 1 and rerun: lane B now shows a parity error at 65 ns, and the run is clean. Then register perr on env.port_a.drv: both scoreboards pass, as in the hook, and only CBIDLE fails the run because lane B's monitor saw no bad beat.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: port_a.drv calls: cov port_b.drv calls: perr cov 20 ns port_b parity flipped on item 2 (x=32) 35 ns port_a y=17 matches 35 ns port_b y=17 matches 65 ns port_a y=33 matches 65 ns port_b y=33, predicted parity error 95 ns port_a y=49 matches 95 ns port_b y=49 matches beats taken: port_a 3 good 0 bad, port_b 2 good 1 bad UVM_ERROR : 1

Check yourself

Predict the outputThis test uses the example's bench, with lane B still ignoring parity. It registers perr on env.port_a.drv, and perr_b with uvm_callbacks #(lane_monitor, lane_driver_cb)::add(env.port_b.mon, perr_b). It does not check that lane B took a bad beat. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// One lane of the chip: the U8 incrementer with a parity bit on its input. The lane takes x at a
// rising edge where valid and ready are 1 and is busy for the next two edges. One edge later it
// shows the result for one cycle: y = x + 1 with err = 0, or err = 1 when par was not ^x.
// With err = 1 the value on y is unspecified (this RTL happens to drive 0).
interface lane_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       par = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
  logic       err = 0;
endinterface

// CHECK_PARITY = 0 is a fault switch: that lane ignores the parity bit.
module lane #(parameter bit CHECK_PARITY = 1) (lane_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic       bad = 0;
  logic [7:0] stage = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged;
    bus.err       <= bad;
    bus.y         <= bad ? 8'd0 : stage;
    staged        <= bus.valid && bus.ready;
    bad           <= CHECK_PARITY && (bus.par != ^bus.x);
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= 2;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// What the sequence asks for.
class lane_item extends uvm_sequence_item;
  `uvm_object_utils(lane_item)
  bit [7:0] x;
  int       n;                             // 1, 2, 3: position in the sequence
  function new(string name = "lane_item"); super.new(name); endfunction
endclass

// What the driver is about to put on the pins. Callbacks may edit this, never the item.
class lane_beat;
  bit [7:0] x;
  bit       par;
endclass

// What the monitor saw going in and coming out.
class lane_in extends uvm_sequence_item;
  `uvm_object_utils(lane_in)
  bit [7:0] x;
  bit       par;
  function new(string name = "lane_in"); super.new(name); endfunction
endclass

class lane_out extends uvm_sequence_item;
  `uvm_object_utils(lane_out)
  bit [7:0] y;
  bit       err;
  function new(string name = "lane_out"); super.new(name); endfunction
  function string show();
    string s;
    if (err) s = "parity error";
    else     s = $sformatf("y=%0d", y);
    return s;
  endfunction
endclass

typedef class lane_driver;

// The hooks the driver offers. Each does nothing until a registered callback overrides it.
class lane_driver_cb extends uvm_callback;
  `uvm_object_utils(lane_driver_cb)
  function new(string name = "lane_driver_cb"); super.new(name); endfunction
  // After the driver has built the beat, before it touches the pins.
  virtual function void pre_drive(lane_driver drv, lane_item it, lane_beat beat); endfunction
  // After the lane has taken the beat (an edge with ready), before item_done.
  virtual function void post_drive(lane_driver drv, lane_item it, lane_beat beat); endfunction
endclass

class lane_driver extends uvm_driver #(lane_item);
  `uvm_component_utils(lane_driver)
  `uvm_register_cb(lane_driver, lane_driver_cb)       // this driver type offers lane_driver_cb hooks
  virtual lane_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    lane_beat beat;
    forever begin
      @(negedge vif.clk);
      seq_item_port.try_next_item(req);
      if (req == null) begin
        vif.valid <= 0;
        continue;
      end
      beat = new;
      beat.x   = req.x;
      beat.par = ^req.x;                                     // correct parity
      `uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat))
      vif.valid <= 1;
      vif.x     <= beat.x;
      vif.par   <= beat.par;
      do @(posedge vif.clk); while (!vif.ready);             // the lane takes the beat here
      `uvm_do_callbacks(lane_driver, lane_driver_cb, post_drive(this, req, beat))
      seq_item_port.item_done();
    end
  endtask
endclass

class lane_monitor extends uvm_monitor;
  `uvm_component_utils(lane_monitor)
  virtual lane_if vif;
  uvm_analysis_port #(lane_in)  in_ap;
  uvm_analysis_port #(lane_out) out_ap;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_ap  = new("in_ap", this);
    out_ap = new("out_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.valid && vif.ready) begin
        lane_in t = lane_in::type_id::create("t");
        t.x   = vif.x;
        t.par = vif.par;
        in_ap.write(t);
      end
      if (vif.out_valid) begin
        lane_out r = lane_out::type_id::create("r");
        r.y   = vif.y;
        r.err = vif.err;
        out_ap.write(r);
      end
    end
  endtask
endclass

`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)

// In-order scoreboard. It predicts from what the monitor saw on the pins, so it needs no
// knowledge of any callback: a beat with bad parity predicts a parity error.
class lane_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(lane_scoreboard)
  uvm_analysis_imp_in  #(lane_in,  lane_scoreboard) in_imp;
  uvm_analysis_imp_out #(lane_out, lane_scoreboard) out_imp;
  string   lane;
  lane_out expected[$];
  int      matched, mismatched, errors_seen;
  int      bad_in;                         // beats the monitor saw with bad parity
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_imp  = new("in_imp", this);
    out_imp = new("out_imp", this);
  endfunction
  function void write_in(lane_in t);
    lane_out e = lane_out::type_id::create("e");
    e.err = (t.par != ^t.x);
    if (e.err) bad_in++;
    if (!e.err) e.y = t.x + 1;
    expected.push_back(e);
  endfunction
  function void write_out(lane_out r);
    lane_out e;
    if (expected.size() == 0) begin
      `uvm_error("SB", $sformatf("%s: %s with nothing predicted", lane, r.show()))
      return;
    end
    e = expected.pop_front();
    if (r.err) errors_seen++;
    if (r.err == e.err && (r.err || r.y == e.y)) begin   // y means nothing when err = 1
      matched++;
    end
    else begin
      mismatched++;
      `uvm_error("SB", $sformatf("%s: %s, predicted %s", lane, r.show(), e.show()))
    end
  endfunction
  function void check_phase(uvm_phase phase);
    if (expected.size() != 0)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", lane, expected.size()))
  endfunction
endclass

class lane_agent extends uvm_agent;
  `uvm_component_utils(lane_agent)
  uvm_sequencer #(lane_item) sqr;
  lane_driver  drv;
  lane_monitor mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual lane_if vif;
    super.build_phase(phase);
    if (!uvm_config_db #(virtual lane_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface for this agent")
    sqr = uvm_sequencer #(lane_item)::type_id::create("sqr", this);
    drv = lane_driver::type_id::create("drv", this);
    mon = lane_monitor::type_id::create("mon", this);
    drv.vif = vif;
    mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

// Two lanes, each with its own agent and scoreboard.
class chip_env extends uvm_env;
  `uvm_component_utils(chip_env)
  lane_agent      port_a, port_b;
  lane_scoreboard sb_a, sb_b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    port_a = lane_agent::type_id::create("port_a", this);
    port_b = lane_agent::type_id::create("port_b", this);
    sb_a   = lane_scoreboard::type_id::create("sb_a", this);
    sb_b   = lane_scoreboard::type_id::create("sb_b", this);
    sb_a.lane = "port_a";
    sb_b.lane = "port_b";
  endfunction
  function void connect_phase(uvm_phase phase);
    port_a.mon.in_ap.connect(sb_a.in_imp);
    port_a.mon.out_ap.connect(sb_a.out_imp);
    port_b.mon.in_ap.connect(sb_b.in_imp);
    port_b.mon.out_ap.connect(sb_b.out_imp);
  endfunction
endclass

// Sends 16, 32 and 48, numbered 1, 2 and 3.
class lane_seq extends uvm_sequence #(lane_item);
  `uvm_object_utils(lane_seq)
  function new(string name = "lane_seq"); super.new(name); endfunction
  task body();
    bit [7:0] values[3] = '{16, 32, 48};
    foreach (values[i]) begin
      lane_item it = lane_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      it.n = i + 1;
      finish_item(it);
    end
  endtask
endclass

// Error injection, written in the test: flip the parity bit of item 2. It edits only the beat.
class parity_error_cb extends lane_driver_cb;
  `uvm_object_utils(parity_error_cb)
  int target = 2;
  int hits;
  function new(string name = "parity_error_cb"); super.new(name); endfunction
  function void pre_drive(lane_driver drv, lane_item it, lane_beat beat);
    if (it.n != target) return;
    beat.par = !beat.par;
    hits++;
  endfunction
endclass

class silent_test extends uvm_test;
  `uvm_component_utils(silent_test)
  chip_env        env;
  parity_error_cb perr, perr_b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env    = chip_env::type_id::create("env", this);
    perr   = parity_error_cb::type_id::create("perr");
    perr_b = parity_error_cb::type_id::create("perr_b");
  endfunction
  function void connect_phase(uvm_phase phase);
    uvm_callbacks #(lane_driver, lane_driver_cb)::add(env.port_a.drv, perr);
    uvm_callbacks #(lane_monitor, lane_driver_cb)::add(env.port_b.mon, perr_b);
  endfunction
  task run_phase(uvm_phase phase);
    lane_seq seq_a = lane_seq::type_id::create("seq_a");
    lane_seq seq_b = lane_seq::type_id::create("seq_b");
    phase.raise_objection(this);
    fork
      seq_a.start(env.port_a.sqr);
      seq_b.start(env.port_b.sqr);
    join
    #25;
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    $display("perr hits %0d, perr_b hits %0d; port_b %0d matched, %0d mismatched",
             perr.hits, perr_b.hits, env.sb_b.matched, env.sb_b.mismatched);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  lane_if bus_a (clk);
  lane_if bus_b (clk);
  lane dut_a (bus_a);
  lane #(.CHECK_PARITY(0)) dut_b (bus_b);      // fault switch: lane B ignores parity
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual lane_if)::set(null, "uvm_test_top.env.port_a", "vif", bus_a);
    uvm_config_db #(virtual lane_if)::set(null, "uvm_test_top.env.port_b", "vif", bus_b);
    run_test("silent_test");
  end
endmodule
Predict the outputOn port_a, the test adds perr, then probe1, then probe2 with UVM_PREPEND. Both probes count beats and bad parity in pre_drive. After the first sequence it calls perr.callback_mode(0) and deletes probe1, then runs the same sequence again. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// One lane of the chip: the U8 incrementer with a parity bit on its input. The lane takes x at a
// rising edge where valid and ready are 1 and is busy for the next two edges. One edge later it
// shows the result for one cycle: y = x + 1 with err = 0, or err = 1 when par was not ^x.
// With err = 1 the value on y is unspecified (this RTL happens to drive 0).
interface lane_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       par = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
  logic       err = 0;
endinterface

// CHECK_PARITY = 0 is a fault switch: that lane ignores the parity bit.
module lane #(parameter bit CHECK_PARITY = 1) (lane_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic       bad = 0;
  logic [7:0] stage = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged;
    bus.err       <= bad;
    bus.y         <= bad ? 8'd0 : stage;
    staged        <= bus.valid && bus.ready;
    bad           <= CHECK_PARITY && (bus.par != ^bus.x);
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= 2;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// What the sequence asks for.
class lane_item extends uvm_sequence_item;
  `uvm_object_utils(lane_item)
  bit [7:0] x;
  int       n;                             // 1, 2, 3: position in the sequence
  function new(string name = "lane_item"); super.new(name); endfunction
endclass

// What the driver is about to put on the pins. Callbacks may edit this, never the item.
class lane_beat;
  bit [7:0] x;
  bit       par;
endclass

// What the monitor saw going in and coming out.
class lane_in extends uvm_sequence_item;
  `uvm_object_utils(lane_in)
  bit [7:0] x;
  bit       par;
  function new(string name = "lane_in"); super.new(name); endfunction
endclass

class lane_out extends uvm_sequence_item;
  `uvm_object_utils(lane_out)
  bit [7:0] y;
  bit       err;
  function new(string name = "lane_out"); super.new(name); endfunction
  function string show();
    string s;
    if (err) s = "parity error";
    else     s = $sformatf("y=%0d", y);
    return s;
  endfunction
endclass

typedef class lane_driver;

// The hooks the driver offers. Each does nothing until a registered callback overrides it.
class lane_driver_cb extends uvm_callback;
  `uvm_object_utils(lane_driver_cb)
  function new(string name = "lane_driver_cb"); super.new(name); endfunction
  // After the driver has built the beat, before it touches the pins.
  virtual function void pre_drive(lane_driver drv, lane_item it, lane_beat beat); endfunction
  // After the lane has taken the beat (an edge with ready), before item_done.
  virtual function void post_drive(lane_driver drv, lane_item it, lane_beat beat); endfunction
endclass

class lane_driver extends uvm_driver #(lane_item);
  `uvm_component_utils(lane_driver)
  `uvm_register_cb(lane_driver, lane_driver_cb)       // this driver type offers lane_driver_cb hooks
  virtual lane_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    lane_beat beat;
    forever begin
      @(negedge vif.clk);
      seq_item_port.try_next_item(req);
      if (req == null) begin
        vif.valid <= 0;
        continue;
      end
      beat = new;
      beat.x   = req.x;
      beat.par = ^req.x;                                     // correct parity
      `uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat))
      vif.valid <= 1;
      vif.x     <= beat.x;
      vif.par   <= beat.par;
      do @(posedge vif.clk); while (!vif.ready);             // the lane takes the beat here
      `uvm_do_callbacks(lane_driver, lane_driver_cb, post_drive(this, req, beat))
      seq_item_port.item_done();
    end
  endtask
endclass

class lane_monitor extends uvm_monitor;
  `uvm_component_utils(lane_monitor)
  virtual lane_if vif;
  uvm_analysis_port #(lane_in)  in_ap;
  uvm_analysis_port #(lane_out) out_ap;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_ap  = new("in_ap", this);
    out_ap = new("out_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.valid && vif.ready) begin
        lane_in t = lane_in::type_id::create("t");
        t.x   = vif.x;
        t.par = vif.par;
        in_ap.write(t);
      end
      if (vif.out_valid) begin
        lane_out r = lane_out::type_id::create("r");
        r.y   = vif.y;
        r.err = vif.err;
        out_ap.write(r);
      end
    end
  endtask
endclass

`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)

// In-order scoreboard. It predicts from what the monitor saw on the pins, so it needs no
// knowledge of any callback: a beat with bad parity predicts a parity error.
class lane_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(lane_scoreboard)
  uvm_analysis_imp_in  #(lane_in,  lane_scoreboard) in_imp;
  uvm_analysis_imp_out #(lane_out, lane_scoreboard) out_imp;
  string   lane;
  lane_out expected[$];
  int      matched, mismatched, errors_seen;
  int      bad_in;                         // beats the monitor saw with bad parity
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_imp  = new("in_imp", this);
    out_imp = new("out_imp", this);
  endfunction
  function void write_in(lane_in t);
    lane_out e = lane_out::type_id::create("e");
    e.err = (t.par != ^t.x);
    if (e.err) bad_in++;
    if (!e.err) e.y = t.x + 1;
    expected.push_back(e);
  endfunction
  function void write_out(lane_out r);
    lane_out e;
    if (expected.size() == 0) begin
      `uvm_error("SB", $sformatf("%s: %s with nothing predicted", lane, r.show()))
      return;
    end
    e = expected.pop_front();
    if (r.err) errors_seen++;
    if (r.err == e.err && (r.err || r.y == e.y)) begin   // y means nothing when err = 1
      matched++;
    end
    else begin
      mismatched++;
      `uvm_error("SB", $sformatf("%s: %s, predicted %s", lane, r.show(), e.show()))
    end
  endfunction
  function void check_phase(uvm_phase phase);
    if (expected.size() != 0)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", lane, expected.size()))
  endfunction
endclass

class lane_agent extends uvm_agent;
  `uvm_component_utils(lane_agent)
  uvm_sequencer #(lane_item) sqr;
  lane_driver  drv;
  lane_monitor mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual lane_if vif;
    super.build_phase(phase);
    if (!uvm_config_db #(virtual lane_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface for this agent")
    sqr = uvm_sequencer #(lane_item)::type_id::create("sqr", this);
    drv = lane_driver::type_id::create("drv", this);
    mon = lane_monitor::type_id::create("mon", this);
    drv.vif = vif;
    mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

// Two lanes, each with its own agent and scoreboard.
class chip_env extends uvm_env;
  `uvm_component_utils(chip_env)
  lane_agent      port_a, port_b;
  lane_scoreboard sb_a, sb_b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    port_a = lane_agent::type_id::create("port_a", this);
    port_b = lane_agent::type_id::create("port_b", this);
    sb_a   = lane_scoreboard::type_id::create("sb_a", this);
    sb_b   = lane_scoreboard::type_id::create("sb_b", this);
    sb_a.lane = "port_a";
    sb_b.lane = "port_b";
  endfunction
  function void connect_phase(uvm_phase phase);
    port_a.mon.in_ap.connect(sb_a.in_imp);
    port_a.mon.out_ap.connect(sb_a.out_imp);
    port_b.mon.in_ap.connect(sb_b.in_imp);
    port_b.mon.out_ap.connect(sb_b.out_imp);
  endfunction
endclass

// Sends 16, 32 and 48, numbered 1, 2 and 3.
class lane_seq extends uvm_sequence #(lane_item);
  `uvm_object_utils(lane_seq)
  function new(string name = "lane_seq"); super.new(name); endfunction
  task body();
    bit [7:0] values[3] = '{16, 32, 48};
    foreach (values[i]) begin
      lane_item it = lane_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      it.n = i + 1;
      finish_item(it);
    end
  endtask
endclass

// Error injection, written in the test: flip the parity bit of item 2. It edits only the beat.
class parity_error_cb extends lane_driver_cb;
  `uvm_object_utils(parity_error_cb)
  int target = 2;
  int hits;
  function new(string name = "parity_error_cb"); super.new(name); endfunction
  function void pre_drive(lane_driver drv, lane_item it, lane_beat beat);
    if (it.n != target) return;
    beat.par = !beat.par;
    hits++;
  endfunction
endclass

// Samples the beat in pre_drive, so what it sees depends on where it sits in the order.
class probe_cb extends lane_driver_cb;
  `uvm_object_utils(probe_cb)
  int beats, bad;
  function new(string name = "probe_cb"); super.new(name); endfunction
  function void pre_drive(lane_driver drv, lane_item it, lane_beat beat);
    beats++;
    if (beat.par != ^beat.x) bad++;
  endfunction
endclass

class order_test extends uvm_test;
  `uvm_component_utils(order_test)
  chip_env        env;
  parity_error_cb perr;
  probe_cb        probe1, probe2;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env    = chip_env::type_id::create("env", this);
    perr   = parity_error_cb::type_id::create("perr");
    probe1 = probe_cb::type_id::create("probe1");
    probe2 = probe_cb::type_id::create("probe2");
  endfunction
  function void connect_phase(uvm_phase phase);
    uvm_callbacks #(lane_driver, lane_driver_cb)::add(env.port_a.drv, perr);
    uvm_callbacks #(lane_driver, lane_driver_cb)::add(env.port_a.drv, probe1);
    uvm_callbacks #(lane_driver, lane_driver_cb)::add(env.port_a.drv, probe2, UVM_PREPEND);
  endfunction
  task run_phase(uvm_phase phase);
    lane_seq seq1 = lane_seq::type_id::create("seq1");
    lane_seq seq2 = lane_seq::type_id::create("seq2");
    phase.raise_objection(this);
    seq1.start(env.port_a.sqr);
    perr.callback_mode(0);
    uvm_callbacks #(lane_driver, lane_driver_cb)::delete(env.port_a.drv, probe1);
    seq2.start(env.port_a.sqr);
    #25;
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    $display("probe1 %0d beats %0d bad; probe2 %0d beats %0d bad; perr hits %0d",
             probe1.beats, probe1.bad, probe2.beats, probe2.bad, perr.hits);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  lane_if bus_a (clk);
  lane_if bus_b (clk);
  lane dut_a (bus_a);
  lane dut_b (bus_b);
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual lane_if)::set(null, "uvm_test_top.env.port_a", "vif", bus_a);
    uvm_config_db #(virtual lane_if)::set(null, "uvm_test_top.env.port_b", "vif", bus_b);
    run_test("order_test");
  end
endmodule
Spot the bugEach option is the method inside the test's injector class, registered on env.port_b.drv. Both lanes check parity correctly here, so lane B should show exactly one parity error. Which option injects nothing?
Predict the outputSomeone moves the injector's flip into post_drive and registers it on env.port_b.drv before the coverage hook cov, which also runs in post_drive. Lane B ignores parity, as in the example. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// One lane of the chip: the U8 incrementer with a parity bit on its input. The lane takes x at a
// rising edge where valid and ready are 1 and is busy for the next two edges. One edge later it
// shows the result for one cycle: y = x + 1 with err = 0, or err = 1 when par was not ^x.
// With err = 1 the value on y is unspecified (this RTL happens to drive 0).
interface lane_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       par = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
  logic       err = 0;
endinterface

// CHECK_PARITY = 0 is a fault switch: that lane ignores the parity bit.
module lane #(parameter bit CHECK_PARITY = 1) (lane_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic       bad = 0;
  logic [7:0] stage = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged;
    bus.err       <= bad;
    bus.y         <= bad ? 8'd0 : stage;
    staged        <= bus.valid && bus.ready;
    bad           <= CHECK_PARITY && (bus.par != ^bus.x);
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= 2;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// What the sequence asks for.
class lane_item extends uvm_sequence_item;
  `uvm_object_utils(lane_item)
  bit [7:0] x;
  int       n;                             // 1, 2, 3: position in the sequence
  function new(string name = "lane_item"); super.new(name); endfunction
endclass

// What the driver is about to put on the pins. Callbacks may edit this, never the item.
class lane_beat;
  bit [7:0] x;
  bit       par;
endclass

// What the monitor saw going in and coming out.
class lane_in extends uvm_sequence_item;
  `uvm_object_utils(lane_in)
  bit [7:0] x;
  bit       par;
  function new(string name = "lane_in"); super.new(name); endfunction
endclass

class lane_out extends uvm_sequence_item;
  `uvm_object_utils(lane_out)
  bit [7:0] y;
  bit       err;
  function new(string name = "lane_out"); super.new(name); endfunction
  function string show();
    string s;
    if (err) s = "parity error";
    else     s = $sformatf("y=%0d", y);
    return s;
  endfunction
endclass

typedef class lane_driver;

// The hooks the driver offers. Each does nothing until a registered callback overrides it.
class lane_driver_cb extends uvm_callback;
  `uvm_object_utils(lane_driver_cb)
  function new(string name = "lane_driver_cb"); super.new(name); endfunction
  // After the driver has built the beat, before it touches the pins.
  virtual function void pre_drive(lane_driver drv, lane_item it, lane_beat beat); endfunction
  // After the lane has taken the beat (an edge with ready), before item_done.
  virtual function void post_drive(lane_driver drv, lane_item it, lane_beat beat); endfunction
endclass

class lane_driver extends uvm_driver #(lane_item);
  `uvm_component_utils(lane_driver)
  `uvm_register_cb(lane_driver, lane_driver_cb)       // this driver type offers lane_driver_cb hooks
  virtual lane_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    lane_beat beat;
    forever begin
      @(negedge vif.clk);
      seq_item_port.try_next_item(req);
      if (req == null) begin
        vif.valid <= 0;
        continue;
      end
      beat = new;
      beat.x   = req.x;
      beat.par = ^req.x;                                     // correct parity
      `uvm_do_callbacks(lane_driver, lane_driver_cb, pre_drive(this, req, beat))
      vif.valid <= 1;
      vif.x     <= beat.x;
      vif.par   <= beat.par;
      do @(posedge vif.clk); while (!vif.ready);             // the lane takes the beat here
      `uvm_do_callbacks(lane_driver, lane_driver_cb, post_drive(this, req, beat))
      seq_item_port.item_done();
    end
  endtask
endclass

class lane_monitor extends uvm_monitor;
  `uvm_component_utils(lane_monitor)
  virtual lane_if vif;
  uvm_analysis_port #(lane_in)  in_ap;
  uvm_analysis_port #(lane_out) out_ap;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_ap  = new("in_ap", this);
    out_ap = new("out_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.valid && vif.ready) begin
        lane_in t = lane_in::type_id::create("t");
        t.x   = vif.x;
        t.par = vif.par;
        in_ap.write(t);
      end
      if (vif.out_valid) begin
        lane_out r = lane_out::type_id::create("r");
        r.y   = vif.y;
        r.err = vif.err;
        out_ap.write(r);
      end
    end
  endtask
endclass

`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)

// In-order scoreboard. It predicts from what the monitor saw on the pins, so it needs no
// knowledge of any callback: a beat with bad parity predicts a parity error.
class lane_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(lane_scoreboard)
  uvm_analysis_imp_in  #(lane_in,  lane_scoreboard) in_imp;
  uvm_analysis_imp_out #(lane_out, lane_scoreboard) out_imp;
  string   lane;
  lane_out expected[$];
  int      matched, mismatched, errors_seen;
  int      bad_in;                         // beats the monitor saw with bad parity
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_imp  = new("in_imp", this);
    out_imp = new("out_imp", this);
  endfunction
  function void write_in(lane_in t);
    lane_out e = lane_out::type_id::create("e");
    e.err = (t.par != ^t.x);
    if (e.err) bad_in++;
    if (!e.err) e.y = t.x + 1;
    expected.push_back(e);
  endfunction
  function void write_out(lane_out r);
    lane_out e;
    if (expected.size() == 0) begin
      `uvm_error("SB", $sformatf("%s: %s with nothing predicted", lane, r.show()))
      return;
    end
    e = expected.pop_front();
    if (r.err) errors_seen++;
    if (r.err == e.err && (r.err || r.y == e.y)) begin   // y means nothing when err = 1
      matched++;
    end
    else begin
      mismatched++;
      `uvm_error("SB", $sformatf("%s: %s, predicted %s", lane, r.show(), e.show()))
    end
  endfunction
  function void check_phase(uvm_phase phase);
    if (expected.size() != 0)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", lane, expected.size()))
  endfunction
endclass

class lane_agent extends uvm_agent;
  `uvm_component_utils(lane_agent)
  uvm_sequencer #(lane_item) sqr;
  lane_driver  drv;
  lane_monitor mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual lane_if vif;
    super.build_phase(phase);
    if (!uvm_config_db #(virtual lane_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface for this agent")
    sqr = uvm_sequencer #(lane_item)::type_id::create("sqr", this);
    drv = lane_driver::type_id::create("drv", this);
    mon = lane_monitor::type_id::create("mon", this);
    drv.vif = vif;
    mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

// Two lanes, each with its own agent and scoreboard.
class chip_env extends uvm_env;
  `uvm_component_utils(chip_env)
  lane_agent      port_a, port_b;
  lane_scoreboard sb_a, sb_b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    port_a = lane_agent::type_id::create("port_a", this);
    port_b = lane_agent::type_id::create("port_b", this);
    sb_a   = lane_scoreboard::type_id::create("sb_a", this);
    sb_b   = lane_scoreboard::type_id::create("sb_b", this);
    sb_a.lane = "port_a";
    sb_b.lane = "port_b";
  endfunction
  function void connect_phase(uvm_phase phase);
    port_a.mon.in_ap.connect(sb_a.in_imp);
    port_a.mon.out_ap.connect(sb_a.out_imp);
    port_b.mon.in_ap.connect(sb_b.in_imp);
    port_b.mon.out_ap.connect(sb_b.out_imp);
  endfunction
endclass

// Sends 16, 32 and 48, numbered 1, 2 and 3.
class lane_seq extends uvm_sequence #(lane_item);
  `uvm_object_utils(lane_seq)
  function new(string name = "lane_seq"); super.new(name); endfunction
  task body();
    bit [7:0] values[3] = '{16, 32, 48};
    foreach (values[i]) begin
      lane_item it = lane_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      it.n = i + 1;
      finish_item(it);
    end
  endtask
endclass

// Coverage hook, also written in the test: counts the beats each lane took, by parity.
class parity_cov_cb extends lane_driver_cb;
  `uvm_object_utils(parity_cov_cb)
  int good[string], bad[string];
  function new(string name = "parity_cov_cb"); super.new(name); endfunction
  function void post_drive(lane_driver drv, lane_item it, lane_beat beat);
    string lane = drv.get_parent().get_name();
    if (!good.exists(lane)) begin
      good[lane] = 0;
      bad[lane]  = 0;
    end
    if (beat.par == ^beat.x) good[lane]++;
    else                     bad[lane]++;
  endfunction
endclass

// The names of the enabled callbacks a driver will call, in calling order.
function automatic string roster(lane_driver d);
  uvm_callback_iter #(lane_driver, lane_driver_cb) iter = new(d);
  lane_driver_cb cb = iter.first();
  string s = "";
  while (cb != null) begin
    s  = {s, " ", cb.get_name()};
    cb = iter.next();
  end
  return s;
endfunction

// The same injector, but it edits the beat after the lane has taken it.
class late_error_cb extends lane_driver_cb;
  `uvm_object_utils(late_error_cb)
  function new(string name = "late_error_cb"); super.new(name); endfunction
  function void post_drive(lane_driver drv, lane_item it, lane_beat beat);
    if (it.n == 2) beat.par = !beat.par;
  endfunction
endclass

class late_test extends uvm_test;
  `uvm_component_utils(late_test)
  chip_env      env;
  late_error_cb perr;
  parity_cov_cb cov;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    env  = chip_env::type_id::create("env", this);
    perr = late_error_cb::type_id::create("perr");
    cov  = parity_cov_cb::type_id::create("cov");
  endfunction
  function void connect_phase(uvm_phase phase);
    uvm_callbacks #(lane_driver, lane_driver_cb)::add(env.port_b.drv, perr);
    uvm_callbacks #(lane_driver, lane_driver_cb)::add(env.port_b.drv, cov);
  endfunction
  task run_phase(uvm_phase phase);
    lane_seq seq = lane_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(env.port_b.sqr);
    #25;
    phase.drop_objection(this);
  endtask
  function void check_phase(uvm_phase phase);
    if (env.sb_b.bad_in == 0)
      `uvm_error("CBIDLE", "port_b's monitor saw no bad-parity beat")
  endfunction
  function void report_phase(uvm_phase phase);
    $display("cov counted %0d bad beat(s) on port_b; port_b's monitor saw %0d", cov.bad["port_b"], env.sb_b.bad_in);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  lane_if bus_a (clk);
  lane_if bus_b (clk);
  lane dut_a (bus_a);
  lane #(.CHECK_PARITY(0)) dut_b (bus_b);      // fault switch: lane B ignores parity
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual lane_if)::set(null, "uvm_test_top.env.port_a", "vif", bus_a);
    uvm_config_db #(virtual lane_if)::set(null, "uvm_test_top.env.port_b", "vif", bus_b);
    run_test("late_test");
  end
endmodule
ConceptUsers of the lane VIP keep asking for a different value on x between items: hold the last value, or drive 0. Each bench picks one and keeps it for the whole run. As the VIP author, which extension point fits best?
ConceptA test calls uvm_callbacks #(lane_driver, lane_driver_cb)::add(drv_b, perr) in its build_phase, where drv_b is a test variable it planned to fill later. drv_b is still null at that moment. What happens?

Remember

  • The VIP author places hook points where the driver's state is complete, passes pin values to edit and the item to read, and keeps those arguments stable.
  • Modify in a pre-transfer hook, observe in a post-transfer hook; within one hook, callbacks run in the order added, and UVM_PREPEND puts one first.
  • add(obj, cb) affects one instance; add(null, cb) affects every instance of the type. callback_mode(0) skips a callback; delete removes it.
  • A callback on the wrong instance or type, behind a misspelled hook, or in a hook that runs too late never reaches the pins; check in check_phase that the target's monitor saw the injected case.
  • Prefer a config knob for a foreseeable value, an override to replace behaviour, and a callback for stackable, switchable additions at one point.

Practice