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 Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
    • Reusable Testbench Architecture
    • Checking Out-of-Order Traffic
      • ID-Tagged Scoreboard
      • Multi-Stream Scoreboard
      • Drain and Leak Accounting
      • Memory-Model Checker
    • Assertion Depth
    • Coverage Modelling for Real Features
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Checking Out-of-Order Traffic
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Checking Out-of-Order Traffic
  5. Multi-Stream Scoreboard

Part 9 · Principal-Level Verification · Advanced

Multi-Stream Scoreboard

Routing predictor, per-stream queues and per-stream accounting for a 2-in, 3-out router, with a planted misroute.

On this page

    Streams, not ports

    A router or crossbar with N inputs and M outputs carries up to N times M independent streams. Order is normally kept within a stream (one source to one destination). Items from different sources may interleave at a shared output in any order the arbiter chooses.

    So the ordering key is the pair (source, destination). The scoreboard needs three parts:

    1. A routing predictor that computes the destination from the input item, written from the spec.

    2. One queue per stream, filled by the input monitor through the predictor.

    3. Per-stream accounting: how many items each stream sent and received, checked at the end.

    Diagram
    in0 --+                    predictor: addr -> dst
            |   +-----------+       stream key = src * 3 + dst
      in1 --+-->|  router   |--> out0      exp_q[0], exp_q[3]
                |   2 x 3   |--> out1      exp_q[1], exp_q[4]
                +-----------+--> out2      exp_q[2], exp_q[5]

    Write the predictor from the spec

    The predictor duplicates the address map. That is the point: it must come from the specification, not from the RTL. A predictor copied from the RTL agrees with every RTL bug.

    The router here maps 00-3F to output 0, 40-9F to output 1 and A0-FF to output 2. Input 0 wins a shared output; input 1 stalls with in_ready = 0. The planted bug (MISROUTE = 1) sends 90-9F to output 2.

    Runnable example: router with per-stream accounting

    router_2x3.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
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    // 2-in, 3-out router. Address map: 00-3F -> out 0, 40-9F -> out 1, A0-FF -> out 2.
    // Input 0 wins a shared output; input 1 then stalls (in1_ready = 0).
    module router_2x3 #(parameter bit MISROUTE = 0) (
      input  logic       clk, rst_n,
      input  logic       in_valid [2],
      input  logic [7:0] in_addr  [2],
      input  logic [7:0] in_data  [2],
      output logic       in_ready [2],
      output logic       out_valid[3],
      output logic [7:0] out_addr [3],
      output logic [7:0] out_data [3],
      output logic       out_src  [3]
    );
      function automatic logic [1:0] dest(logic [7:0] a);
        if (a < 8'h40) return 0;
        if (MISROUTE && a >= 8'h90 && a < 8'hA0) return 2;   // planted bug
        if (a < 8'hA0) return 1;
        return 2;
      endfunction
    
      logic [1:0] d0, d1;
      assign d0 = dest(in_addr[0]);
      assign d1 = dest(in_addr[1]);
      assign in_ready[0] = 1'b1;
      assign in_ready[1] = !(in_valid[0] && d0 == d1);
    
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          for (int k = 0; k < 3; k++) begin
            out_valid[k] <= 0; out_addr[k] <= 0; out_data[k] <= 0; out_src[k] <= 0;
          end
        end else begin
          for (int k = 0; k < 3; k++) begin
            out_valid[k] <= 0;
            if (in_valid[0] && d0 == 2'(k)) begin
              out_valid[k] <= 1; out_addr[k] <= in_addr[0]; out_data[k] <= in_data[0]; out_src[k] <= 0;
            end else if (in_valid[1] && in_ready[1] && d1 == 2'(k)) begin
              out_valid[k] <= 1; out_addr[k] <= in_addr[1]; out_data[k] <= in_data[1]; out_src[k] <= 1;
            end
          end
        end
      end
    endmodule
    
    typedef struct packed { logic [7:0] addr; logic [7:0] data; } pkt_t;
    
    // Routing predictor + one in-order queue per (source, destination) stream.
    class stream_scoreboard #(int NS = 2, int ND = 3);
      pkt_t exp_q[int][$];             // key = src * ND + dst: one queue per stream
      int sent[NS][ND], got[NS][ND];
      int n_errors;
    
      // The predictor: the same address map, written from the spec, not the RTL.
      function int route(pkt_t p);
        if (p.addr < 8'h40) return 0;
        if (p.addr < 8'hA0) return 1;
        return 2;
      endfunction
    
      function void add_input(int src, pkt_t p);
        int d = route(p);
        exp_q[src * ND + d].push_back(p);
        sent[src][d]++;
      endfunction
    
      function void check_output(int dst, int src, pkt_t p);
        pkt_t e;
        got[src][dst]++;
        if (exp_q[src * ND + dst].size() == 0) begin
          n_errors++;
          $display("ERROR out%0d: unexpected pkt from in%0d addr %h (stream in%0d->out%0d empty)",
                   dst, src, p.addr, src, dst);
          return;
        end
        e = exp_q[src * ND + dst].pop_front();
        if (p !== e) begin
          n_errors++;
          $display("ERROR in%0d->out%0d: expected %h/%h got %h/%h", src, dst, e.addr, e.data, p.addr, p.data);
        end
      endfunction
    
      function void final_report();
        for (int s = 0; s < NS; s++)
          for (int d = 0; d < ND; d++) begin
            $display("stream in%0d->out%0d: sent %0d got %0d left %0d",
                     s, d, sent[s][d], got[s][d], exp_q[s * ND + d].size());
            if (exp_q[s * ND + d].size() != 0 || sent[s][d] != got[s][d]) n_errors++;
          end
      endfunction
    endclass
    

    With MISROUTE(0) the bench passes: 120 packets, six streams in order and empty, 22 stalls on input 1. The stall count is checked, because a run with no contention never tests the arbitration path.

    Set MISROUTE(1) on the dut instance and the bench fails. The first error names the bug exactly: out2: unexpected pkt from in0 addr 9d (stream in0->out2 empty). The accounting then shows both inputs' ->out1 streams short and their ->out2 streams over by the same count (5 for in0, 4 for in1).

    Note what follows the first error: 17 in0->out1 mismatches. The stream lost an item, so every later comparison is against a stale head. Read the first error, as in Failure Triage Method. Some teams resynchronize on a mismatch by searching ahead in the stream queue and reporting the skipped items as dropped; that shortens logs but must never hide the first error.

    When the output does not carry a source tag

    This router reports out_src, so the scoreboard looks up one stream. Many real designs do not. Then an output item could belong to any stream that feeds that output, and the scoreboard must match it against the head of each candidate stream:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet: no source tag on the output. Only stream heads may match.
    function void check_untagged(int dst, pkt_t p);
      for (int s = 0; s < NS; s++) begin
        int key = s * ND + dst;
        if (exp_q.exists(key) && exp_q[key].size() != 0 && exp_q[key][0] === p) begin
          void'(exp_q[key].pop_front());
          return;
        end
      end
      n_errors++;
      $display("ERROR out%0d: %h matches no stream head", dst, p);
    endfunction

    Matching only the heads keeps in-stream order checked. If two heads can be equal, the match is ambiguous; give the stimulus unique payloads (a sequence field in the data) so it never happens.

    Common pitfall

    In Verilator 5.048 (the tool used for this course), push_back on a fixed-size array of queues declared as a class member, indexed by a variable, lost earlier entries in our tests. An associative array of queues keyed by stream number worked, and it scales better, so the example uses that. Re-test this pattern when you change tools.

    Symptom at the end

    Likely cause

    One stream short, another over by the same count

    Misroute

    One stream short, nothing over

    Drop

    One stream over, nothing short

    Duplicate or predictor bug

    Counts right, data mismatches in one stream

    Reorder or corruption within the stream

    Interview

    Question: "How do you check a crossbar where the arbiter's order is not specified?" Answer: key by (source, destination), predict the destination from the spec, compare only stream heads, and account per stream at the end. Arbitration order is then free, and its fairness is checked separately with assertions or coverage, not by the data scoreboard.

    Check yourself Why must the bench check stalls > 0?

    Without contention, input 1 never stalls, and the scoreboard never sees two sources interleaving at one output. A pass would then say nothing about the shared-output case.

    Check yourself The predictor was copied from the RTL's dest() function, including its bug. What does the scoreboard report for the misroute?

    Nothing. Predictor and DUT agree, so every item lands in the expected stream. The predictor must come from the spec.

    Related topics

    • Previous: ID-Tagged Scoreboard

    • Next: Drain and Leak Accounting

    • Reference Models and Coverage (Part 5)

    Previous

    238 / 259

    Next: Drain and Leak Accounting

    Up next

    Up nextDrain and Leak Accounting

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Memory-Model Checker