Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
    • Agent Architecture
    • Reusable VIP: Active, Passive and Protocol Agents
      • Active and Passive in Delivery: What a VIP Owes Its Integrator
      • Two Passive Monitors, One Bus
      • VIP Parameterization: Widths Up Front, Behavior at Runtime
      • Protocol Agent Patterns: APB, AXI and Streaming
      • Versioning, Delivery and Release Readiness
    • Error and Fault Injection
    • Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Reusable VIP: Active, Passive and Protocol Agents
  1. Topics
  2. UVM Verification
  3. UVM Agents & Protocol VIP
  4. Reusable VIP: Active, Passive and Protocol Agents
  5. Two Passive Monitors, One Bus

Part 6 · Agents & Protocol IP · Intermediate

Two Passive Monitors, One Bus

In the two-DMA subsystem, dma0.out_agt and dma1.in_agt watch the same middle bus. Why both stay, what each feeds, and how not to double count.

On this page

    Open the topology of dma_subsys_test and count the monitors on the bus between the two DMAs. There are two: dma0.out_agt.mon and dma1.in_agt.mon, both passive, both connected by config_db to the very same interface, mid. Every word that crosses the middle is sampled twice and published twice. A reviewer flags it as waste: "delete one of them". Should you?

    The idea: two inspectors at one border

    When a truck crosses a border, the exporting country's inspector checks what leaves and the importing country's inspector checks what arrives. It is the same truck on the same road, inspected twice, and nobody calls that waste: each inspector answers to a different authority and keeps working if the other is replaced. The two monitors on the middle bus are the same. dma0.out_agt belongs to dma0's block env and feeds dma0's scoreboard with dma0's outputs. dma1.in_agt belongs to dma1's block env and feeds dma1's scoreboard with dma1's inputs. That those are the same wires is a fact about this subsystem, not about either env.

    This is vertical reuse: block envs reused unchanged inside a bigger env. Its value is that each block env stays complete and self-checking, so a failure localises at once ("dma1.sb mismatched, dma0.sb is clean: the bug is in dma1"). Deleting one monitor would mean editing a block env, or feeding one block's scoreboard from the other block's agent, and both destroy that.

    The picture

    Flow diagram: dma0's active input agent drives dut0, which drives the middle bus into dut1, which drives the output watched by dma1's passive output agent; two passive agents, dma0.out_agt and dma1.in_agt, both watch the middle bus; dma0's scoreboard takes dma0's input and output agents, dma1's scoreboard takes dma1's, and the end-to-end scoreboard takes dma0's input and dma1's output.
    The subsystem bench. Two passive monitors watch the middle bus, one per block env. The end-to-end checker uses only the chain's two ends.

    Build it

    The env: two unchanged block envs and a switch

    dma_subsys_env reads a small config object, creating defaults if the test set none. When chain_internal is set, dma1's input is fed by dma0, so the env marks dma1.in_agt passive before the block envs are built. It then builds dma0 and dma1 from the unchanged dma_env class, and the end-to-end scoreboard if asked.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // snippet: from the course DMA bench
    // Two DMAs in a chain (dma0's output feeds dma1's input), verified by reusing the
    // block env twice, unchanged, plus an end-to-end checker.
    class dma_subsys_cfg extends uvm_object;
      bit has_e2e_checker = 1;                   // the end-to-end scoreboard
      bit chain_internal  = 1;                   // dma1's input is driven by dma0, so its agent is passive
      `uvm_object_utils(dma_subsys_cfg)
      function new(string name = "dma_subsys_cfg");
        super.new(name);
      endfunction
    endclass
    
    class dma_subsys_env extends uvm_env;
      `uvm_component_utils(dma_subsys_env)
      dma_subsys_cfg     cfg;
      dma_env            dma0, dma1;             // the block env, reused as is
      dma_e2e_scoreboard e2e;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(dma_subsys_cfg)::get(this, "", "cfg", cfg))
          cfg = dma_subsys_cfg::type_id::create("cfg");   // defaults if the test set none
        if (cfg.chain_internal)
          uvm_config_db#(uvm_active_passive_enum)::set(this, "dma1.in_agt", "is_active", UVM_PASSIVE);
        dma0 = dma_env::type_id::create("dma0", this);
        dma1 = dma_env::type_id::create("dma1", this);
        if (cfg.has_e2e_checker) e2e = dma_e2e_scoreboard::type_id::create("e2e", this);
      endfunction
    
      function void connect_phase(uvm_phase phase);
        if (cfg.has_e2e_checker) begin
          dma0.apb.ap.connect(e2e.key0_export);
          dma1.apb.ap.connect(e2e.key1_export);
          dma0.in_agt.ap.connect(e2e.first_export);
          dma1.out_agt.ap.connect(e2e.last_export);
        end
      endfunction
    endclass

    Note what the end-to-end scoreboard subscribes to: dma0's input and dma1's output, plus both APB streams for the keys. It checks out = in ^ key0 ^ key1 and never looks at the middle. It is a third, independent opinion, and if it ever disagrees with two clean block scoreboards, the bug is in how the blocks were composed rather than in either block.

    The top: one interface, two paths

    The block env asks config_db for its stream interfaces at env.in_agt.* and env.out_agt.*. The subsystem top sets the same names one level deeper, and gives mid to both agents that watch it:

    design.sv
    1
    2
    3
    4
    5
    // snippet: from dma_subsys_tb.sv
    uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.dma0.in_agt.*",  "vif", s_ax);
    uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.dma0.out_agt.*", "vif", mid);
    uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.dma1.in_agt.*",  "vif", mid);
    uvm_config_db#(virtual axis_if)::set(null, "uvm_test_top.env.dma1.out_agt.*", "vif", m_ax);

    Nothing on mid is driven by the testbench: dut0 drives TDATA, TVALID and TLAST, and dut1 drives TREADY. Both monitors therefore see the real handshake between two pieces of RTL, which is precisely what passive monitoring is for.

    What makes two monitors on one bus safe

    • Each monitor publishes its own fresh object. One beat becomes two axis_item objects, one per monitor. A subscriber that stores items never shares an object with the other block. (The fresh-object rule and the sampling rules are taught in The monitor.)

    • Each monitor samples through its own clocking block, so both see the values from just before the same edge and publish in the same timestep, with identical contents.

    • Subscribers never assume a beat is reported once in the whole bench. Within one block env it is; across envs it is not. Anything that subscribes to several block envs must choose one source per bus.

    Watch it run

    dma_subsys_test with +UVM_VERBOSITY=UVM_HIGH prints every beat each stream monitor publishes. The first word through the chain:

    Diagram
    UVM_INFO dma_tb.sv(299) @ 305000: uvm_test_top.env.dma0.in_agt.mon [AXIS_MON] data=46ee4a23
    UVM_INFO dma_tb.sv(299) @ 315000: uvm_test_top.env.dma0.out_agt.mon [AXIS_MON] data=46ee6801
    UVM_INFO dma_tb.sv(299) @ 315000: uvm_test_top.env.dma1.in_agt.mon [AXIS_MON] data=46ee6801
    UVM_INFO dma_tb.sv(299) @ 325000: uvm_test_top.env.dma1.out_agt.mon [AXIS_MON] data=57ff6801
    • dma0 XORs with its key 0x00002222 (4a23 becomes 6801), and one clock later dma1 XORs with 0x11110000 (46ee becomes 57ff). The data values are random and differ by simulator and seed.

    • The two middle-bus lines have the same time and the same data, from two different monitors: one beat, two reports, each to its own block env.

    • At the end, all three checkers report 10 words matched: dma0.sb, dma1.sb and e2e.

    What breaks: the subsystem coverage that counted twenty

    A team added a subsystem-level coverage collector for "traffic between the blocks" and, wanting every bit of it, connected both dma0.out_agt.ap and dma1.in_agt.ap to the same analysis imp. Analysis ports allow that: an imp can have any number of ports connected, and a second connection never replaces the first. The collector reported twice the beats that were sent, its packet-length bins filled with lengths no test had produced (two interleaved copies of each packet, assembled into longer ones), and the team chased a phantom DUT bug for a day.

    The collector needed one source per bus, not two. The rule generalises to anything above the block envs: a subsystem-level subscriber picks exactly one monitor per physical bus, documents which, and leaves the block envs' own subscribers alone. When coverage is collected inside each block env too, give each covergroup instance its own name (set_inst_name(get_full_name())) so the merged report shows dma0.out_agt and dma1.in_agt as two views of one bus rather than one inflated count.

    Interview

    "At subsystem level, two block environments each monitor the same internal bus. Is that a problem?"

    A weak answer: "Yes, it is redundant; remove one monitor and share it." That forces an edit to a block env and couples one block's checking to the other's agent.

    A strong answer: "No, it is deliberate. Each block env is reused unchanged and stays self-checking: one monitor feeds the upstream block's output checks, the other feeds the downstream block's input predictions. A mismatch then localises to one block. The cost is two objects per beat, which is negligible. The real rule is for anything built above the block envs: an end-to-end checker uses the chain's ends, and a subsystem subscriber picks one monitor per bus, so nothing double counts. Coverage instances get distinct names so merged reports stay readable."

    Follow-ups to expect: How does dma1's input agent get its interface? (the top sets the same mid interface at both agents' paths) and What would make you merge them? (only simulation performance at very large scale, and then through configuration, never by editing a block env).

    Check yourself The subsystem config is changed to chain_internal = 0, but the top still connects dma1's input to mid. What goes wrong?

    dma1.in_agt is built active, so its driver starts driving TVALID, TDATA and TLAST on mid, wires that dut0 already drives. Two drivers fight over the middle bus, and dma1's input no longer comes from dma0. chain_internal exists precisely to keep the agent passive when its bus is driven by RTL.

    Check yourself Why does the end-to-end scoreboard not subscribe to either middle-bus monitor?

    It checks the chain as a black box, out = in ^ key0 ^ key1, so it needs only the ends. Staying off the middle makes it independent of both block scoreboards: if a block scoreboard and the e2e checker disagree, the discrepancy points at how the blocks were composed, which a checker using the middle could not reveal.

    Next: the stream agent was easy to reuse because it never needed to change shape. When the bus width or ID width differs between projects, you need VIP parameterization.

    Related topics

    • The monitor

    • Analysis connectivity patterns

    • Environment & Test

    • VIP parameterization

    Previous

    103 / 280

    Next: VIP Parameterization: Widths Up Front, Behavior at Runtime

    Up next

    Up nextVIP Parameterization: Widths Up Front, Behavior at Runtime

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Protocol Agent Patterns: APB, AXI and Streaming
    • Versioning, Delivery and Release Readiness