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
  • 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
    • Verification Architecture and Planning
      • Writing a Testplan from the Spec
      • Estimating Verification Effort
      • A Block Bench That Upstream Can Reuse
      • From Block to Subsystem to SoC
      • Configuration Strategy Across Levels
      • Passive Agents and Who Owns the Check
      • Buy vs Build VIP, and Qualifying VIP
      • Choosing the Verification Strategy per Block
    • UVM with SVA and Formal
    • Leadership and Senior Judgment
    • Interview Bank
Loading lesson

‹Verification Architecture and Planning
  1. Topics
  2. UVM Verification
  3. Senior Prep: Architecture, Leadership and Interviews
  4. Verification Architecture and Planning
  5. A Block Bench That Upstream Can Reuse

Part 13 · Senior Prep · Intermediate

A Block Bench That Upstream Can Reuse

The reuse contract a block bench owes the subsystem and chip, checked item by item against the DMA bench.

On this page

    The subsystem team takes your DMA block env on a Monday. By Tuesday they are back with questions. Why does the base test's reg_write helper only work on a sequencer called env.apb.sqr? Why is there no switch to turn the block scoreboard off? Why does the register model lock itself? Their subsystem test ended up rewriting the job-programming code from scratch, and nobody is sure the two copies agree.

    Every one of those questions comes from the real course bench. This lesson treats a block bench as a product with two customers, the block regression and the next level up, and checks the DMA bench against the contract that makes upstream reuse work.

    The idea: a block bench is a component, not a script

    A block bench that only serves the block regression is a script: it works, and it works only where it was written. A block bench that the subsystem and chip teams can instantiate without editing is a component, and it needs an interface the way any component does. That interface has a few parts: agents that can stop driving, monitors that publish everything a checker needs, checkers fed only by monitors, a configuration object instead of hard-coded decisions, stimulus written as sequences that run on any sequencer, and a register model that can become a sub-block.

    A Lego brick is the picture. It is useful in a thousand models because of its studs and tubes, the standard interface on its outside; nobody cares how it is moulded inside. A bench whose decisions are moulded inside, such as a hard-coded path or a fixed active agent, is a brick with no studs: it only works as itself.

    The picture: what the block delivers upwards

    Diagram: a block deliverables group containing agents, monitors, scoreboard, coverage, sequences, register block, assertions and config object, all feeding the subsystem and SoC environment.
    The block bench's reuse contract. Each item is something the next level consumes without opening the block env.

    Build it: the reuse contract, checked against the DMA bench

    Here is the contract with an honest verdict for each item. The good news first: the core of the bench was built for reuse, which is why dma_subsys_env could instantiate dma_env twice unchanged.

    Contract item

    The DMA bench today

    Verdict

    Agents build only a monitor when passive

    axis_agent and apb_agent gate the sequencer and driver on get_is_active()

    Met

    Monitors publish a fresh object per transfer, with everything checkers need

    All three monitors create a new item per transfer

    Met

    Scoreboard fed only by monitors

    dma_scoreboard listens to the APB, input and output monitors, never to sequences

    Met: this is why dma1.sb works off a passive agent

    No hard-coded interface paths

    Every driver and monitor gets vif relative to itself

    Met: the subsystem top sets the same names one level deeper

    A config object with the env's knobs

    dma_env sets out_agt passive directly; no has_scoreboard or has_coverage; the checking env is a subclass

    Not met

    Stimulus as sequences that take a sequencer

    reg_write, reg_read and run_job are tasks on dma_base_test, bound to env.apb.sqr

    Not met: dma_subsys_test rewrote program_dma

    Register model usable as a sub-block

    dma_reg_block::build() calls lock_model()

    Partly: works, but generated blocks leave locking to the top

    Coverage instances named distinctly

    set_inst_name with the full component name

    Met

    Protocol assertions that travel with the RTL

    None in the block bench

    Not met: see assertion VIP and bind

    No global side effects from the env

    The verdict server and timeout are installed by tests, not the env

    Met

    What "met" looks like: the env and the agent

    The env decides structure and nothing else. It makes the DUT-driven port passive, creates three agents and a scoreboard, and wires monitors to the scoreboard. Note what it does not do: it never names an interface, a test or a sequence.

    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
    // snippet: from the course DMA bench
    class dma_env extends uvm_env;
      `uvm_component_utils(dma_env)
      apb_agent      apb;
      axis_agent     in_agt, out_agt;
      dma_scoreboard sb;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // The output side is observed only: the DUT drives it.
        uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
        apb     = apb_agent::type_id::create("apb", this);
        in_agt  = axis_agent::type_id::create("in_agt", this);
        out_agt = axis_agent::type_id::create("out_agt", this);
        sb      = dma_scoreboard::type_id::create("sb", this);
      endfunction
    
      function void connect_phase(uvm_phase phase);
        apb.ap.connect(sb.apb_export);
        in_agt.ap.connect(sb.in_export);
        out_agt.ap.connect(sb.out_export);
      endfunction
    endclass

    The agent builds its monitor unconditionally and its driver only when active. That single if is what lets the same class watch the middle bus of the subsystem, where dut0 drives and nothing in the testbench may.

    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
    // snippet: from the course DMA bench
    class axis_agent extends uvm_agent;
      `uvm_component_utils(axis_agent)
      axis_sequencer sqr;
      axis_driver    drv;
      axis_monitor   mon;
      uvm_analysis_port #(axis_item) ap;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon = axis_monitor::type_id::create("mon", this);
        if (get_is_active() == UVM_ACTIVE) begin
          sqr = axis_sequencer::type_id::create("sqr", this);
          drv = axis_driver::type_id::create("drv", this);
        end
      endfunction
    
      function void connect_phase(uvm_phase phase);
        ap = mon.ap;
        if (get_is_active() == UVM_ACTIVE)
          drv.seq_item_port.connect(sqr.seq_item_export);
      endfunction
    endclass

    Fixing "not met": stimulus that runs on any sequencer

    The base test's helpers are convenient for block tests and useless upstream, because they reach for env.apb.sqr. Moving the same steps into a sequence makes them portable: the caller picks the sequencer, so the same code programs dma0 or dma1 in the subsystem, or runs on an interconnect agent's sequencer at chip level through a layering sequence. This is a proposal, not bench code:

    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
    // snippet: a portable job-programming sequence (a proposal, not in the bench)
    class dma_program_seq extends uvm_sequence #(apb_item);
      `uvm_object_utils(dma_program_seq)
      rand bit [31:0]   key;
      rand bit [15:0]   len;
      rand bit          irq_en;
    
      function new(string name = "dma_program_seq");
        super.new(name);
      endfunction
    
      task body();
        write_reg(KEY,  key);
        write_reg(LEN,  len);
        write_reg(CTRL, {30'b0, irq_en, 1'b1});     // IRQ_EN and GO
      endtask
    
      task write_reg(bit [7:0] addr, bit [31:0] data);
        apb_rw_seq s = apb_rw_seq::type_id::create("s");
        s.addr = addr; s.write = 1; s.data = data;
        s.start(m_sequencer, this);                  // child of this sequence, same sequencer
        if (s.slverr) `uvm_error("PROG", $sformatf("SLVERR writing %02h", addr))
      endtask
    endclass
    
    // Block test:      seq.start(env.apb.sqr);
    // Subsystem test:  seq.start(env.dma1.apb.sqr);

    The other two gaps have the same shape. A dma_env_cfg with has_scoreboard, has_coverage and per-agent modes replaces decisions buried in build_phase (the configuration lesson builds it). And the register block should leave lock_model() to whoever owns the root model, so the chip can compose it before anything is locked.

    Watch it run: the proof of reuse is a log you did not have to edit for

    The subsystem test runs two unmodified copies of dma_env, each with its own block scoreboard, plus the end-to-end checker:

    Diagram
    +UVM_TESTNAME=dma_subsys_test   (run.sh dma_subsys_tb.sv)
    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(1182) @ 875000: uvm_test_top.env.e2e [E2E] 10 words matched end to end, 0 mismatched
    UVM_ERROR :    0
    • Same line number, two paths. Both block scoreboards report from dma_tb.sv(411): one class, two instances, no fork of the code.

    • `dma1.sb` matched 10 words using a passive input agent on a bus the testbench never drives. That works only because the scoreboard listens to monitors.

    • What the log hides is the duplicated program_dma in the subsystem test, the cost of the one contract item the bench did not meet.

    What breaks: a component upstream extends, and methods that were not virtual

    Reuse means other people extend your classes, usually through the factory. While the bench was being built, a factory-overridden scoreboard redefined its write_* methods, and none of them ever ran (bench war story 2). The base class had declared them without virtual, so calls through a base-class handle (which is how the analysis imps call them) went to the base versions. There was no error and no warning; the override simply had no effect, and the test passed while checking with the old logic.

    For a block bench that will be reused, every method a consumer might reasonably change is a contract: write_*, predict(), driver hooks such as pre_drive, and report hooks. Declare them virtual, document which ones are meant to be overridden, and keep one test in the block regression that overrides each and proves the override runs. A reuse contract nobody exercises is a reuse contract that breaks silently.

    Interview

    "What makes a block testbench reusable at SoC level?"

    A weak answer: "Use UVM and make the agents configurable." It is true of every bench and says nothing about what was decided.

    A strong answer names the contract and a real gap: "Agents that build only a monitor when passive; monitors that publish a fresh item with everything a checker needs; scoreboards fed only by monitors, so they keep working when the stimulus side goes away; interface handles fetched relative to each component; a config object for structural choices; stimulus as sequences that take a sequencer; a register block that can be a sub-block; and assertions bound to the RTL. On our DMA bench the first four held, and the subsystem env reused the block env twice unchanged. The test helpers didn't: they were bound to env.apb.sqr, so the subsystem test duplicated the programming code. I'd move them into a sequence."

    Follow-ups: Why must the scoreboard never listen to sequences? (at chip level the sequences are gone or are C code on a CPU; monitors are all that remain). What would you not reuse from the block? (block-level stimulus and directed tests that assume the block's ports are free to drive).

    Check yourself The chip team says your block scoreboard "doesn't work" at chip level: it reports SB_EMPTY. The CPU programs the DMA and data flows. What is the most likely cause?

    The scoreboard is not receiving monitor traffic at chip level, most often because an agent's monitor is not connected or its virtual interface points at the wrong bus. Less often, the scoreboard depended on something only active mode provided (a sequence callback or driver state). Check the topology print for the passive agents' monitors, check the vif paths, and check that every scoreboard input comes from a monitor's analysis port.

    Check yourself Why is dma_program_seq started with s.start(m_sequencer, this) instead of s.start(env.apb.sqr)?

    m_sequencer is whatever sequencer the parent sequence was started on, so the child follows the caller's choice. Passing this as the parent makes it a child sequence, which also keeps it inside any lock or grab the parent holds. Naming env.apb.sqr would bind the sequence to one path and defeat the point of moving the code out of the test.

    Next: with a reusable block in hand, From Block to Subsystem to SoC composes it upwards and asks what each level must add.

    Related topics

    • The agent as the reuse unit

    • The environment and its config object

    • Designing a test library

    • VIP and IP reuse at scale

    Previous

    251 / 280

    Next: From Block to Subsystem to SoC

    Up next

    Up nextFrom Block to Subsystem to SoC

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Writing a Testplan from the Spec
    • Configuration Strategy Across Levels
    • Passive Agents and Who Owns the Check