Vertical reuse: a block environment inside a subsystem

Reuse a block environment inside a subsystem bench, keep its checks working when its bus becomes internal, and catch the block environment that still drives.

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

Block B's environment moved into the subsystem bench unchanged, and its scoreboard reported no errors. It also reported no inputs: still active, its driver wrote valid = 0 at each idle falling edge of the internal bus, erasing every result that block A sent across. Block A's checks passed, and nothing compared the two blocks.

Block B's agentBlock A in / outBlock B in / outBlock B scoreboardLink check
Passive (the example)3 / 33 / 33 matched3 values matched
Left active (the hook)3 / 30 / 0No errorsFails: 3 of A's results never reached B

Measured on the example subsystem. Counts are the transfers and results each monitor published.

From block bench to subsystem bench

At block level, an environment owns everything for one block: an agent, a predictor and comparator from U8.1, and a config object. Vertical reuse puts that same environment class inside a larger bench, so the checks written once keep running at the next level of integration.

The same block environment, twice, in a subsystem benchBlock B's input is now block A's output. Arrows into a bus are driving; arrows out of a bus are observing.
The same block environment, twice, in a subsystem benchBlock B's input is now block A's output. Arrows into a bus are driving; arrows out of a bus are observing. test sub_test_base Builds sub_env_cfgonce env sub env Passes each block itscfg; compares Aoutputs with B inputs block env blk_a env(active) drv, sqr, mon, pred,cmp block env blk_b env(passive) mon, pred, cmp; nodriver interface ext bus tb.ext, at thesubsystem pins design Block A RTL Shows y = x + 1 interface mid bus tb.dut.mid, internal cfgcfg.blk_acfg.blk_bdrivesxresult is B inputobserved
  1. Configure

    The test fills one sub_env_cfg. The sub env hands cfg.blk_a to blk_a and cfg.blk_b to blk_b; block B's agent config says UVM_PASSIVE.

    More context

    The first two lines of the run show the result: "blk_a UVM_ACTIVE agent builds: mon sqr drv" and "blk_b UVM_PASSIVE agent builds: mon".

    Follow the calls, values and waits
    1. data sub_env_cfg: sub_test_base → sub env
    2. data cfg.blk_a: sub env → blk_a env (active)
    3. data cfg.blk_b (passive agent): sub env → blk_b env (passive)
  2. Drive A15 ns (illustrative)

    Only block A's driver touches pins. It puts 16 on ext and the design takes it at 15 ns.

    More context

    Block B has no driver, so nothing else drives the subsystem. Left active, block B builds a driver that takes nothing, and the LINK check reports three results that never arrived.

    Follow the calls, values and waits
    1. data x = 16: blk_a env (active) → ext bus
    2. data x = 16: ext bus → Block A RTL
  3. Cross35 ns (illustrative)

    Block A shows 17 on its result pins. Inside the subsystem that result is block B's input, taken at 35 ns.

    More context

    At the same edge block A's own comparator logs "35 ns blk_a y=17 matches". One value is A's output and B's input at once.

    Follow the calls, values and waits
    1. data A result 17 = B input: Block A RTL → mid bus
  4. Observe B's input35 ns (illustrative)

    Block B's monitor watches mid and feeds its own predictor and comparator, unchanged from the block bench.

    More context

    B's monitor samples 17 at 35 ns, and its predictor expects 18. Nothing in block B's environment knows that its input came from block A.

    Follow the calls, values and waits
    1. data A result 17 = B input: mid bus → blk_b env (passive)
    2. Waiting: blk_b env (passive) — expects 18 from block B
  5. Check B55 ns (illustrative)

    B's result, 18, is checked at 55 ns.

    More context

    The log reads "55 ns blk_b y=18 matches". Each B result arrives 20 ns after the A result that fed it.

    Follow the calls, values and waits
    1. Released: blk_b env (passive)
Read the diagram as text
  • sub_test_base: Builds sub_env_cfg once
  • sub env: Passes each block its cfg; compares A outputs with B inputs
  • blk_a env (active): drv, sqr, mon, pred, cmp
  • blk_b env (passive): mon, pred, cmp; no driver
  • ext bus: tb.ext, at the subsystem pins
  • Block A RTL: Shows y = x + 1
  • mid bus: tb.dut.mid, internal
  • sub_test_base → sub env: cfg
  • sub env → blk_a env (active): cfg.blk_a
  • sub env → blk_b env (passive): cfg.blk_b
  • blk_a env (active) → ext bus: drives
  • ext bus → Block A RTL: x
  • Block A RTL → mid bus: result is B input
  • mid bus → blk_b env (passive): observed

Two copies of one environment class. What changed is one config field and where the virtual interface points.

Here the subsystem chains two copies of the U8 incrementer, so the bench holds two incr_env objects. Block A's input is still at the pins and its environment stays active. Block B's input is now block A's output, which the bench must not drive, so its agent becomes passive, as in U11.1.

What changes and what stays

Part of block B's envBlock benchSubsystem bench
Driver and sequencerBuilt, drive B's pinsNot built
MonitorWatches B's interfaceSame class, pointed at dut.mid
Predictor and comparatorFed by the monitorUnchanged
ConfigurationSet by the block testPassed down by the sub env
StimulusB's own sequencesOnly what block A produces
End of testThe block test drops after its sequenceEach block env drains itself

The checking half of the environment moves unchanged.

This works only because the environment was written for it. The monitor reads pins, never the driver. The predictor works from observed inputs, never from the sequence. The agent reads its mode from its config before building children. An environment that broke any of these would need editing here.

Block B's coverage now fills only with what block A happens to produce. A block A that never outputs 0 leaves that hole in B's coverage, and the fix belongs in the subsystem's stimulus.

Configuration passes down one level at a time

Configuration goes down one level at a timeEach arrow is one uvm_config_db::set by the component named on it, for a path relative to itself.
Configuration goes down one level at a timeEach arrow is one `uvm_config_db::set` by the component named on it, for a path relative to itself. object sub_env_cfg Set by the test for"sub" object cfg.blk_a max_latency = 20 ns object cfg.blk_b max_latency = 20 ns object cfg.blk_a.agt vif = ext,UVM_ACTIVE object cfg.blk_b.agt vif = dut.mid,UVM_PASSIVE sub sets "blk_a"sub sets "blk_b"blk_a sets "agt"blk_b sets "agt"
  1. The sub env hands down

    The sub env gets sub_env_cfg and sets one field for each child: cfg.blk_a for "blk_a" and cfg.blk_b for "blk_b".

    More context

    Both sets run in the sub env's build_phase, before either block env is built. The paths are relative to the sub env, so they name only its children.

    Follow the calls, values and waits
    1. data cfg.blk_a: sub_env_cfg → cfg.blk_a
    2. data cfg.blk_b: sub_env_cfg → cfg.blk_b
  2. blk_a hands down

    Block A's env gets its incr_env_cfg and sets its agt field for "agt", exactly as it does on the block bench.

    More context

    The run prints "blk_a UVM_ACTIVE agent builds: mon sqr drv". The agent reads only the object its parent set for it.

    Follow the calls, values and waits
    1. data cfg.blk_a.agt (active): cfg.blk_a → cfg.blk_a.agt
  3. blk_b hands down

    Block B's env runs the same code. Its agent config points at dut.mid and says UVM_PASSIVE.

    More context

    The run prints "blk_b UVM_PASSIVE agent builds: mon". The difference was decided by the test, two levels up, in the data it filled.

    Follow the calls, values and waits
    1. data cfg.blk_b.agt (passive): cfg.blk_b → cfg.blk_b.agt
Read the diagram as text
  • sub_env_cfg: Set by the test for "sub"
  • cfg.blk_a: max_latency = 20 ns
  • cfg.blk_b: max_latency = 20 ns
  • cfg.blk_a.agt: vif = ext, UVM_ACTIVE
  • cfg.blk_b.agt: vif = dut.mid, UVM_PASSIVE
  • sub_env_cfg → cfg.blk_a: sub sets "blk_a"
  • sub_env_cfg → cfg.blk_b: sub sets "blk_b"
  • cfg.blk_a → cfg.blk_a.agt: blk_a sets "agt"
  • cfg.blk_b → cfg.blk_b.agt: blk_b sets "agt"

No layer names a grandchild. The block env's own code, the "agt" step, is the same at block and subsystem level.

The subsystem config object holds one block config per block, and each block config holds its agent config. The test fills the whole tree and sets it once, for "sub". The sub env gets it and sets cfg.blk_a for "blk_a" and cfg.blk_b for "blk_b". Each block env sets its agent config for "agt", the same line it ran at block level.

The subsystem test base. Derived tests change sequences, not the structure.
class sub_test_base extends uvm_test;
  function void build_phase(uvm_phase phase);
    cfg = sub_env_cfg::type_id::create("cfg");
    // ... get ext_vif and mid_vif, store them in cfg.blk_a.agt and cfg.blk_b.agt
    cfg.blk_b.agt.is_active = UVM_PASSIVE;     // block A drives block B's input now
    uvm_config_db #(sub_env_cfg)::set(this, "sub", "cfg", cfg);
    sub = sub_env::type_id::create("sub", this);
  endfunction
endclass

A test base class holds this build so every subsystem test inherits it. A derived test such as sub_smoke_test only adds its run_phase. The block test's own lines do not move up: they set paths such as "env" that do not exist in the subsystem. Copied in, they would leave block B's get empty, and incr_env stops with NOCFG at time 0.

Reaching the internal bus

Block B's monitor needs a virtual interface on mid, which exists only inside incr_sub. Because the RTL connects the blocks through an interface instance, the top module can hand it over with a hierarchical reference: uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "mid_vif", dut.mid).

Hierarchical reference

uvm_config_db #(virtual incr_if)::set(
  null, "uvm_test_top", "mid_vif", dut.mid);

Works when the internal bus is an interface instance, as here. The path ties the top module to the RTL hierarchy.

bind a probe onto plain wires

bind sub_rtl incr_probe_if b_probe (
  .clk(clk), .valid(b_valid), .x(b_x),
  .ready(b_ready), .out_valid(b_ov), .y(b_y));
// then pass tb.dut.b_probe to the bench

Puts an interface inside RTL you cannot edit. The probe's signals are inputs, so the monitor must read a read-only interface.

Most RTL connects blocks with plain wires, so bind is the common choice. A VIP meant for this kind of reuse gives its monitor an interface that a bound probe can satisfy. The Cookbook's integration example uses a third form: a binder module that assigns internal signals into a bench-level interface.

Each environment decides when it is done

The subsystem test drops its objection when block A's sequence returns, at 75 ns. At that moment block A owes one result, for 48. Block B owes one, for 33, and its third input, A's answer for 48, has not arrived yet. Neither the test nor the sub env knows the latencies, but each block env does: it is in its config, max_latency.

The drain has two different bounds. max_latency, 20 ns, is a property of the design: every result shows 20 ns after its input. On its own that cannot prove nothing more is coming, because new input may still arrive from upstream. Here it is enough: the test has stopped driving block A, and A's last result reaches B within A's own 20 ns, inside B's window, which then restarts. In general a block's quiet window must also cover the latency of everything upstream of it. drain_limit, 100 ns, is this test's policy: owed work that has not finished by then counts as lost. Without the deadline, a lost result would hold the objection until the global timeout, and check_phase would never report it.

In incr_env, slightly shortened: hold the run phase until this block's work is done, or until the deadline.
function void phase_ready_to_end(uvm_phase phase);
  if (phase.get_name() != "run" || draining) return;
  draining = 1;
  phase.raise_objection(this, "draining");
  fork begin
    fork
      begin
        int seen;
        do begin
          wait (cmp.matched + cmp.mismatched == pred.predicted);
          seen = pred.predicted;
          #(cfg.max_latency);              // quiet window: the design's latency
        end while (pred.predicted != seen);
      end
      #(cfg.drain_limit);                  // failure deadline: this test's policy
    join_any
    disable fork;
    phase.drop_objection(this, "draining");
  end join_none
endfunction

UVM calls phase_ready_to_end on every component when the objection count first reaches zero. A component that raises an objection there keeps the phase open, and UVM asks again after it drops, up to 20 rounds by default.

Results and drains after the test drops at 75 ns
012345time (ns)758595105115135block A owesblock B oweseventtest dropsB: y=34A: y=49B window restartsA drained; B: y=50B drainedblk_a drainedblk_b drained

B balances at 85 ns, but block A's last result reaches it at 95 ns, inside its quiet window, so B waits again.

The quiet window needs the restart. Block B balanced at 85 ns while block A still held 48. A drain that ended after the first window would have dropped at 105 ns with B's last result still owed. Only block A's own drain would have kept the run open. After any drain, check_phase in each block env reports predictions that never met a result.

When the block environment still drives

Leave block B active and nothing fails to build. B's driver gets no items, so at every falling edge it writes valid = 0, as the U8 driver does when idle. Block A's result sets mid.valid just after a rising edge, and B's driver clears it half a cycle later, before B samples it.

Block A's first result on mid, two ways
012from (ns)253035ext.out_validmid.valid, B passivemid.valid, B activeB takes at 35 ns-17 / noneB's idle driver writes 0B samples the 30 ns column

Each column holds a value from that time until the next column. The rising edge at 35 ns samples the 30 ns column, then block A clears its result. Passive: B takes 17. Active: B takes nothing.

Counting is not enough for that check. With bit 2 of the internal bus stuck at 1, block B takes 21, 37 and 53 where block A showed 17, 33 and 49. Both block scoreboards pass, because B predicts from what it took, and the counts are equal. Comparing A's y with B's x, in order, reports all three.

Every check inside block B still passes, because a scoreboard that sees no inputs predicts nothing and misses nothing. Only a check across the blocks notices. The sub env subscribes to block A's result port and block B's input port and pairs the values in order. Its check_phase reports LINK for every result of A that never reached B.

Try it

The first two lines show block B with only its monitor. Each block's comparator prints its own results; B's arrive 20 ns after A's, and each block env reports when it has drained.

Delete the cfg.blk_b.agt.is_active = UVM_PASSIVE; line in sub_test_base and rerun: block B builds a driver, takes nothing, and the LINK check reports three results that never arrived. Then build incr_sub with #(.STUCK(1)) in tb and watch the link compare values. Then set max_latency to 5 ns in incr_env_cfg and see which drain ends too early, and which check reports it.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: blk_a UVM_ACTIVE agent builds: mon sqr drv blk_b UVM_PASSIVE agent builds: mon 35 ns blk_a y=17 matches 55 ns blk_b y=18 matches 65 ns blk_a y=33 matches 85 ns blk_b y=34 matches 95 ns blk_a y=49 matches 115 ns blk_a drained: 3 predicted, 3 compared 115 ns blk_b y=50 matches 135 ns blk_b drained: 3 predicted, 3 compared block A: 3 in, 3 out; block B: 3 in, 3 out; link 3 matched, 0 mismatched UVM_ERROR : 0

Check yourself

Predict the outputThis subsystem test base leaves out cfg.blk_b.agt.is_active = UVM_PASSIVE, so block B's agent stays active. Everything else is the example. What line does the sub env's report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The incrementer from U7 and U8: takes x at a rising edge where valid and ready are 1, is busy
// for two edges, and shows y = x + 1 with out_valid for one cycle; a monitor sees it two edges later.
interface incr_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
endinterface

// LOSE = 1 is a fault switch: the block never shows its third result.
module incr_pipe #(parameter bit LOSE = 0) (incr_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic [7:0] stage = 0;
  int         shown = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged && !(LOSE && shown == 2);
    if (staged) shown <= shown + 1;
    bus.y         <= stage;
    staged        <= bus.valid && bus.ready;
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= 2;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// The subsystem: block A's result is block B's input, on a bus the bench cannot drive.
// Block A takes at most one value every three edges, so block B is always ready in time.
// STUCK = 1 is a fault switch: bit 2 of the internal bus is stuck at 1.
module incr_sub #(parameter bit STUCK = 0, parameter bit LOSE_B = 0) (incr_if ext);
  incr_if   mid (ext.clk);                  // block B's bus, internal to the subsystem
  incr_pipe blk_a (ext);
  incr_pipe #(.LOSE(LOSE_B)) blk_b (mid);
  always @* begin
    mid.valid = ext.out_valid;
    mid.x     = STUCK ? (ext.y | 8'h04) : ext.y;
  end
endmodule

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass

// The result. compare() and convert2string() are what the comparator calls.
class incr_result extends uvm_sequence_item;
  `uvm_object_utils(incr_result)
  bit [7:0] y;
  function new(string name = "incr_result"); super.new(name); endfunction
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    incr_result o;
    if (!$cast(o, rhs)) return 0;
    return comparer.compare_field_int("y", y, o.y, 8);
  endfunction
  function string convert2string(); return $sformatf("y=%0d", y); endfunction
endclass

class incr_seq extends uvm_sequence #(incr_item);
  `uvm_object_utils(incr_seq)
  bit [7:0] values[$];
  function new(string name = "incr_seq"); super.new(name); values = '{16, 32, 48}; endfunction
  task body();
    foreach (values[i]) begin
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      finish_item(it);
    end
  endtask
endclass

// The U8 driver. With no item it writes valid = 0 at every falling edge.
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  virtual incr_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.valid <= 0;
        continue;
      end
      vif.valid <= 1;
      vif.x     <= req.x;
      do @(posedge vif.clk); while (!vif.ready);
      seq_item_port.item_done();
    end
  endtask
endclass

class incr_monitor extends uvm_monitor;
  `uvm_component_utils(incr_monitor)
  virtual incr_if vif;
  uvm_analysis_port #(incr_item)   in_ap;
  uvm_analysis_port #(incr_result) out_ap;
  int inputs, results;                       // what this monitor has published
  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
        incr_item t = incr_item::type_id::create("t");
        t.x = vif.x;
        inputs++;
        in_ap.write(t);
      end
      if (vif.out_valid) begin
        incr_result r = incr_result::type_id::create("r");
        r.y = vif.y;
        results++;
        out_ap.write(r);
      end
    end
  endtask
endclass

class incr_agent_cfg extends uvm_object;
  `uvm_object_utils(incr_agent_cfg)
  virtual incr_if         vif;
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  function new(string name = "incr_agent_cfg"); super.new(name); endfunction
endclass

class incr_agent extends uvm_agent;
  `uvm_component_utils(incr_agent)
  incr_agent_cfg             cfg;
  uvm_sequencer #(incr_item) sqr;
  incr_driver                drv;
  incr_monitor               mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_agent_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no incr_agent_cfg for this agent")
    is_active = cfg.is_active;
    mon = incr_monitor::type_id::create("mon", this);
    mon.vif = cfg.vif;
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
      drv = incr_driver::type_id::create("drv", this);
      drv.vif = cfg.vif;
    end
  endfunction
  function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

class incr_predictor extends uvm_subscriber #(incr_item);
  `uvm_component_utils(incr_predictor)
  uvm_analysis_port #(incr_result) ap;
  int predicted;
  function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
  function void write(incr_item t);
    incr_result e = incr_result::type_id::create("e");
    e.y = t.x + 1;
    predicted++;
    ap.write(e);
  endfunction
endclass

class incr_comparator extends uvm_component;
  `uvm_component_utils(incr_comparator)
  uvm_analysis_export #(incr_result) before_export, after_export;   // predicted, actual
  uvm_tlm_analysis_fifo #(incr_result) before_fifo, after_fifo;
  int matched, mismatched;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    before_export = new("before_export", this);
    after_export  = new("after_export", this);
    before_fifo   = new("before_fifo", this);
    after_fifo    = new("after_fifo", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    before_export.connect(before_fifo.analysis_export);
    after_export.connect(after_fifo.analysis_export);
  endfunction
  task run_phase(uvm_phase phase);
    incr_result e, a;
    forever begin
      before_fifo.get(e);
      after_fifo.get(a);
      if (e.compare(a)) begin
        matched++;
      end
      else begin
        mismatched++;
        `uvm_error("CMP", $sformatf("%s, predicted %s", a.convert2string(), e.convert2string()))
      end
    end
  endtask
  function void check_phase(uvm_phase phase);
    if (after_fifo.used() != 0)
      `uvm_error("CMP", $sformatf("%0d result(s) arrived with no prediction", after_fifo.used()))
  endfunction
endclass

class incr_env_cfg extends uvm_object;
  `uvm_object_utils(incr_env_cfg)
  incr_agent_cfg agt;
  time           max_latency = 20ns;        // design: a result is always 20 ns after its input
  time           drain_limit = 100ns;       // test policy: how long owed work may take after the test drops
  function new(string name = "incr_env_cfg");
    super.new(name);
    agt = incr_agent_cfg::type_id::create({name, "_agt"});
  endfunction
endclass

class incr_env extends uvm_env;
  `uvm_component_utils(incr_env)
  incr_env_cfg    cfg;
  incr_agent      agt;
  incr_predictor  pred;
  incr_comparator cmp;
  bit             draining;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", {get_full_name(), ": no incr_env_cfg"})
    uvm_config_db #(incr_agent_cfg)::set(this, "agt", "cfg", cfg.agt);
    agt  = incr_agent::type_id::create("agt", this);
    pred = incr_predictor::type_id::create("pred", this);
    cmp  = incr_comparator::type_id::create("cmp", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    agt.mon.in_ap.connect(pred.analysis_export);
    pred.ap.connect(cmp.before_export);
    agt.mon.out_ap.connect(cmp.after_export);
  endfunction
  // Hold the run phase until this block's predictions have all met results, then stay for one
  // quiet window. A prediction that arrives inside the window starts the wait again. The whole
  // drain gives up at drain_limit, so a lost result still reaches check_phase.
  function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run" || draining) return;
    draining = 1;
    phase.raise_objection(this, "draining");
    fork begin
      bit drained = 0;
      fork
        begin
          int seen;
          do begin
            wait (cmp.matched + cmp.mismatched == pred.predicted);
            seen = pred.predicted;
            #(cfg.max_latency);                // quiet window: the design's latency
          end while (pred.predicted != seen);
          drained = 1;
        end
        #(cfg.drain_limit);                    // failure deadline: this test's policy
      join_any
      disable fork;
      phase.drop_objection(this, "draining");
    end join_none
  endfunction
  function void check_phase(uvm_phase phase);
    if (cmp.matched + cmp.mismatched != pred.predicted)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", get_name(),
                                 pred.predicted - cmp.matched - cmp.mismatched))
  endfunction
endclass

`uvm_analysis_imp_decl(_a_out)
`uvm_analysis_imp_decl(_b_in)

class sub_env_cfg extends uvm_object;
  `uvm_object_utils(sub_env_cfg)
  incr_env_cfg blk_a, blk_b;
  function new(string name = "sub_env_cfg");
    super.new(name);
    blk_a = incr_env_cfg::type_id::create("blk_a_cfg");
    blk_b = incr_env_cfg::type_id::create("blk_b_cfg");
  endfunction
endclass

class sub_env extends uvm_env;
  `uvm_component_utils(sub_env)
  sub_env_cfg cfg;
  incr_env    blk_a, blk_b;
  // The link check: block A's observed results against block B's observed inputs, in order.
  uvm_analysis_imp_a_out #(incr_result, sub_env) a_out_imp;
  uvm_analysis_imp_b_in  #(incr_item, sub_env)   b_in_imp;
  bit [7:0] a_shown[$], b_taken[$];        // whichever side arrives first waits here
  int       link_matched, link_mismatched;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    a_out_imp = new("a_out_imp", this);
    b_in_imp  = new("b_in_imp", this);
  endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(sub_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no sub_env_cfg")
    // Each block env gets its part, at a path relative to this env.
    uvm_config_db #(incr_env_cfg)::set(this, "blk_a", "cfg", cfg.blk_a);
    uvm_config_db #(incr_env_cfg)::set(this, "blk_b", "cfg", cfg.blk_b);
    blk_a = incr_env::type_id::create("blk_a", this);
    blk_b = incr_env::type_id::create("blk_b", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    blk_a.agt.mon.out_ap.connect(a_out_imp);
    blk_b.agt.mon.in_ap.connect(b_in_imp);
  endfunction
  // Both monitors sample the same edge, in either order, so the two streams are paired
  // symmetrically: whichever value arrives first waits for its partner.
  function void link(bit [7:0] shown, bit [7:0] taken);
    if (shown == taken) link_matched++;
    else begin
      link_mismatched++;
      `uvm_error("LINK", $sformatf("block A showed y=%0d, block B took x=%0d", shown, taken))
    end
  endfunction
  function void write_a_out(incr_result r);
    if (b_taken.size() != 0) link(r.y, b_taken.pop_front());
    else a_shown.push_back(r.y);
  endfunction
  function void write_b_in(incr_item t);
    if (a_shown.size() != 0) link(a_shown.pop_front(), t.x);
    else b_taken.push_back(t.x);
  endfunction
  // Every result block A showed must have reached block B, and B took nothing else.
  function void check_phase(uvm_phase phase);
    if (a_shown.size() != 0)
      `uvm_error("LINK", $sformatf("%0d result(s) of block A never reached block B", a_shown.size()))
    if (b_taken.size() != 0)
      `uvm_error("LINK", $sformatf("block B took %0d input(s) that block A never showed", b_taken.size()))
  endfunction
  function void report_phase(uvm_phase phase);
    $display("block A: %0d in, %0d out; block B: %0d in, %0d out; link %0d matched, %0d mismatched",
             blk_a.agt.mon.inputs, blk_a.agt.mon.results, blk_b.agt.mon.inputs, blk_b.agt.mon.results,
             link_matched, link_mismatched);
  endfunction
endclass

class sub_test_base extends uvm_test;
  `uvm_component_utils(sub_test_base)
  sub_env     sub;
  sub_env_cfg cfg;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual incr_if ext_vif, mid_vif;
    cfg = sub_env_cfg::type_id::create("cfg");
    if (!uvm_config_db #(virtual incr_if)::get(this, "", "ext_vif", ext_vif) ||
        !uvm_config_db #(virtual incr_if)::get(this, "", "mid_vif", mid_vif))
      `uvm_fatal("NOVIF", "a subsystem interface is missing")
    cfg.blk_a.agt.vif = ext_vif;
    cfg.blk_b.agt.vif = mid_vif;
    uvm_config_db #(sub_env_cfg)::set(this, "sub", "cfg", cfg);
    sub = sub_env::type_id::create("sub", this);
  endfunction
endclass

// Stimulus enters only through block A. Each block env holds the phase until it has drained.
class sub_smoke_test extends sub_test_base;
  `uvm_component_utils(sub_smoke_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    incr_seq seq = incr_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sub.blk_a.agt.sqr);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  incr_if  ext (clk);
  incr_sub dut (ext);
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "ext_vif", ext);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "mid_vif", dut.mid);  // the internal bus
    run_test("sub_smoke_test");
  end
endmodule
Predict the outputBlock B is passive again, but the subsystem is built with #(.STUCK(1)): bit 2 of the internal bus is stuck at 1. What line does the sub env's report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The incrementer from U7 and U8: takes x at a rising edge where valid and ready are 1, is busy
// for two edges, and shows y = x + 1 with out_valid for one cycle; a monitor sees it two edges later.
interface incr_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
endinterface

// LOSE = 1 is a fault switch: the block never shows its third result.
module incr_pipe #(parameter bit LOSE = 0) (incr_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic [7:0] stage = 0;
  int         shown = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged && !(LOSE && shown == 2);
    if (staged) shown <= shown + 1;
    bus.y         <= stage;
    staged        <= bus.valid && bus.ready;
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= 2;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// The subsystem: block A's result is block B's input, on a bus the bench cannot drive.
// Block A takes at most one value every three edges, so block B is always ready in time.
// STUCK = 1 is a fault switch: bit 2 of the internal bus is stuck at 1.
module incr_sub #(parameter bit STUCK = 0, parameter bit LOSE_B = 0) (incr_if ext);
  incr_if   mid (ext.clk);                  // block B's bus, internal to the subsystem
  incr_pipe blk_a (ext);
  incr_pipe #(.LOSE(LOSE_B)) blk_b (mid);
  always @* begin
    mid.valid = ext.out_valid;
    mid.x     = STUCK ? (ext.y | 8'h04) : ext.y;
  end
endmodule

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass

// The result. compare() and convert2string() are what the comparator calls.
class incr_result extends uvm_sequence_item;
  `uvm_object_utils(incr_result)
  bit [7:0] y;
  function new(string name = "incr_result"); super.new(name); endfunction
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    incr_result o;
    if (!$cast(o, rhs)) return 0;
    return comparer.compare_field_int("y", y, o.y, 8);
  endfunction
  function string convert2string(); return $sformatf("y=%0d", y); endfunction
endclass

class incr_seq extends uvm_sequence #(incr_item);
  `uvm_object_utils(incr_seq)
  bit [7:0] values[$];
  function new(string name = "incr_seq"); super.new(name); values = '{16, 32, 48}; endfunction
  task body();
    foreach (values[i]) begin
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      finish_item(it);
    end
  endtask
endclass

// The U8 driver. With no item it writes valid = 0 at every falling edge.
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  virtual incr_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.valid <= 0;
        continue;
      end
      vif.valid <= 1;
      vif.x     <= req.x;
      do @(posedge vif.clk); while (!vif.ready);
      seq_item_port.item_done();
    end
  endtask
endclass

class incr_monitor extends uvm_monitor;
  `uvm_component_utils(incr_monitor)
  virtual incr_if vif;
  uvm_analysis_port #(incr_item)   in_ap;
  uvm_analysis_port #(incr_result) out_ap;
  int inputs, results;                       // what this monitor has published
  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
        incr_item t = incr_item::type_id::create("t");
        t.x = vif.x;
        inputs++;
        in_ap.write(t);
      end
      if (vif.out_valid) begin
        incr_result r = incr_result::type_id::create("r");
        r.y = vif.y;
        results++;
        out_ap.write(r);
      end
    end
  endtask
endclass

class incr_agent_cfg extends uvm_object;
  `uvm_object_utils(incr_agent_cfg)
  virtual incr_if         vif;
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  function new(string name = "incr_agent_cfg"); super.new(name); endfunction
endclass

class incr_agent extends uvm_agent;
  `uvm_component_utils(incr_agent)
  incr_agent_cfg             cfg;
  uvm_sequencer #(incr_item) sqr;
  incr_driver                drv;
  incr_monitor               mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_agent_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no incr_agent_cfg for this agent")
    is_active = cfg.is_active;
    mon = incr_monitor::type_id::create("mon", this);
    mon.vif = cfg.vif;
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
      drv = incr_driver::type_id::create("drv", this);
      drv.vif = cfg.vif;
    end
  endfunction
  function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

class incr_predictor extends uvm_subscriber #(incr_item);
  `uvm_component_utils(incr_predictor)
  uvm_analysis_port #(incr_result) ap;
  int predicted;
  function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
  function void write(incr_item t);
    incr_result e = incr_result::type_id::create("e");
    e.y = t.x + 1;
    predicted++;
    ap.write(e);
  endfunction
endclass

class incr_comparator extends uvm_component;
  `uvm_component_utils(incr_comparator)
  uvm_analysis_export #(incr_result) before_export, after_export;   // predicted, actual
  uvm_tlm_analysis_fifo #(incr_result) before_fifo, after_fifo;
  int matched, mismatched;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    before_export = new("before_export", this);
    after_export  = new("after_export", this);
    before_fifo   = new("before_fifo", this);
    after_fifo    = new("after_fifo", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    before_export.connect(before_fifo.analysis_export);
    after_export.connect(after_fifo.analysis_export);
  endfunction
  task run_phase(uvm_phase phase);
    incr_result e, a;
    forever begin
      before_fifo.get(e);
      after_fifo.get(a);
      if (e.compare(a)) begin
        matched++;
      end
      else begin
        mismatched++;
        `uvm_error("CMP", $sformatf("%s, predicted %s", a.convert2string(), e.convert2string()))
      end
    end
  endtask
  function void check_phase(uvm_phase phase);
    if (after_fifo.used() != 0)
      `uvm_error("CMP", $sformatf("%0d result(s) arrived with no prediction", after_fifo.used()))
  endfunction
endclass

class incr_env_cfg extends uvm_object;
  `uvm_object_utils(incr_env_cfg)
  incr_agent_cfg agt;
  time           max_latency = 20ns;        // design: a result is always 20 ns after its input
  time           drain_limit = 100ns;       // test policy: how long owed work may take after the test drops
  function new(string name = "incr_env_cfg");
    super.new(name);
    agt = incr_agent_cfg::type_id::create({name, "_agt"});
  endfunction
endclass

class incr_env extends uvm_env;
  `uvm_component_utils(incr_env)
  incr_env_cfg    cfg;
  incr_agent      agt;
  incr_predictor  pred;
  incr_comparator cmp;
  bit             draining;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", {get_full_name(), ": no incr_env_cfg"})
    uvm_config_db #(incr_agent_cfg)::set(this, "agt", "cfg", cfg.agt);
    agt  = incr_agent::type_id::create("agt", this);
    pred = incr_predictor::type_id::create("pred", this);
    cmp  = incr_comparator::type_id::create("cmp", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    agt.mon.in_ap.connect(pred.analysis_export);
    pred.ap.connect(cmp.before_export);
    agt.mon.out_ap.connect(cmp.after_export);
  endfunction
  // Hold the run phase until this block's predictions have all met results, then stay for one
  // quiet window. A prediction that arrives inside the window starts the wait again. The whole
  // drain gives up at drain_limit, so a lost result still reaches check_phase.
  function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run" || draining) return;
    draining = 1;
    phase.raise_objection(this, "draining");
    fork begin
      bit drained = 0;
      fork
        begin
          int seen;
          do begin
            wait (cmp.matched + cmp.mismatched == pred.predicted);
            seen = pred.predicted;
            #(cfg.max_latency);                // quiet window: the design's latency
          end while (pred.predicted != seen);
          drained = 1;
        end
        #(cfg.drain_limit);                    // failure deadline: this test's policy
      join_any
      disable fork;
      phase.drop_objection(this, "draining");
    end join_none
  endfunction
  function void check_phase(uvm_phase phase);
    if (cmp.matched + cmp.mismatched != pred.predicted)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", get_name(),
                                 pred.predicted - cmp.matched - cmp.mismatched))
  endfunction
endclass

`uvm_analysis_imp_decl(_a_out)
`uvm_analysis_imp_decl(_b_in)

class sub_env_cfg extends uvm_object;
  `uvm_object_utils(sub_env_cfg)
  incr_env_cfg blk_a, blk_b;
  function new(string name = "sub_env_cfg");
    super.new(name);
    blk_a = incr_env_cfg::type_id::create("blk_a_cfg");
    blk_b = incr_env_cfg::type_id::create("blk_b_cfg");
  endfunction
endclass

class sub_env extends uvm_env;
  `uvm_component_utils(sub_env)
  sub_env_cfg cfg;
  incr_env    blk_a, blk_b;
  // The link check: block A's observed results against block B's observed inputs, in order.
  uvm_analysis_imp_a_out #(incr_result, sub_env) a_out_imp;
  uvm_analysis_imp_b_in  #(incr_item, sub_env)   b_in_imp;
  bit [7:0] a_shown[$], b_taken[$];        // whichever side arrives first waits here
  int       link_matched, link_mismatched;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    a_out_imp = new("a_out_imp", this);
    b_in_imp  = new("b_in_imp", this);
  endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(sub_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no sub_env_cfg")
    // Each block env gets its part, at a path relative to this env.
    uvm_config_db #(incr_env_cfg)::set(this, "blk_a", "cfg", cfg.blk_a);
    uvm_config_db #(incr_env_cfg)::set(this, "blk_b", "cfg", cfg.blk_b);
    blk_a = incr_env::type_id::create("blk_a", this);
    blk_b = incr_env::type_id::create("blk_b", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    blk_a.agt.mon.out_ap.connect(a_out_imp);
    blk_b.agt.mon.in_ap.connect(b_in_imp);
  endfunction
  // Both monitors sample the same edge, in either order, so the two streams are paired
  // symmetrically: whichever value arrives first waits for its partner.
  function void link(bit [7:0] shown, bit [7:0] taken);
    if (shown == taken) link_matched++;
    else begin
      link_mismatched++;
      `uvm_error("LINK", $sformatf("block A showed y=%0d, block B took x=%0d", shown, taken))
    end
  endfunction
  function void write_a_out(incr_result r);
    if (b_taken.size() != 0) link(r.y, b_taken.pop_front());
    else a_shown.push_back(r.y);
  endfunction
  function void write_b_in(incr_item t);
    if (a_shown.size() != 0) link(a_shown.pop_front(), t.x);
    else b_taken.push_back(t.x);
  endfunction
  // Every result block A showed must have reached block B, and B took nothing else.
  function void check_phase(uvm_phase phase);
    if (a_shown.size() != 0)
      `uvm_error("LINK", $sformatf("%0d result(s) of block A never reached block B", a_shown.size()))
    if (b_taken.size() != 0)
      `uvm_error("LINK", $sformatf("block B took %0d input(s) that block A never showed", b_taken.size()))
  endfunction
  function void report_phase(uvm_phase phase);
    $display("block A: %0d in, %0d out; block B: %0d in, %0d out; link %0d matched, %0d mismatched",
             blk_a.agt.mon.inputs, blk_a.agt.mon.results, blk_b.agt.mon.inputs, blk_b.agt.mon.results,
             link_matched, link_mismatched);
  endfunction
endclass

class sub_test_base extends uvm_test;
  `uvm_component_utils(sub_test_base)
  sub_env     sub;
  sub_env_cfg cfg;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual incr_if ext_vif, mid_vif;
    cfg = sub_env_cfg::type_id::create("cfg");
    if (!uvm_config_db #(virtual incr_if)::get(this, "", "ext_vif", ext_vif) ||
        !uvm_config_db #(virtual incr_if)::get(this, "", "mid_vif", mid_vif))
      `uvm_fatal("NOVIF", "a subsystem interface is missing")
    cfg.blk_a.agt.vif = ext_vif;
    cfg.blk_b.agt.vif = mid_vif;
    cfg.blk_b.agt.is_active = UVM_PASSIVE;     // block A drives block B's input now
    uvm_config_db #(sub_env_cfg)::set(this, "sub", "cfg", cfg);
    sub = sub_env::type_id::create("sub", this);
  endfunction
endclass

// Stimulus enters only through block A. Each block env holds the phase until it has drained.
class sub_smoke_test extends sub_test_base;
  `uvm_component_utils(sub_smoke_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    incr_seq seq = incr_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sub.blk_a.agt.sqr);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  incr_if  ext (clk);
  incr_sub #(.STUCK(1)) dut (ext);      // fault switch: one internal wire stuck at 1
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "ext_vif", ext);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "mid_vif", dut.mid);  // the internal bus
    run_test("sub_smoke_test");
  end
endmodule
Spot the bugEach option is the sub env's code that hands the block configs to its children. Which one makes block B's environment stop at time 0?
ConceptBlock B's environment is passive in the subsystem. Which description of it is right?
Predict the outputThe subsystem is built with #(.LOSE_B(1)): block B never shows its third result. The test still drops at 75 ns, and each block env drains with max_latency = 20 ns and drain_limit = 100 ns. What line does the test's report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The incrementer from U7 and U8: takes x at a rising edge where valid and ready are 1, is busy
// for two edges, and shows y = x + 1 with out_valid for one cycle; a monitor sees it two edges later.
interface incr_if (input logic clk);
  logic       valid = 0;
  logic [7:0] x = 0;
  logic       ready;
  logic       out_valid = 0;
  logic [7:0] y = 0;
endinterface

// LOSE = 1 is a fault switch: the block never shows its third result.
module incr_pipe #(parameter bit LOSE = 0) (incr_if bus);
  logic [1:0] busy = 0;
  logic       staged = 0;
  logic [7:0] stage = 0;
  int         shown = 0;
  assign bus.ready = (busy == 0);
  always @(posedge bus.clk) begin
    bus.out_valid <= staged && !(LOSE && shown == 2);
    if (staged) shown <= shown + 1;
    bus.y         <= stage;
    staged        <= bus.valid && bus.ready;
    stage         <= bus.x + 1;
    if (bus.valid && bus.ready) busy <= 2;
    else if (busy != 0)         busy <= busy - 1;
  end
endmodule

// The subsystem: block A's result is block B's input, on a bus the bench cannot drive.
// Block A takes at most one value every three edges, so block B is always ready in time.
// STUCK = 1 is a fault switch: bit 2 of the internal bus is stuck at 1.
module incr_sub #(parameter bit STUCK = 0, parameter bit LOSE_B = 0) (incr_if ext);
  incr_if   mid (ext.clk);                  // block B's bus, internal to the subsystem
  incr_pipe blk_a (ext);
  incr_pipe #(.LOSE(LOSE_B)) blk_b (mid);
  always @* begin
    mid.valid = ext.out_valid;
    mid.x     = STUCK ? (ext.y | 8'h04) : ext.y;
  end
endmodule

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass

// The result. compare() and convert2string() are what the comparator calls.
class incr_result extends uvm_sequence_item;
  `uvm_object_utils(incr_result)
  bit [7:0] y;
  function new(string name = "incr_result"); super.new(name); endfunction
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    incr_result o;
    if (!$cast(o, rhs)) return 0;
    return comparer.compare_field_int("y", y, o.y, 8);
  endfunction
  function string convert2string(); return $sformatf("y=%0d", y); endfunction
endclass

class incr_seq extends uvm_sequence #(incr_item);
  `uvm_object_utils(incr_seq)
  bit [7:0] values[$];
  function new(string name = "incr_seq"); super.new(name); values = '{16, 32, 48}; endfunction
  task body();
    foreach (values[i]) begin
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      finish_item(it);
    end
  endtask
endclass

// The U8 driver. With no item it writes valid = 0 at every falling edge.
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  virtual incr_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.valid <= 0;
        continue;
      end
      vif.valid <= 1;
      vif.x     <= req.x;
      do @(posedge vif.clk); while (!vif.ready);
      seq_item_port.item_done();
    end
  endtask
endclass

class incr_monitor extends uvm_monitor;
  `uvm_component_utils(incr_monitor)
  virtual incr_if vif;
  uvm_analysis_port #(incr_item)   in_ap;
  uvm_analysis_port #(incr_result) out_ap;
  int inputs, results;                       // what this monitor has published
  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
        incr_item t = incr_item::type_id::create("t");
        t.x = vif.x;
        inputs++;
        in_ap.write(t);
      end
      if (vif.out_valid) begin
        incr_result r = incr_result::type_id::create("r");
        r.y = vif.y;
        results++;
        out_ap.write(r);
      end
    end
  endtask
endclass

class incr_agent_cfg extends uvm_object;
  `uvm_object_utils(incr_agent_cfg)
  virtual incr_if         vif;
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  function new(string name = "incr_agent_cfg"); super.new(name); endfunction
endclass

class incr_agent extends uvm_agent;
  `uvm_component_utils(incr_agent)
  incr_agent_cfg             cfg;
  uvm_sequencer #(incr_item) sqr;
  incr_driver                drv;
  incr_monitor               mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_agent_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no incr_agent_cfg for this agent")
    is_active = cfg.is_active;
    mon = incr_monitor::type_id::create("mon", this);
    mon.vif = cfg.vif;
    if (get_is_active() == UVM_ACTIVE) begin
      sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
      drv = incr_driver::type_id::create("drv", this);
      drv.vif = cfg.vif;
    end
  endfunction
  function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

class incr_predictor extends uvm_subscriber #(incr_item);
  `uvm_component_utils(incr_predictor)
  uvm_analysis_port #(incr_result) ap;
  int predicted;
  function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
  function void write(incr_item t);
    incr_result e = incr_result::type_id::create("e");
    e.y = t.x + 1;
    predicted++;
    ap.write(e);
  endfunction
endclass

class incr_comparator extends uvm_component;
  `uvm_component_utils(incr_comparator)
  uvm_analysis_export #(incr_result) before_export, after_export;   // predicted, actual
  uvm_tlm_analysis_fifo #(incr_result) before_fifo, after_fifo;
  int matched, mismatched;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    before_export = new("before_export", this);
    after_export  = new("after_export", this);
    before_fifo   = new("before_fifo", this);
    after_fifo    = new("after_fifo", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    before_export.connect(before_fifo.analysis_export);
    after_export.connect(after_fifo.analysis_export);
  endfunction
  task run_phase(uvm_phase phase);
    incr_result e, a;
    forever begin
      before_fifo.get(e);
      after_fifo.get(a);
      if (e.compare(a)) begin
        matched++;
      end
      else begin
        mismatched++;
        `uvm_error("CMP", $sformatf("%s, predicted %s", a.convert2string(), e.convert2string()))
      end
    end
  endtask
  function void check_phase(uvm_phase phase);
    if (after_fifo.used() != 0)
      `uvm_error("CMP", $sformatf("%0d result(s) arrived with no prediction", after_fifo.used()))
  endfunction
endclass

class incr_env_cfg extends uvm_object;
  `uvm_object_utils(incr_env_cfg)
  incr_agent_cfg agt;
  time           max_latency = 20ns;        // design: a result is always 20 ns after its input
  time           drain_limit = 100ns;       // test policy: how long owed work may take after the test drops
  function new(string name = "incr_env_cfg");
    super.new(name);
    agt = incr_agent_cfg::type_id::create({name, "_agt"});
  endfunction
endclass

class incr_env extends uvm_env;
  `uvm_component_utils(incr_env)
  incr_env_cfg    cfg;
  incr_agent      agt;
  incr_predictor  pred;
  incr_comparator cmp;
  bit             draining;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(incr_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", {get_full_name(), ": no incr_env_cfg"})
    uvm_config_db #(incr_agent_cfg)::set(this, "agt", "cfg", cfg.agt);
    agt  = incr_agent::type_id::create("agt", this);
    pred = incr_predictor::type_id::create("pred", this);
    cmp  = incr_comparator::type_id::create("cmp", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    agt.mon.in_ap.connect(pred.analysis_export);
    pred.ap.connect(cmp.before_export);
    agt.mon.out_ap.connect(cmp.after_export);
  endfunction
  // Hold the run phase until this block's predictions have all met results, then stay for one
  // quiet window. A prediction that arrives inside the window starts the wait again. The whole
  // drain gives up at drain_limit, so a lost result still reaches check_phase.
  function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run" || draining) return;
    draining = 1;
    phase.raise_objection(this, "draining");
    fork begin
      bit drained = 0;
      fork
        begin
          int seen;
          do begin
            wait (cmp.matched + cmp.mismatched == pred.predicted);
            seen = pred.predicted;
            #(cfg.max_latency);                // quiet window: the design's latency
          end while (pred.predicted != seen);
          drained = 1;
        end
        #(cfg.drain_limit);                    // failure deadline: this test's policy
      join_any
      disable fork;
      phase.drop_objection(this, "draining");
    end join_none
  endfunction
  function void check_phase(uvm_phase phase);
    if (cmp.matched + cmp.mismatched != pred.predicted)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", get_name(),
                                 pred.predicted - cmp.matched - cmp.mismatched))
  endfunction
endclass

`uvm_analysis_imp_decl(_a_out)
`uvm_analysis_imp_decl(_b_in)

class sub_env_cfg extends uvm_object;
  `uvm_object_utils(sub_env_cfg)
  incr_env_cfg blk_a, blk_b;
  function new(string name = "sub_env_cfg");
    super.new(name);
    blk_a = incr_env_cfg::type_id::create("blk_a_cfg");
    blk_b = incr_env_cfg::type_id::create("blk_b_cfg");
  endfunction
endclass

class sub_env extends uvm_env;
  `uvm_component_utils(sub_env)
  sub_env_cfg cfg;
  incr_env    blk_a, blk_b;
  // The link check: block A's observed results against block B's observed inputs, in order.
  uvm_analysis_imp_a_out #(incr_result, sub_env) a_out_imp;
  uvm_analysis_imp_b_in  #(incr_item, sub_env)   b_in_imp;
  bit [7:0] a_shown[$], b_taken[$];        // whichever side arrives first waits here
  int       link_matched, link_mismatched;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    a_out_imp = new("a_out_imp", this);
    b_in_imp  = new("b_in_imp", this);
  endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(sub_env_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no sub_env_cfg")
    // Each block env gets its part, at a path relative to this env.
    uvm_config_db #(incr_env_cfg)::set(this, "blk_a", "cfg", cfg.blk_a);
    uvm_config_db #(incr_env_cfg)::set(this, "blk_b", "cfg", cfg.blk_b);
    blk_a = incr_env::type_id::create("blk_a", this);
    blk_b = incr_env::type_id::create("blk_b", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    blk_a.agt.mon.out_ap.connect(a_out_imp);
    blk_b.agt.mon.in_ap.connect(b_in_imp);
  endfunction
  // Both monitors sample the same edge, in either order, so the two streams are paired
  // symmetrically: whichever value arrives first waits for its partner.
  function void link(bit [7:0] shown, bit [7:0] taken);
    if (shown == taken) link_matched++;
    else begin
      link_mismatched++;
      `uvm_error("LINK", $sformatf("block A showed y=%0d, block B took x=%0d", shown, taken))
    end
  endfunction
  function void write_a_out(incr_result r);
    if (b_taken.size() != 0) link(r.y, b_taken.pop_front());
    else a_shown.push_back(r.y);
  endfunction
  function void write_b_in(incr_item t);
    if (a_shown.size() != 0) link(a_shown.pop_front(), t.x);
    else b_taken.push_back(t.x);
  endfunction
  // Every result block A showed must have reached block B, and B took nothing else.
  function void check_phase(uvm_phase phase);
    if (a_shown.size() != 0)
      `uvm_error("LINK", $sformatf("%0d result(s) of block A never reached block B", a_shown.size()))
    if (b_taken.size() != 0)
      `uvm_error("LINK", $sformatf("block B took %0d input(s) that block A never showed", b_taken.size()))
  endfunction
endclass

class sub_test_base extends uvm_test;
  `uvm_component_utils(sub_test_base)
  sub_env     sub;
  sub_env_cfg cfg;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual incr_if ext_vif, mid_vif;
    cfg = sub_env_cfg::type_id::create("cfg");
    if (!uvm_config_db #(virtual incr_if)::get(this, "", "ext_vif", ext_vif) ||
        !uvm_config_db #(virtual incr_if)::get(this, "", "mid_vif", mid_vif))
      `uvm_fatal("NOVIF", "a subsystem interface is missing")
    cfg.blk_a.agt.vif = ext_vif;
    cfg.blk_b.agt.vif = mid_vif;
    cfg.blk_b.agt.is_active = UVM_PASSIVE;     // block A drives block B's input now
    uvm_config_db #(sub_env_cfg)::set(this, "sub", "cfg", cfg);
    sub = sub_env::type_id::create("sub", this);
  endfunction
endclass

// Stimulus enters only through block A. Each block env holds the phase until it has drained.
class sub_smoke_test extends sub_test_base;
  `uvm_component_utils(sub_smoke_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    incr_seq seq = incr_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sub.blk_a.agt.sqr);
    phase.drop_objection(this);
  endtask
endclass

class lost_test extends sub_smoke_test;
  `uvm_component_utils(lost_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void report_phase(uvm_phase phase);
    $display("run ended at %0d ns; block B owed %0d result(s)", $time,
             sub.blk_b.pred.predicted - sub.blk_b.cmp.matched - sub.blk_b.cmp.mismatched);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  incr_if  ext (clk);
  incr_sub #(.LOSE_B(1)) dut (ext);      // fault switch: block B loses its third result
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "ext_vif", ext);
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top", "mid_vif", dut.mid);  // the internal bus
    run_test("lost_test");
  end
endmodule
ConceptIn another subsystem, the blocks are connected by plain wires in RTL you may not edit. How does block B's passive monitor reach that internal bus?

Remember

  • Vertical reuse moves a block environment into a larger bench unchanged; where its bus becomes internal, its agent turns passive and its monitor still feeds the same checks.
  • Configuration objects nest: the test fills the tree once, and each layer sets its children's part at a path relative to itself.
  • A passive monitor reaches an internal bus through a hierarchical reference to an interface instance, or through an interface bound onto RTL wires.
  • Each block environment drains itself in phase_ready_to_end: a quiet window set by the design's latency that restarts on new predictions, inside a deadline set by test policy so lost work still reaches check_phase.
  • Block checks pass when they see nothing or a corrupted input, so the subsystem adds a check across blocks: every result one block shows must arrive, unchanged and in order, as the next block's input.