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. Expected vs Actual: The Two-Stream Model

Part 8 · Checking & Coverage · Intermediate

Expected vs Actual: The Two-Stream Model

Where each side of a compare comes from: expected predicted from observed inputs and observed register state, actual taken from the output monitor.

On this page

    An interviewer leans back: your DMA scoreboard learns the XOR key from the APB monitor, not from the test. Good. So what happens when software writes KEY in the middle of a job? You start to answer that the monitor sees the write, so the scoreboard has the new key... and stop. The DMA's register spec says KEY is ignored while the block is busy. Your scoreboard does not know that. Seven words later it would be reporting seven mismatches against a DUT that did exactly what its spec says.

    This lesson is about the two halves of every compare: where expected comes from, where actual comes from, and why getting either one from the wrong place produces failures that are not bugs, or worse, passes that are not passes.

    The idea: mark the answer sheet against the paper actually handed out

    Actual is what the DUT did, as seen by a monitor on the interface where the DUT's result appears. Expected is what the DUT should have done, computed by a model from the DUT's inputs. A prediction is one expected item, made at the moment the input that causes it is observed. An observation point is the place on the pins where a monitor watches: in the DMA bench there are three, the APB bus, the input stream and the output stream.

    Think of an exam marker. The answer key must be worked out from the paper the student was actually handed, not from the exam setter's plan (a misprinted question changes the right answer), and certainly not from the student's own sheet. In testbench terms: expected comes from observed inputs, never from the sequence's intent; actual comes from the output monitor, never from the driver; and the two never come from the same place, or the check proves nothing.

    The picture

    Flowchart: the APB and input monitors feed a predict step that pushes onto an expected queue; the output monitor's words are compared with the oldest queued prediction; differences raise SB_MISMATCH or SB_UNEXP; items left at the end raise SB_LEFTOVER or SB_EMPTY in check_phase.
    The data flow inside dma_scoreboard. Two monitors feed the model; its predictions wait in a queue; each output word is compared with the oldest prediction; whatever is left at the end is reported in check_phase.

    Every scoreboard in this topic is a variation on this picture. Later lessons change how the queue is organised (one per ID, one per flow), what the predict step does (split, merge, drop), and what happens at the end. The arrows stay: nothing flows into the scoreboard from a sequence or a driver.

    Build it: the bench's scoreboard

    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
    53
    54
    55
    56
    // snippet: from the course DMA bench
    // Predicts every output word from the input word and the KEY register the
    // DUT was programmed with, then compares in order. The write_* methods are
    // virtual so a derived scoreboard (a factory override) can extend them.
    class dma_scoreboard extends uvm_scoreboard;
      `uvm_component_utils(dma_scoreboard)
      uvm_analysis_imp_apb #(apb_item,  dma_scoreboard) apb_export;
      uvm_analysis_imp_in  #(axis_item, dma_scoreboard) in_export;
      uvm_analysis_imp_out #(axis_item, dma_scoreboard) out_export;
    
      bit [31:0]   key;
      bit [31:0]   expected[$];
      int unsigned n_match, n_mismatch;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        apb_export = new("apb_export", this);
        in_export  = new("in_export", this);
        out_export = new("out_export", this);
      endfunction
    
      virtual function void write_apb(apb_item t);
        if (t.write && !t.slverr && t.addr == KEY) key = t.data;
      endfunction
    
      virtual function void write_in(axis_item t);
        expected.push_back(t.data ^ key);
      endfunction
    
      virtual function void write_out(axis_item t);
        bit [31:0] exp;
        if (expected.size() == 0) begin
          `uvm_error("SB_UNEXP", $sformatf("output word %08h with nothing expected", t.data))
          n_mismatch++;
          return;
        end
        exp = expected.pop_front();
        if (t.data !== exp) begin
          `uvm_error("SB_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
          n_mismatch++;
        end else begin
          n_match++;
        end
      endfunction
    
      function void check_phase(uvm_phase phase);
        if (expected.size() != 0)
          `uvm_error("SB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
        if (n_match == 0)
          `uvm_error("SB_EMPTY", "no words were compared: the test checked nothing")
      endfunction
    
      function void report_phase(uvm_phase phase);
        `uvm_info("SB", $sformatf("%0d words matched, %0d mismatched", n_match, n_mismatch), UVM_LOW)
      endfunction
    endclass

    Register state comes from the observed bus

    write_apb updates the scoreboard's copy of KEY only for a write, to the KEY address, that completed without PSLVERR. Each condition matters. A read of KEY must not change the model. A write that the DUT rejected with an error never reached the register. And because the value comes from the APB monitor, a sequence that meant to write KEY but used the wrong address leaves the model unchanged, exactly like the DUT.

    Each observed input word becomes one prediction

    write_in runs once per beat the input monitor saw, that is, once per clock where TVALID and TREADY were both high. It pushes t.data ^ key onto expected. It stores a 32-bit value, not the item handle, which also means nothing a monitor does later can change a stored prediction.

    Each observed output word is compared with the oldest prediction

    write_out pops the front of the queue and compares with !==, the four-state inequality. In this bench that buys nothing against X: the monitor copies TDATA into the item's 2-state bit [31:0] data, so an X or Z on the bus has already become 0 before the compare runs. To catch X, test $isunknown(vif.mon_cb.tdata) in the monitor before the copy, or keep the item's data as logic, and run on a four-state simulator (Verilator is two-state). An output word with nothing expected is SB_UNEXP. The two end-of-test checks in check_phase (SB_LEFTOVER, SB_EMPTY) get a lesson of their own: End-of-Test Checks.

    Why observed input, not the sequence

    dma_truncate_test makes the point with a real run. A factory override swaps the stream driver for axis_truncating_driver, which forces TLAST on beat 3 and silently drops the rest of each packet. The sequence still generated eight items. Only four ever appeared on the bus.

    Diagram
    +UVM_TESTNAME=dma_truncate_test
    UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
    UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
    UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
    UVM_ERROR :    0

    Four words in, four predictions, four matches, nothing left over. A scoreboard fed from the sequence would have held eight predictions and reported four as never delivered, blaming the DUT for words it never received. Prediction from the observed input followed the bus, so the scoreboard needed no knowledge of the fault the driver injected.

    Where expected can come from

    Source

    How it works

    Where it is taught

    Model inside the scoreboard

    The write_* methods compute expected directly, as dma_scoreboard does. Simple and fast for small transforms.

    This topic

    Separate predictor component

    A component turns observed inputs into expected items and publishes them; the scoreboard or a comparator only compares. Reusable, swappable by factory override.

    Predictor design

    C or C++ model through DPI-C

    The predictor calls an existing algorithm model instead of re-coding it in SystemVerilog.

    DPI-C models

    Golden RTL

    A trusted earlier design gets the same stimulus; its output monitor becomes expected. Rare: it needs a trusted copy and identical stimulus.

    Side note only

    Whatever the source, the inputs to the model are monitor observations. The model is the only place where the spec's rules live, which is why the next section matters.

    What breaks: the KEY write the DUT ignored

    This is the interviewer's question, run for real on a scratch copy of the bench. The test programs KEY = 0x11111111 and an 8-word job, starts the packet, and 60 ns later writes KEY = 0x22222222 while the DMA is busy. The DUT's RTL has if (!busy) key <= pwdata;, as its spec says. The bench scoreboard updates its key on every successful KEY write.

    Diagram
    (scratch run on a copy of the bench)
    UVM_INFO scratch_tb.sv(135) @ 255000: uvm_test_top [KEY] KEY rewritten in the middle of the job
    UVM_ERROR dma_tb.sv(396) @ 265000: uvm_test_top.env.sb [SB_MISMATCH] expected 5ba2c25b, got 6891f168
    UVM_ERROR dma_tb.sv(396) @ 345000: uvm_test_top.env.sb [SB_MISMATCH] expected e9eeeafc, got daddd9cf
      ... four more like these ...
    UVM_ERROR dma_tb.sv(396) @ 525000: uvm_test_top.env.sb [SB_MISMATCH] expected 8cc33683, got bff005b0
    UVM_INFO scratch_tb.sv(138) @ 555000: uvm_test_top [KEY] KEY reads back 11111111
    UVM_INFO dma_tb.sv(411) @ 695000: uvm_test_top.env.sb [SB] 1 words matched, 7 mismatched
    UVM_ERROR :    7

    Seven errors, and the DUT is right: KEY reads back 0x11111111. XOR any expected value with the value it got and you get 0x33333333, which is the old key XOR the new key. That pattern says the model used the wrong key, not the data path corrupted a word. The scoreboard was fed from the right place; what it lacked was a rule. Observation tells the model that a write happened; only the spec says whether the write took effect.

    The fix is to model the rule. A derived scoreboard tracks BUSY from the observed GO write and the observed input words, and ignores LEN and KEY writes while a job is running:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // snippet: the DUT ignores LEN and KEY writes while BUSY, so the model must too
    bit          busy;
    int unsigned len_reg, taken;
    
    virtual function void write_apb(apb_item t);
      if (!t.write || t.slverr) return;
      if (t.addr == KEY && !busy) key     = t.data;
      if (t.addr == LEN && !busy) len_reg = t.data[15:0];
      if (t.addr == CTRL && t.data[0] && !busy && len_reg != 0) begin
        busy  = 1;                                // GO: a job of len_reg words starts
        taken = 0;
      end
    endfunction
    
    virtual function void write_in(axis_item t);
      super.write_in(t);                          // predict with the key in force
      taken++;
      if (taken == len_reg || t.last) busy = 0;   // the last word, or a short packet (ERR)
    endfunction

    One boundary remains: a KEY write that completes on the same clock edge as the job's last input word. The DUT sees BUSY still high on that edge and ignores the write. The scoreboard's answer depends on which monitor's write runs first in that timestep, and the standard does not say. That is a real corner, and the right response is a directed test for it, not hope.

    The directed test for that edge

    bench/dma_busy_edge_tb.sv holds it. dma_key_edge_test runs eleven rounds. Each round starts a 4-word job and writes a new KEY one clock later than the round before, so the write sweeps across the end of the job. A 2-word job with no KEY write follows, and its words show whether the scoreboard and the DUT agree on the key. A watcher on the pins counts the rounds where the KEY write and the last word share an edge, and the test fails if none did: a pass has to mean the corner ran. Two plusargs pick the model (+SB=naive for the scoreboard above) and the callback order (+IN_FIRST delays the APB monitor's write by #0, still at the same $time, so the input word arrives first, as another simulator may order it).

    Scoreboard

    APB write first (Verilator's order)

    Input write first (+IN_FIRST)

    Busy-aware model above

    66 matched, 0 mismatched

    2 mismatched

    With the same-edge rule below

    66 matched, 0 mismatched

    66 matched, 0 mismatched

    Diagram
    +UVM_TESTNAME=dma_key_edge_test +SB=naive +IN_FIRST   (run.sh scripts/data/uvm-course/bench/dma_busy_edge_tb.sv)
    UVM_INFO dma_busy_edge_tb.sv(162) @ 1835000: uvm_test_top [EDGE] KEY write and the job's last word on the same edge
    UVM_ERROR dma_tb.sv(396) @ 1985000: uvm_test_top.env.sb [SB_MISMATCH] expected b0e824a0, got 83db1793
    UVM_ERROR dma_tb.sv(396) @ 2005000: uvm_test_top.env.sb [SB_MISMATCH] expected f4ab6d1f, got c7985e2c
    UVM_INFO dma_busy_edge_tb.sv(199) @ 4745000: uvm_test_top [EDGE] 1 round(s) hit the same edge
    UVM_INFO dma_tb.sv(411) @ 4745000: uvm_test_top.env.sb [SB] 64 words matched, 2 mismatched
    UVM_ERROR :    2

    Expected XOR actual is 0x33333333 for both words: old key XOR new key, the same signature as the story above. The busy-aware model passed on this simulator only because of the order Verilator happens to choose. The fix replaces the order with a rule: every observation in one timestep was sampled on the same edge, so a register write must see BUSY as it was before that edge. When the last word clears busy, the model remembers the time, and a write stamped with the same time still counts as a write while busy:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: from dma_scoreboard_busy_edge in bench/dma_busy_edge_tb.sv
    time busy_ended_at = -1;
    
    virtual function void end_job();             // called by write_in on the last word
      super.end_job();                           // busy = 0
      busy_ended_at = $time;
    endfunction
    
    virtual function bit busy_at_edge();         // write_apb tests this instead of busy
      return busy || ($time == busy_ended_at);
    endfunction

    The same rule covers GO and LEN, which write_apb also gates on BUSY. The lesson generalises: when two monitors sample the same edge, decide which state each observation sees, write that down in the model, and test both delivery orders on purpose.

    Interview

    "Where does your scoreboard's expected data come from?"

    Weak: "From the reference model. The sequence tells it what was sent and it computes the output."

    Strong: "From observed inputs only: the input monitor's words and the APB monitor's completed register writes, filtered the way the DUT filters them (no reads, no PSLVERR writes, and no KEY or LEN writes while busy). Actual comes from the output monitor. That way driver bugs, rejected writes and injected faults show up as the DUT really saw them, and the model is the single place the spec's rules live."

    Follow-ups: How would you know your model, not the DUT, is wrong? (look at the shape of the mismatch: a constant XOR between expected and actual points at model state) and What if the register is written through a backdoor? (the model must learn it too: from the register model's mirror, or the backdoor write must be published to it).

    Check yourself A scoreboard predicts from the sequence's items instead of the input monitor. List two DUT-independent events that would now cause false failures.

    Anything that changes what reaches the pins: a driver that drops or modifies beats (like axis_truncating_driver), a sequence killed mid-packet before every item was driven (as in dma_reset_test), or a DUT that stops accepting words when LEN is reached so the rest of the packet is never taken. In each case the sequence's items and the observed beats differ, and only the observed beats are what the DUT processed.

    Check yourself Two words mismatch, and expected XOR actual is 0x00000400 for both. Model bug or DUT bug?

    Most likely a DUT data-path bug on bit 10: a single, fixed bit flipping in otherwise correct data points at a stuck or swapped bit in the RTL, not at model state. Compare with the KEY story, where the XOR was the difference between two keys. The shape of the difference is the first clue to which side is wrong.

    Check yourself Why does write_apb ignore writes that completed with PSLVERR, and what would break if it did not?

    A write that ended with PSLVERR did not reach any register (the DMA answers PSLVERR only for unmapped addresses). If the model accepted it, a test that wrote to a wrong address with KEY's data would change the model's key but not the DUT's, and every following word would mismatch. Filtering keeps the model to what the DUT actually stored.

    Next: the scoreboard takes three different streams. Three Streams, One Scoreboard shows how one class gets three write methods, and the one keyword that decides whether a derived scoreboard's methods ever run.

    Related topics

    • Three streams, one scoreboard

    • Predictor design

    • Your first UVM testbench

    Previous

    126 / 280

    Next: Three Streams, One Scoreboard: imp_decl and Virtual write_*

    Up next

    Up nextThree Streams, One Scoreboard: imp_decl and Virtual write_*

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • 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