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
    • Scoreboard Architecture: Expected vs Actual
      • Expected vs Actual: The Two-Stream Model
      • Three Streams, One Scoreboard: imp_decl and Virtual write_*
      • Env Wiring: From Block Scoreboard to End-to-End Checker
      • In-Order Matching: One Queue, Four Outcomes
      • Out-of-Order Matching: A Queue per ID
      • Debugging a Scoreboard: Silence, Shapes and Cascades
      • Scoreboards for Messy Traffic: Drops, Reorders, Splits and Merges
      • End-of-Test Checks: What Must Be Empty Before a Test Passes
    • Reference Models: Predictors, DPI-C and Built-in Comparators
    • Coverage & Closure
  • 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

‹Scoreboard Architecture: Expected vs Actual
  1. Topics
  2. UVM Verification
  3. Checking & Coverage in UVM
  4. Scoreboard Architecture: Expected vs Actual
  5. Env Wiring: From Block Scoreboard to End-to-End Checker

Part 8 · Checking & Coverage · Intermediate

Env Wiring: From Block Scoreboard to End-to-End Checker

Connecting monitors to scoreboards in connect_phase through agent ports, and adding an end-to-end checker across two chained DMAs without changing the block env.

On this page

    The DMA is going into a subsystem: two copies in a chain, dma0's output stream feeding dma1's input. The block env is reused twice, unchanged, and each copy's scoreboard still passes. Then the architect asks the question block scoreboards cannot answer: does a word that enters dma0 leave dma1 correctly? You add a checker, wire it up, and get ten errors on the first run, while both block scoreboards report 10 out of 10. Which one do you believe, and where is the bug?

    The idea: a scoreboard checks between two observation points

    A scoreboard does not check "the DUT". It checks a contract between two observation points: what goes in at one set of monitors must come out, transformed by the spec, at another. dma_scoreboard checks one DMA, from its input stream and APB bus to its output stream. An end-to-end checker picks observation points further apart, the input of dma0 and the output of dma1, and checks the whole chain's contract without looking at the middle.

    Wiring is how you choose those points. It happens in connect_phase, after every component exists, and it should go through each agent's own analysis port (agt.ap), never through its internals (agt.mon.ap). An agent's ap is its public face: the agent may later add filtering, a second monitor or a protocol checker behind it, and every env that wired to ap keeps working. Think of it as plugging into a building's front-door intercom rather than running a wire through a window to one particular flat.

    The picture

    Flowchart: inside the subsystem env, dma0's APB and input ports and dma1's APB and output ports feed the end-to-end checker; each block scoreboard keeps its own connections.
    dma_subsys_env: each block env keeps its own scoreboard (some of their connections are omitted here); the end-to-end checker taps four ports that already exist inside the block envs.

    Build it

    The block env: three connections

    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

    connect_phase has one line per stream, from each agent's ap to the matching scoreboard imp. The agent sets ap = mon.ap in its own connect_phase, a handle copy. That works only because connect_phase runs bottom-up: the agents' connect_phase finishes before the env's starts, so apb.ap is a real port by the time the env uses it. Doing the same wiring in the env's build_phase would dereference a null handle.

    The end-to-end checker: the chain's contract in one line

    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
    50
    51
    52
    // snippet: from the course DMA bench
    // Checks the chain as one: every word out of dma1 equals the word into dma0
    // XOR both keys. It needs no knowledge of the word in the middle.
    class dma_e2e_scoreboard extends uvm_scoreboard;
      `uvm_component_utils(dma_e2e_scoreboard)
      uvm_analysis_imp_key0  #(apb_item,  dma_e2e_scoreboard) key0_export;
      uvm_analysis_imp_key1  #(apb_item,  dma_e2e_scoreboard) key1_export;
      uvm_analysis_imp_first #(axis_item, dma_e2e_scoreboard) first_export;
      uvm_analysis_imp_last  #(axis_item, dma_e2e_scoreboard) last_export;
      bit [31:0]   key0, key1;
      bit [31:0]   expected[$];
      int unsigned n_match, n_mismatch;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        key0_export  = new("key0_export", this);
        key1_export  = new("key1_export", this);
        first_export = new("first_export", this);
        last_export  = new("last_export", this);
      endfunction
    
      virtual function void write_key0(apb_item t);
        if (t.write && !t.slverr && t.addr == KEY) key0 = t.data;
      endfunction
      virtual function void write_key1(apb_item t);
        if (t.write && !t.slverr && t.addr == KEY) key1 = t.data;
      endfunction
      virtual function void write_first(axis_item t);
        expected.push_back(t.data ^ key0 ^ key1);
      endfunction
      virtual function void write_last(axis_item t);
        bit [31:0] exp;
        if (expected.size() == 0) begin
          `uvm_error("E2E_UNEXP", $sformatf("word %08h out of dma1 with nothing expected", t.data))
          n_mismatch++; return;
        end
        exp = expected.pop_front();
        if (t.data !== exp) begin
          `uvm_error("E2E_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
          n_mismatch++;
        end else n_match++;
      endfunction
    
      function void check_phase(uvm_phase phase);
        if (expected.size() != 0) `uvm_error("E2E_LEFTOVER", $sformatf("%0d word(s) never left dma1", expected.size()))
        if (n_match == 0) `uvm_error("E2E_EMPTY", "nothing was compared end to end")
      endfunction
    
      function void report_phase(uvm_phase phase);
        `uvm_info("E2E", $sformatf("%0d words matched end to end, %0d mismatched", n_match, n_mismatch), UVM_LOW)
      endfunction
    endclass

    The model is t.data ^ key0 ^ key1: a word entering dma0 should leave dma1 XORed with both keys. It needs no knowledge of the word in the middle, and that is deliberate. The block scoreboards check each block; the end-to-end checker checks that the blocks were composed correctly: right order, right keys, nothing lost between them. Its structure mirrors the block scoreboard: in-order queue, !== compare, a leftover check and an empty check.

    The subsystem env: reuse, then tap

    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
    // snippet: from the course DMA bench
    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

    Two details matter. dma1.in_agt is made passive before the block envs are built, because dma0 drives that bus; its monitor still runs, which is why dma1.sb keeps working. And the checker is optional (has_e2e_checker), created and wired only when the config asks for it, so the same env serves runs that want only block checks.

    Watch it run

    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

    Three scoreboards, three independent verdicts on the same ten words. When they agree, you have checked each block and the composition. When they disagree, the pattern tells you where to look, as the next section shows.

    What breaks: the checker that was wired to the wrong DMA

    This is the opening scene, run for real on a scratch copy of the bench. One line of the subsystem env's connect_phase was copy-pasted and not edited: dma0.apb.ap.connect(e2e.key1_export), where it should say dma1. The connection is legal, so nothing complains at elaboration.

    Diagram
    (scratch run: subsystem env with one miswired connection)
    UVM_ERROR dma_tb.sv(1171) @ 325000: uvm_test_top.env.e2e [E2E_MISMATCH] expected 46ee4a23, got 57ff6801
    UVM_ERROR dma_tb.sv(1171) @ 375000: uvm_test_top.env.e2e [E2E_MISMATCH] expected 5921544c, got 4830766e
      ... eight more ...
    UVM_ERROR dma_tb.sv(1178) @ 875000: uvm_test_top.env.e2e [E2E_EMPTY] nothing was compared end to end
    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] 0 words matched end to end, 10 mismatched
    UVM_ERROR :   11

    Read the three lines at the end together. Both block scoreboards pass, so each DMA did its job and the data between them was right. Only the checker that spans them fails, on every word. That combination points at the checker, not the hardware. Then XOR one pair: 0x46ee4a23 ^ 0x57ff6801 = 0x11112222, which is dma0's key (0x00002222) XOR dma1's key (0x11110000). The checker's model used dma0's key twice, so it was missing dma1's key exactly. One debug_connected_to() on dma1.apb.ap would have shown it had no connection to the checker at all.

    This is the main reason to keep the block scoreboards when you add an integration checker: they localise a failure. Remove them to save run time and the same log reads the chain is broken, with no hint whether the bug is in dma0, dma1, the composition, or the checker. If run time matters, turn them off per run with a configuration knob, not by deleting them.

    Common pitfall

    A connection from the wrong agent instance is legal and silent at elaboration. Print each scoreboard export's sources once at bring-up (debug_connected_to() on the ports, or debug_provided_to() on the imps) and read the instance names, not just the count.

    Interview

    "You integrate two blocks that each have a passing scoreboard. How do you check the integration?"

    Weak: "Run the block scoreboards at subsystem level; if both pass, the integration is fine."

    Strong: "Block scoreboards prove each block against its own inputs, but not that the blocks were composed as intended: the order, the configuration of each, or loss between them. I reuse the block envs unchanged, make internally driven agents passive so their monitors still feed the block scoreboards, and add an end-to-end checker that taps the outermost observation points through agent ports. Its model is the composition of the block contracts. When the end-to-end checker fails and the block checks pass, I look at the composition or the checker first."

    Follow-ups: Why wire to `agt.ap` and not `agt.mon.ap`? (the agent owns what it publishes; internals can change) and What changes at chip level? (more agents go passive, internal buses are observed through bound interfaces, and end-to-end checkers span larger paths).

    Check yourself In the subsystem, dma1.sb fails on every word while dma0.sb and the end-to-end checker both pass. What is the most likely cause?

    If the chain end to end is right and dma0 is right, the words leaving dma1 are right too. A failing dma1.sb then points at dma1's own scoreboard inputs: most likely its passive input agent is watching the wrong bus or its APB stream is miswired, so its model is predicting from the wrong data or the wrong key. Check its connections before touching dma1's RTL.

    Check yourself Someone moves the env's three connect calls into the env's build_phase, right after creating the agents. What happens, and why?

    The run dies with a null-handle error. In build_phase the agents exist but have not run their own build_phase yet (build is top-down), so neither their monitors nor their ap handles exist. Connections belong in connect_phase, which runs bottom-up after every component is built.

    Check yourself Why does the end-to-end checker take the keys from both APB monitors rather than from the test's program_dma calls?

    For the same reason the block scoreboard does: the monitors show what each DMA actually received. If a programming write went to the wrong DMA or was rejected, the test's values and the DUTs' registers differ, and only the observed writes predict what the chain really does.

    Next: with the streams connected, the scoreboard must pair each output with its expected partner. In-Order Matching looks closely at the simplest pairing rule and at the field nobody compared.

    Related topics

    • In-order matching

    • Vertical reuse

    • The env and its config object

    Previous

    128 / 280

    Next: In-Order Matching: One Queue, Four Outcomes

    Up next

    Up nextIn-Order Matching: One Queue, Four Outcomes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Expected vs Actual: The Two-Stream Model
    • Out-of-Order Matching: A Queue per ID
    • Debugging a Scoreboard: Silence, Shapes and Cascades