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
  • Environment & Test Classes11
    • Environment Architecture
      • The Environment and Its Config Object
      • Vertical Reuse: From Block to Subsystem
      • Integrating Third-Party VIP
      • Where the Virtual Sequencer Lives (and Whether You Need One)
    • Tests: Base Classes, Selection and Startup
  • 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

‹Environment Architecture
  1. Topics
  2. UVM Verification
  3. Environment & Test Classes
  4. Environment Architecture
  5. Vertical Reuse: From Block to Subsystem

Part 7 · Environment & Tests · Intermediate

Vertical Reuse: From Block to Subsystem

The two-DMA bench: dma_subsys_env reusing dma_env twice unchanged, dma1.in_agt made passive, the same virtual-interface names one level deeper in the subsystem top, two monitors on one bus, block scoreboards kept plus an end-to-end checker, and what changes again at SoC level.

On this page

    The chip has two DMAs in a chain: the output stream of one feeds the input stream of the other. The block bench took weeks to build and is trusted. The subsystem deadline is in ten days. Writing a new environment would mean new agents, a new scoreboard and new bugs. The bench instead builds its subsystem environment from two copies of dma_env, without editing a line of it. What had to be true of the block env for that to work, and what still had to change?

    The idea: a block env is a module you stack, not a script you copy

    Vertical reuse means using a verification component written for one level (a block) inside the environment of the next level up (a subsystem, then the chip). The block env becomes a sub-env. Three things change around it, and none of them is inside it. Agents on buses that another DUT now drives become passive: they keep monitoring, and stop driving. The virtual interfaces come from a different top. And a new checker covers what no block checker can see, the behaviour of the chain as a whole.

    Shipping containers work this way. A container is packed and sealed once, then stacked on any ship. The ship adds lashing between containers and its own manifest; nobody opens the containers to re-pack them for each voyage. The block env is the container, the subsystem env is the ship, and the end-to-end checker is the manifest.

    The picture: two envs, one shared bus

    Diagram: s_ax into dut0, the mid bus from dut0 to dut1, m_ax out of dut1. dma0's active input agent drives s_ax; dma0's passive output agent and dma1's passive input agent both watch the mid bus; dma1's passive output agent watches m_ax.
    The subsystem bench. Only dma0's input agent drives the stream; the middle bus is driven by dut0 and watched by two monitors, dma0.out_agt and dma1.in_agt. Each env's APB agent (not drawn) still programs its own DMA.

    Build it

    The subsystem top: same names, one level deeper

    The subsystem bench is a second file. It defines DMA_NO_TOP, includes dma_tb.sv for the package and interfaces, and supplies its own top with two DUTs:

    top.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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    // not-run: needs the UVM library. Paste into EDA Playground with UVM 1.2 selected.
    // dma_subsys_tb.sv - two DMAs in a chain, verified by reusing the block bench.
    // dma0's output stream feeds dma1's input. Compile with dma_dut.sv; this file
    // includes dma_tb.sv (keep both files side by side) and replaces its top module.
    // Run: +UVM_TESTNAME=dma_subsys_test
    `define DMA_NO_TOP
    `include "dma_tb.sv"
    
    module top;
      import uvm_pkg::*;
      import dma_pkg::*;
    
      logic clk = 0;
      always #5 clk = ~clk;
      rst_if rst (clk);
      wire rst_n = rst.rst_n;
      initial begin
        rst.rst_n = 1'b0;
        repeat (4) @(posedge clk);
        rst.rst_n <= 1'b1;
      end
    
      apb_if  apb0 (clk, rst_n), apb1 (clk, rst_n);
      axis_if s_ax (clk, rst_n);                   // into dma0
      axis_if mid  (clk, rst_n);                   // dma0 -> dma1, driven only by the DUTs
      axis_if m_ax (clk, rst_n);                   // out of dma1
      irq_if  irq0 (clk), irq1 (clk);
    
      initial m_ax.tready = 1'b1;
    
      dma_dut dut0 (
        .clk, .rst_n,
        .paddr(apb0.paddr), .psel(apb0.psel), .penable(apb0.penable), .pwrite(apb0.pwrite),
        .pwdata(apb0.pwdata), .prdata(apb0.prdata), .pready(apb0.pready), .pslverr(apb0.pslverr),
        .s_tdata(s_ax.tdata), .s_tvalid(s_ax.tvalid), .s_tlast(s_ax.tlast), .s_tready(s_ax.tready),
        .m_tdata(mid.tdata), .m_tvalid(mid.tvalid), .m_tlast(mid.tlast), .m_tready(mid.tready),
        .irq(irq0.irq)
      );
    
      dma_dut dut1 (
        .clk, .rst_n,
        .paddr(apb1.paddr), .psel(apb1.psel), .penable(apb1.penable), .pwrite(apb1.pwrite),
        .pwdata(apb1.pwdata), .prdata(apb1.prdata), .pready(apb1.pready), .pslverr(apb1.pslverr),
        .s_tdata(mid.tdata), .s_tvalid(mid.tvalid), .s_tlast(mid.tlast), .s_tready(mid.tready),
        .m_tdata(m_ax.tdata), .m_tvalid(m_ax.tvalid), .m_tlast(m_ax.tlast), .m_tready(m_ax.tready),
        .irq(irq1.irq)
      );
    
      // The same block-level names, one level deeper: the block env needs no change.
      initial begin
        uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.dma0.apb.*", "vif", apb0);
        uvm_config_db#(virtual apb_if)::set(null, "uvm_test_top.env.dma1.apb.*", "vif", apb1);
        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);
        uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top", "irq0_vif", irq0);
        uvm_config_db#(virtual irq_if)::set(null, "uvm_test_top", "irq1_vif", irq1);
        run_test("dma_subsys_test");
      end
    endmodule
    • The same field names, one level deeper. The block top sets vif on uvm_test_top.env.apb.*; this top sets it on uvm_test_top.env.dma0.apb.* and uvm_test_top.env.dma1.apb.*. Every driver and monitor still asks for vif from its own scope, so none of them changes.

    • One interface, two consumers. mid is set for both dma0.out_agt.* and dma1.in_agt.*. Two monitors watching one bus is normal and costs nothing; two drivers on one bus is a bug.

    • `mid` is driven only by the DUTs: dut0's output ports and dut1's TREADY. No testbench code touches it, which is why dma1.in_agt must be passive.

    • The interrupts go to the test as irq0_vif and irq1_vif, because the subsystem test waits for both. It sets no timeout, so a lost word or a missing interrupt would hang until the library's default (9200 s) instead of failing fast; a real subsystem test calls uvm_root::get().set_timeout(...) in build, as the base test lesson recommends.

    The env: two copies and a checker

    dma_subsys_env, shown in The Environment and Its Config Object, creates dma0 and dma1 with dma_env::type_id::create, makes dma1.in_agt passive when chain_internal is set, and adds dma_e2e_scoreboard. Its connect_phase feeds the checker from four places that already exist inside the block envs: both APB monitors (for the keys), dma0's input monitor (first words) and dma1's output monitor (last words). The checker's whole model is two lines:

    design.sv
    1
    2
    3
    4
    // snippet: the end-to-end model in dma_e2e_scoreboard
    virtual function void write_first(axis_item t);
      expected.push_back(t.data ^ key0 ^ key1);   // a word entering dma0, as it should leave dma1
    endfunction

    It needs no knowledge of the word in the middle. That is deliberate: the integration checker checks the chain's contract, and the block scoreboards check each block.

    Why keep both block scoreboards

    Each block scoreboard still predicts from its own input monitor and compares against its own output monitor. For dma1.sb, the input monitor is the passive dma1.in_agt watching the middle bus, which is exactly why a passive agent keeps its monitor. When the end-to-end check fails, the block results say where: if dma0.sb passes and dma1.sb fails, the bug is in dma1. Removing them to save run time throws that localization away; turn them off per run with a has_scoreboard knob instead, if you must.

    The test

    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
    43
    44
    45
    46
    47
    48
    49
    // snippet: from the course DMA bench
    // Two DMAs in a chain. Each block keeps its own scoreboard; the end-to-end
    // checker covers the chain. Run with dma_subsys_tb.sv.
    class dma_subsys_test extends uvm_test;
      `uvm_component_utils(dma_subsys_test)
      dma_subsys_env env;
      virtual irq_if irq0_vif, irq1_vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        dma_subsys_cfg cfg = dma_subsys_cfg::type_id::create("cfg");
        super.build_phase(phase);
        uvm_config_db#(dma_subsys_cfg)::set(this, "env", "cfg", cfg);
        env = dma_subsys_env::type_id::create("env", this);
        if (!uvm_config_db#(virtual irq_if)::get(this, "", "irq0_vif", irq0_vif) ||
            !uvm_config_db#(virtual irq_if)::get(this, "", "irq1_vif", irq1_vif))
          `uvm_fatal("NOVIF", "irq interfaces not set for the subsystem test")
      endfunction
    
      function void end_of_elaboration_phase(uvm_phase phase);
        uvm_root::get().print_topology();
      endfunction
    
      task program_dma(apb_sequencer sqr, bit [31:0] key, int unsigned len);
        apb_rw_seq s;
        bit [31:0] vals[3] = '{key, len, 32'h3};
        bit [7:0]  regs[3] = '{KEY, LEN, CTRL};
        foreach (regs[i]) begin
          s = apb_rw_seq::type_id::create("s");
          s.addr = regs[i]; s.write = 1; s.data = vals[i];
          s.start(sqr);
        end
      endtask
    
      task run_phase(uvm_phase phase);
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
        phase.raise_objection(this);
        program_dma(env.dma1.apb.sqr, 32'h1111_0000, 10);   // downstream first: ready before data arrives
        program_dma(env.dma0.apb.sqr, 32'h0000_2222, 10);
        if (!pkt.randomize() with { n == 10; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.dma0.in_agt.sqr);
        wait (irq0_vif.irq === 1'b1 && irq1_vif.irq === 1'b1);
        #100ns;
        phase.drop_objection(this);
      endtask
    endclass

    It extends uvm_test, not dma_base_test: its env is a different class and the base test's helpers hard-code env.apb.sqr. So it writes its own program_dma. It programs dma1 first, so that the downstream DMA is running before data reaches it, then dma0, then streams 10 words into dma0 and waits for both interrupts. Designing a Test Library shows how a shared programming sequence removes that duplication.

    What changed, and what did not

    Piece

    Block bench

    Subsystem bench

    dma_env, agents, block scoreboard

    Written here

    Unchanged, instantiated twice

    Agent modes

    out_agt passive (set by dma_env)

    Also dma1.in_agt passive (set by dma_subsys_env from its config)

    Virtual interfaces

    uvm_test_top.env.<agent>.*

    uvm_test_top.env.dma<n>.<agent>.*, from a new top

    Checking

    One block scoreboard

    Two block scoreboards plus dma_e2e_scoreboard

    Test

    dma_base_test family

    New dma_subsys_test

    What changes again at SoC level

    At chip level the same env stacks one level further, and three more things usually change. The APB agents go passive too, because the CPU or an interconnect now drives the register bus; the block scoreboards still learn KEY from the passive APB monitors, so they keep working. Internal buses have no top-level ports, so their interfaces are attached with bind to the RTL instances and published from there. And stimulus moves up: software running on the CPU, or a chip-level virtual sequence driving the few interfaces still at the chip boundary. Block scoreboards may be turned off selectively for speed, with integration checkers and assertions taking over.

    Interface

    Block

    Subsystem

    SoC

    DMA APB

    active

    active (per DMA)

    passive: the CPU programs it

    dma0 input stream

    active

    active

    passive if another IP feeds it

    middle stream

    (output: passive)

    passive, two monitors

    passive, often via bind

    dma1 output stream

    passive

    passive

    passive

    Watch it run

    Run with scripts/tools/uvm-bench/run.sh scripts/data/uvm-course/bench/dma_subsys_tb.sv and +UVM_TESTNAME=dma_subsys_test:

    Diagram
    UVM_INFO dma_tb.sv(411) @ 875000: uvm_test_top.env.dma0.sb [SB] 10 words matched, 0 mismatched
    UVM_INFO dma_tb.sv(411) @ 875000: uvm_test_top.env.dma1.sb [SB] 10 words matched, 0 mismatched
    UVM_INFO dma_tb.sv(1178) @ 875000: uvm_test_top.env.e2e [E2E] 10 words matched end to end, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0
    • 10 / 10 / 10. Each block scoreboard matched all 10 words through its own DMA, and the chain matched all 10 end to end.

    • The same line number, dma_tb.sv(411), prints for both block scoreboards: one class, two instances, told apart by their full names.

    • dma1.sb matched 10 words although its env drives nothing on its input. The passive monitor on the middle bus supplied every prediction.

    What breaks: the knob you are tempted to delete

    Imagine chain_internal did not exist and dma1.in_agt stayed active, as it is in the block env. Its stream driver starts at time 0 with vif.tvalid <= 1'b0 on the middle bus, which dut0's output port already drives. Depending on the simulator, that is an elaboration error about a variable with both a continuous and a procedural driver, or a bus whose TVALID is overwritten. Even a driver that is never given an item has done damage, because the reset value it drives is a drive.

    Teams hit this when a sub-env's agent modes are hard-coded for its block bench, or when someone "simplifies" a config by removing a knob that is 1 in every test they know about. The rule that prevents it: every agent on an interface that can be internal at a higher level must have its mode decided by configuration, and the integrating env, which knows what drives each bus, sets it.

    Interview

    "How do you reuse a block-level environment at subsystem level?"

    A weak answer: "Copy the block env into the subsystem env and remove the drivers that aren't needed." Copying forks the code; the block and subsystem versions drift apart within weeks.

    A strong answer: "Instantiate the block env unchanged as a sub-env, once per block instance. Agents on buses that another DUT now drives become passive through configuration set by the subsystem env, and their monitors keep feeding the block scoreboards. The new top sets the same virtual-interface names one level deeper, so no driver or monitor changes. Block scoreboards stay for localization, and a new end-to-end checker covers the chain from the boundary monitors. At SoC level the register agents go passive too, internal buses are bound in, and stimulus moves to software or a top virtual sequence."

    Follow-ups: Two monitors on one bus: is that a problem? (no; two drivers is). How does the passive block scoreboard learn the key at SoC level? (from the passive APB monitor, or from the register model's mirror).

    Check yourself The end-to-end checker fails on word 4. dma0.sb reports 10 matched; dma1.sb reports 9 matched and 1 mismatched. Where do you look first?

    In dma1. dma0's output, observed on the middle bus, was correct for all 10 words, and dma1's own scoreboard, predicting from that same middle bus, saw a wrong output word. That localization is what keeping the block scoreboards buys.

    Check yourself Why can dma0.out_agt and dma1.in_agt both receive the mid interface without conflict, while making either one active would be a bug?

    Monitors only sample. Any number of them can watch one interface, each publishing its own objects. An active agent's driver writes the interface's signals, and on mid those signals are already driven by dut0 (and TREADY by dut1).

    Check yourself At SoC level the APB bus is driven by the CPU. Which part of dma_env still works unchanged, and what must the chip env set?

    The APB monitor and the block scoreboard keep working: the scoreboard learns KEY from observed APB writes, whoever made them. The chip env must set the APB agent passive (and the stream agents, if other IPs drive those streams), and the chip top must publish interfaces for buses that have no chip-level ports, typically through bind.

    Next: at chip level some agents come from vendors, not from you. Integrating Third-Party VIP shows how to fit one into the bench without letting it leak.

    Related topics

    • Integrating Third-Party VIP

    • Passive monitor-only agents

    • Subsystem and SoC composition

    • Passive agents at SoC level

    Previous

    116 / 280

    Next: Integrating Third-Party VIP

    Up next

    Up nextIntegrating Third-Party VIP

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Where the Virtual Sequencer Lives (and Whether You Need One)