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. Scoreboards for Messy Traffic: Drops, Reorders, Splits and Merges

Part 8 · Checking & Coverage · Intermediate

Scoreboards for Messy Traffic: Drops, Reorders, Splits and Merges

Scoreboarding DUTs that legally lose, reorder, fragment, combine or replicate traffic: allowed-loss accounting, per-flow ordering, multicast, and latency checks that tell lost from late.

On this page

    The DMA is a polite design: one word in, one word out, same order, every time. Your next block is a packet switch. Under congestion it drops packets, and the spec says that is fine as long as it counts them. It reorders traffic between flows but not within one. It splits large frames into cells and reassembles them on the far side, and it copies multicast packets to several ports. Point the DMA's one-queue scoreboard at it and every one of those legal behaviours is a failure. Write a scoreboard that accepts them all and you may accept real bugs too. How does a scoreboard know which differences the spec allows?

    The idea: a parcel depot's reconciliation

    A parcel depot does not insist that every parcel leaves exactly as it arrived. Parcels are split into several boxes, small ones are bundled, some are routed to several addresses, and a few are returned to the sender for a stated reason. What the depot's clerk does insist on is that every parcel ends in exactly one known state: delivered, delivered late, returned with a reason, or lost. Lost is the only one that needs an investigation, and unaccounted for is never allowed.

    A scoreboard for messy traffic is that clerk. Every prediction must end as matched, dropped with a reason the DUT made visible (an allowed loss), late (arrived, but after its deadline) or lost (never arrived). Every actual item must find a prediction or be unexpected. The spec's list of permitted transformations, and nothing else, decides which transitions are legal.

    The picture: where each item can end

    Flowchart: a prediction waits with a timestamp; it becomes matched, late, an allowed loss when the DUT reports a drop, or lost if nothing arrives by the lost limit; a lost item that arrives afterwards is classified as late; an actual with no prediction is unexpected.
    Every prediction ends in exactly one state. An allowed loss needs evidence from the DUT; a lost item is remembered, so that a very late arrival is reported as late rather than as unexpected.

    Build it

    Where the loss happens decides whether you account for it

    The DMA already loses data in two situations, and they need different treatment because of where the loss happens relative to the observation points.

    • ERR truncation: loss before observation. In dma_truncate_test, the driver cuts each packet after beat 3, so beats 4 to 7 never appear on the bus. The DMA sees TLAST early, stops and reports ERR. The input monitor saw four beats, the scoreboard predicted four, and four matched. There is nothing to account for: the loss happened before the observation point. The same holds when a packet is longer than LEN: the DUT drops TREADY, and words it never accepts are never observed.

    • Reset: loss after observation. In dma_reset_test, a word the input monitor has already seen may be inside the DUT when reset hits. Its prediction is in the queue, and it will never come out. That loss must be accounted for explicitly, which is what dma_scoreboard_ra does.

    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
    // snippet: from the course DMA bench
    // A scoreboard that knows about reset: whatever the DUT held is gone, so the
    // predictions for it are dropped, and KEY is back to its reset value of 0.
    // It flushes when reset ENDS: a beat can complete on the very edge where reset
    // is asserted (the monitor reports it in that timestep), and nothing can
    // complete while reset is held, so the release edge is the race-free point.
    class dma_scoreboard_ra extends dma_scoreboard;
      `uvm_component_utils(dma_scoreboard_ra)
      virtual rst_if rst_vif;
      int unsigned flushed;
    
      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#(virtual rst_if)::get(this, "", "rst_vif", rst_vif))
          `uvm_fatal("NOVIF", "rst_if not set for the scoreboard")
      endfunction
    
      task run_phase(uvm_phase phase);
        forever begin
          @(negedge rst_vif.rst_n);
          @(posedge rst_vif.rst_n);
          flushed += expected.size();
          `uvm_info("SB_RST", $sformatf("reset: %0d prediction(s) dropped", expected.size()), UVM_LOW)
          expected.delete();
          key = 32'h0;
        end
      endtask
    endclass

    The flush is allowed loss in its simplest form: the DUT made the loss visible (reset), the scoreboard drops exactly the predictions for data the DUT held, resets its model of DUT state (KEY returns to 0), and counts what it dropped in flushed so the report can say so. In the run below the count happens to be zero; with other reset timing it is not, and the report states it either way.

    Diagram
    +UVM_TESTNAME=dma_reset_test   (+UVM_VERBOSITY=UVM_MEDIUM)
    UVM_INFO dma_tb.sv(1286) @ 280000: uvm_test_top [RST] reset asserted in the middle of job 1
    UVM_INFO dma_tb.sv(1035) @ 305000: uvm_test_top.env.sb [SB_RST] reset: 0 prediction(s) dropped
    UVM_INFO dma_tb.sv(411) @ 885000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched

    Allowed loss needs evidence, a reason and a budget

    For a switch that drops under congestion, the rule is the same with more bookkeeping. A drop is allowed only when something observable says it happened: a drop strobe on a debug port, a drop counter register, an error response. The scoreboard removes the matching prediction, counts it under a reason, and checks the count against a budget at the end. Never infer a drop from a leftover. "The queue had three items left, so the switch must have dropped them" turns every lost-packet bug into a pass.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // snippet: allowed loss, driven by an observed drop indication
    int unsigned dropped[string];                 // count per reason
    int unsigned max_drops = 0;                   // the test's budget; 0 = none allowed
    
    virtual function void write_drop(drop_item d); // from a monitor on the drop-reporting port
      if (!pending.exists(d.flow) || pending[d.flow].size() == 0) begin
        `uvm_error("SB_DROP_UNEXP", $sformatf("drop reported on flow %0d with nothing pending", d.flow))
        return;
      end
      void'(pending[d.flow].pop_front());         // the oldest packet of that flow was dropped
      dropped[d.reason]++;
    endfunction
    
    function void check_phase(uvm_phase phase);
      int unsigned total;
      foreach (dropped[r]) total += dropped[r];
      if (total > max_drops)
        `uvm_error("SB_DROPS", $sformatf("%0d drops, budget %0d", total, max_drops))
    endfunction

    Reorders: one queue per ordering domain

    A DUT that reorders between flows but not within one is handled exactly like AXI IDs: a queue per flow, keyed by whatever the spec says preserves order (a flow ID, a TDEST, a virtual channel, a priority class). The out-of-order lesson builds that structure; the only new question is what the key is, and the answer always comes from the spec's ordering rules.

    Splits and merges: transform one side so both sides match

    When a DUT fragments a frame into cells, you can compare at either level. Predict cells (the model fragments each expected frame) when the cell boundaries themselves are specified and must be checked. Reassemble actuals (the scoreboard collects cells until the last one, then compares whole frames) when only the frame content matters; fewer, larger compares are easier to debug. Merging, such as a coalescer that combines small writes, is the mirror image: accumulate expected items until the spec's merge boundary, or split the actual.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: reassemble cells per flow, then compare whole frames in order
    byte  partial[int unsigned][$];               // bytes collected so far, per flow
    
    virtual function void write_cell(cell_item c);
      foreach (c.payload[i]) partial[c.flow].push_back(c.payload[i]);
      if (!c.last) return;                        // frame not complete yet
      compare_frame(c.flow, partial[c.flow]);     // pops the front of that flow's frame queue
      partial.delete(c.flow);
    endfunction

    Leftovers now have two places to live: complete frames never delivered, and partial frames whose last cell never came. Check both at the end.

    Multicast: one prediction per destination

    A packet copied to three ports is three predictions, one in each destination port's queue, made from the observed input and the observed routing configuration. Each port's queue is checked independently, so a copy missing on one port shows up as a leftover on that port only. Do not use one prediction with a "seen on port" bit set: it hides which copy went missing, and it breaks when ports deliver at different times.

    Latency and age: when is missing really missing?

    Leftover checks find items that never arrive, but only at the end of the test. An age check finds them while the test runs, and a latency check catches items that arrive but too slowly. Both need a timestamp on every prediction. On a scratch copy of the bench, this derived scoreboard ran under the reactive slave's random backpressure:

    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
    // snippet: verified in a scratch run; extends the bench scoreboard
    class scr_scoreboard_age extends dma_scoreboard;
      `uvm_component_utils(scr_scoreboard_age)
      time born[$];                               // when each prediction was made
      time max_lat, late_limit = 150ns, lost_limit = 1us;
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      virtual function void write_in(axis_item t);
        super.write_in(t);
        born.push_back($time);
      endfunction
      virtual function void write_out(axis_item t);
        if (born.size() != 0) begin
          time lat = $time - born.pop_front();
          if (lat > max_lat) max_lat = lat;
          if (lat > late_limit)
            `uvm_warning("SB_LATE", $sformatf("word %08h took %0t, limit %0t", t.data, lat, late_limit))
        end
        super.write_out(t);
      endfunction
      task run_phase(uvm_phase phase);            // the age watchdog
        forever begin
          #10ns;
          if (born.size() != 0 && $time - born[0] > lost_limit) begin
            `uvm_error("SB_LOST", $sformatf("expected %08h waited %0t: declared lost", expected[0], $time - born[0]))
            void'(expected.pop_front());
            void'(born.pop_front());
          end
        end
      endtask
      function void report_phase(uvm_phase phase);
        super.report_phase(phase);
        `uvm_info("SB_AGE", $sformatf("worst input-to-output latency %0t", max_lat), UVM_LOW)
      endfunction
    endclass

    It works only because the bench's write_* methods are virtual, and it calls super so the base compare still runs. With the late limit tightened to 50 ns for the run, two words were flagged:

    Diagram
    (scratch run: dma_backpressure_test with scr_scoreboard_age, late limit 50 ns)
    UVM_WARNING scratch_tb.sv(164) @ 285000: uvm_test_top.env.sb [SB_LATE] word 463c5f56 took 60000, limit 50000
    UVM_WARNING scratch_tb.sv(164) @ 395000: uvm_test_top.env.sb [SB_LATE] word 8b8d9a54 took 80000, limit 50000
    UVM_INFO dma_tb.sv(411) @ 1005000: uvm_test_top.env.sb [SB] 12 words matched, 0 mismatched
    UVM_INFO scratch_tb.sv(180) @ 1005000: uvm_test_top.env.sb [SB_AGE] worst input-to-output latency 80000
    UVM_WARNING :    4
    UVM_ERROR :    0

    The worst latency under backpressure was 80 ns, eight clocks, for a design whose unstalled latency is one clock. That number is worth having even when nothing fails: it is the evidence for choosing the limits, which must come from the spec (or a performance requirement), not from the slowest run you happened to see.

    Telling dropped from late

    Two limits separate the cases. Past the late limit, an arriving item is late: report it, as a warning or an error depending on whether latency is part of the spec. Past the lost limit, the watchdog declares the item lost and raises an error. The subtle part is what happens if a "lost" item turns up afterwards. If the scoreboard simply forgot it, the arrival is reported as unexpected, and the log now has two errors (lost, then unexpected) for one slow item. Keep a short list of items declared lost; an arrival that matches one is reported as late, and the lost error stands. With in-order matching, the watchdog's pop also keeps the queue aligned, so one slow word does not cascade. The model below runs that classification.

    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
    module tb_lost_vs_late;
      // Tagged items (for example packets with a sequence number). Each prediction records
      // when it was made. Past LOST_AFTER it is declared lost and remembered, so that a very
      // late arrival is classified as late rather than as unexpected.
      localparam int unsigned LOST_AFTER = 100;
      int unsigned born[int unsigned];            // waiting: tag -> time predicted
      bit          lost[int unsigned];            // declared lost, still remembered
      int unsigned n_match, n_late, n_lost, n_unexp;
      int unsigned old_tags[$];
    
      function automatic void sweep(int unsigned now);
        old_tags.delete();
        foreach (born[t]) if (now - born[t] > LOST_AFTER) old_tags.push_back(t);
        foreach (old_tags[i]) begin
          born.delete(old_tags[i]);
          lost[old_tags[i]] = 1;
          n_lost++;
        end
      endfunction
    
      function automatic void actual(int unsigned tag, int unsigned now);
        sweep(now);
        if (born.exists(tag))      begin born.delete(tag); n_match++; end
        else if (lost.exists(tag)) begin lost.delete(tag); n_late++;  end
        else                       n_unexp++;
      endfunction
    
      initial begin
        born[1] = 0; born[2] = 10; born[3] = 20;   // three packets predicted
        actual(1, 30);                             // on time
        actual(3, 50);                             // overtakes 2: allowed between flows here
        actual(2, 160);                            // 150 after prediction: lost at the sweep, then late
        born[4] = 200;                             // never arrives
        actual(9, 210);                            // nobody predicted tag 9
        sweep(400);                                // end of test
        $display("match=%0d late=%0d lost=%0d unexpected=%0d", n_match, n_late, n_lost, n_unexp);
        if (n_match == 2 && n_late == 1 && n_lost == 2 && n_unexp == 1 && lost.num() == 1)
          $display("PASS: every item ended in one known state, and the slow item was late, not unexpected");
        else $display("FAIL: unexpected classification");
        $finish;
      end
    endmodule

    Packet 2 is counted twice by design: once as lost when the watchdog fired, and once as late when it arrived. Both are true, and both belong in the report. Packet 4 stays in lost at the end, which is the end-of-test evidence that it never came.

    What breaks: the flush that raced a beat

    The DMA's own reset accounting had a bug while the bench was being built. The first dma_scoreboard_ra flushed on the falling edge of reset, which seems obvious: reset starts, forget everything. Then a test asserted reset exactly on a clock edge where a word was completing. The output monitor sampled that beat as valid in the same timestep and reported it, but the flush had already emptied the queue, so the word arrived with nothing expected: SB_UNEXP on a perfectly correct DUT.

    The fix was to flush when reset ends. A beat can complete on the edge where reset is asserted, but nothing can complete while reset is held, so the release edge is the one point where the scoreboard knows exactly what the DUT lost. The full story, with the drivers' side, is in Reset on the fly. The lesson for any allowed-loss rule: account for the loss at a moment when the set of lost items is settled, not at the first sign of trouble.

    Interview

    "How would you scoreboard a switch that may drop packets under congestion?"

    Weak: "Allow some leftovers at the end of the test, up to the expected drop rate."

    Strong: "A drop is allowed only when the DUT makes it observable, a drop strobe, counter or status, so a monitor on that indication publishes drop items and the scoreboard removes the matching prediction and counts it per reason, against a budget the test sets. Everything else must match, per flow, in order within the flow. A timestamp on each prediction gives a latency check and an age watchdog, so a missing packet is reported when it goes missing, and one that turns up later is reported as late, not unexpected. Leftovers at the end are still errors."

    Follow-ups: What if the DUT cannot report drops? (then you cannot tell a legal drop from a bug per packet; check counts against a statistical bound and say clearly that this is weaker) and Where do the latency limits come from? (the spec or a performance requirement, informed by measured worst cases).

    Check yourself A DUT drops a packet on a CRC error at its input. Where should the scoreboard learn about it, and should the prediction ever be made?

    The input monitor sees the bad CRC too, because it is on the input pins. The cleanest model applies the spec's rule at prediction time: a packet with a bad CRC predicts "dropped" (count it as an expected drop, or make no output prediction and expect a drop indication). The rule is in the model, driven by observed input, and a DUT that forwards the bad packet then shows up as unexpected.

    Check yourself In dma_truncate_test the DMA ended a job early with ERR, yet the scoreboard needed no allowed-loss logic. Why not?

    The missing words were never on the input bus: the driver dropped them before the observation point. The input monitor saw four beats, so the scoreboard predicted four, and four came out. Loss needs explicit accounting only when it happens after an input was observed and predicted, as with words inside the DMA at reset.

    Check yourself A multicast packet to ports 1 and 3 arrives on port 1 only. With one prediction per destination, what does the scoreboard report, and when?

    Port 1's queue matches its copy. Port 3's queue still holds its prediction: the age watchdog reports it as lost once the lost limit passes, and if nothing arrives, the end-of-test check reports it as a leftover on port 3. The report names the exact copy that went missing.

    Next: every scoreboard ends with a verdict. End-of-Test Checks lists what must be empty before a test passes, and which phase owns each check.

    Related topics

    • End-of-test checks

    • Reset on the fly

    • Out-of-order matching

    Previous

    132 / 280

    Next: End-of-Test Checks: What Must Be Empty Before a Test Passes

    Up next

    Up nextEnd-of-Test Checks: What Must Be Empty Before a Test Passes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • 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