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. ID-Tagged Scoreboard

Part 9 · Principal-Level Verification · Advanced

ID-Tagged Scoreboard

One queue per ID in an associative array: in order within an ID, any order across IDs, proven with legal and illegal reorders.

On this page

    The ordering rule decides the data structure

    Protocols with transaction IDs promise a partial order. AXI is the common example: read responses with the same ID return in request order, while responses with different IDs may return in any order. Many other protocols use the same rule under other names.

    A checker must encode that partial order exactly. The Part 5 scoreboard used one queue, which encodes a total order. Here we need one queue per ID.

    Checker shape

    Legal cross-ID reorder

    Illegal within-ID swap

    One global FIFO

    False error

    Caught

    Search anywhere for a match

    Accepted

    Missed

    One FIFO per ID

    Accepted

    Caught

    The any-order search is the dangerous one. It never raises a false error, so it looks robust, but it cannot see an ordering bug at all.

    Associative array of queues

    The natural structure is T exp_q[int][$]: an associative array keyed by ID, where each entry is a queue in arrival order. An ID gets storage only when it has something outstanding, so a 16-bit ID space costs nothing until used.

    • add_expected(id, item) pushes to the back of that ID's queue.

    • check_actual(id, item) pops the front of that ID's queue and compares. A response for an ID with an empty or missing queue is its own error: the DUT answered something nobody asked for.

    • outstanding() sums the queue sizes. The bench uses it to cap traffic in flight and to prove the test drained.

    • Delete an ID's entry when its queue empties. Reading a key that does not exist returns the default value (an empty queue here), and the LRM allows a tool to warn; checking exists() first keeps the intent clear.

    The class is parameterized on the item type, so the capstone and other benches can reuse it with a struct or a packed vector. For a class-handle item, override same() to compare fields; the default === would compare handles.

    Runnable example: legal and illegal reordering

    The DUT keeps one FIFO per ID and answers one ID every other cycle. Three copies run on the same request stream:

    Instance

    Arbitration

    Expected verdict

    u_rr (MODE 0)

    Round-robin across IDs

    Clean: cross-ID reorder is legal

    u_prio (MODE 1)

    Highest non-empty ID first

    Clean: a different legal reorder

    u_bad (MODE 2)

    Round-robin, but swaps two queued ID-2 responses once

    Errors: within-ID order broken

    Each request carries its sequence number as payload, so every item is unique and a swap is visible. The bench also counts responses that were overtaken by a younger request. If that count were zero, the clean result on u_rr would prove nothing.

    ooo_responder.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
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    // Responder: one FIFO per ID, answers one ID every other cycle.
    // MODE 0: round-robin across IDs (legal reorder)
    // MODE 1: highest non-empty ID first (a different legal reorder)
    // MODE 2: like MODE 0, but swaps two queued ID-2 responses (illegal)
    module ooo_responder #(parameter int MODE = 0) (
      input  logic       clk, rst_n,
      input  logic       req_valid,
      input  logic [1:0] req_id,
      input  logic [7:0] req_data,
      output logic       rsp_valid,
      output logic [1:0] rsp_id,
      output logic [7:0] rsp_data
    );
      logic [7:0] mem [4][8];
      logic [2:0] wr [4], rd [4];
      logic [3:0] cnt [4];
      logic       slot, swapped;
      logic [1:0] rr, pick;
      logic       any;
    
      always_comb begin
        any = 0; pick = 0;
        if (MODE == 1) begin
          for (int i = 0; i < 4; i++) if (cnt[i] != 0) begin any = 1; pick = 2'(i); end
        end else begin
          for (int k = 3; k >= 0; k--) begin
            automatic logic [1:0] i = rr + 2'(k);
            if (cnt[i] != 0) begin any = 1; pick = i; end
          end
        end
      end
    
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          for (int i = 0; i < 4; i++) begin wr[i] <= 0; rd[i] <= 0; cnt[i] <= 0; end
          slot <= 0; rr <= 0; swapped <= 0; rsp_valid <= 0; rsp_id <= 0; rsp_data <= 0;
        end else begin
          automatic logic pop = slot && any;
          slot      <= ~slot;
          rsp_valid <= pop;
          if (req_valid) begin
            mem[req_id][wr[req_id]] <= req_data;
            wr[req_id] <= wr[req_id] + 1;
          end
          if (pop) begin
            rsp_id   <= pick;
            rsp_data <= mem[pick][rd[pick]];
            if (MODE == 2 && pick == 2 && cnt[2] >= 2 && !swapped) begin
              rsp_data <= mem[2][rd[2] + 1];          // newer one goes first
              mem[2][rd[2] + 1] <= mem[2][rd[2]];     // older one is next
              swapped <= 1;
            end
            rd[pick] <= rd[pick] + 1;
            rr <= pick + 1;
          end
          for (int i = 0; i < 4; i++)
            cnt[i] <= cnt[i] + 4'(req_valid && req_id == 2'(i)) - 4'(pop && pick == 2'(i));
        end
      end
    endmodule
    
    // In order within an ID, any order across IDs.
    class id_scoreboard #(type T = logic [7:0]);
      string name;
      protected T exp_q[int][$];      // ID -> expected items, oldest first
      int n_added, n_matched, n_errors;
    
      function new(string name);
        this.name = name;
      endfunction
    
      function void add_expected(int id, T item);
        exp_q[id].push_back(item);
        n_added++;
      endfunction
    
      // Override for class-handle items: === would compare handles.
      virtual function bit same(T a, T b);
        return a === b;
      endfunction
    
      function void check_actual(int id, T item);
        T exp;
        if (!exp_q.exists(id) || exp_q[id].size() == 0) begin
          n_errors++;
          $display("%s: UNEXPECTED response for ID %0d with nothing outstanding (got %h)", name, id, item);
          return;
        end
        exp = exp_q[id].pop_front();
        if (exp_q[id].size() == 0) exp_q.delete(id);
        if (!same(item, exp)) begin
          n_errors++;
          $display("%s: MISMATCH ID %0d expected %h got %h", name, id, exp, item);
        end else n_matched++;
      endfunction
    
      function int outstanding();
        int n = 0;
        foreach (exp_q[id]) n += exp_q[id].size();
        return n;
      endfunction
    
      function void report();
        foreach (exp_q[id])
          $display("%s: ID %0d still has %0d outstanding", name, id, exp_q[id].size());
      endfunction
    endclass
    

    The bench passes with 19 cross-ID reorders accepted and two mismatches on sb_bad (MISMATCH ID 2 expected 03 got 04, then the reverse). Two changes make it fail, and both were run:

    • Collapse the key: force every ID to key 0 in the scoreboard. It becomes a global FIFO and sb_rr reports false errors.

    • Match anywhere: search the whole ID queue for the item instead of comparing the head. sb_bad then reports nothing and the bench fails on the missed swap.

    This is the checker-checks-checker pattern: the bench asserts that the bad instance is flagged, so a scoreboard that silently stopped checking order cannot pass. In UVM the same class sits inside a uvm_scoreboard fed by analysis exports; the standard library has in-order comparators but no out-of-order one.

    Common pitfall

    Keying on the wrong field. If the DUT legally remaps IDs (for example, an interconnect that prepends a port number), key on the ID the ordering rule applies to at that interface, and predict the remapped ID. Keying on the original master ID merges or splits ordering domains.

    Practical details

    • Cap traffic in flight from the scoreboard's own count, as the bench does with outstanding() >= 6. Then the stimulus cannot overrun the DUT and the cap is visible in one place.

    • Burst on one ID on purpose. With random IDs and a fast responder, the per-ID queues rarely held two items, and the within-ID bug was not reachable. Bursts of one to three requests on the same ID fixed that.

    • Make payloads unique (a sequence number or a tag field) when the data path allows it. Identical payloads hide swaps.

    Interview

    Question: "Your scoreboard for an out-of-order bus never fails. How do you know it checks ordering?" Answer: run a design or mutant that swaps two responses within one ID and confirm the scoreboard fails, and run a legal cross-ID reorder and confirm it passes. Also show a counter proving the traffic actually reordered.

    Check yourself Why does the bench require reordered > 0?

    Without real reordering, a global-FIFO scoreboard would also pass on u_rr. The counter proves the stimulus exercised the case the per-ID structure exists for.

    Check yourself A response arrives for an ID whose queue does not exist. Is that a mismatch, a drop or something else?

    Something else: an unexpected response (the DUT answered nothing, or answered twice). Report it separately from data mismatches, because it points at duplication or a corrupted ID rather than wrong data.

    Related topics

    • Checking Out-of-Order Traffic (topic)

    • Next: Multi-Stream Scoreboard

    • Scoreboard Architecture (Part 5)

    • Associative Arrays

    Previous

    237 / 259

    Next: Multi-Stream Scoreboard

    Up next

    Up nextMulti-Stream Scoreboard

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Drain and Leak Accounting
    • Memory-Model Checker