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. Out-of-Order Matching: A Queue per ID

Part 8 · Checking & Coverage · Intermediate

Out-of-Order Matching: A Queue per ID

Scoreboarding AXI-style responses that return out of order: why the structure is a queue per ID, what a repeated ID means, and the details that bite in real interconnects.

On this page

    A whiteboard interview: an AXI slave returns read data out of order. Write the scoreboard. You draw an associative array keyed by ARID, one expected item per ID, and look up each response by its RID. The interviewer nods and asks one more question: the master issues two reads with ID 5 before either returns. What does your scoreboard do? Your array has one slot for ID 5. The second prediction either overwrites the first or gets reported as an error. Neither is right, because two outstanding reads on one ID are perfectly legal AXI.

    The idea: one queue per ticket colour

    AXI tags every transaction with an ID. The protocol makes two promises about responses. Responses with different IDs may return in any order. Responses with the same ID return in the order the requests were issued. So order is not gone; it is kept separately per ID. The data structure that matches those two rules exactly is an associative array of queues: pending[id] is a FIFO of the predictions for that ID.

    Picture a coat check that hands out tickets in a few colours. Coats of different colours can come back in any order, but within one colour the attendant always returns the oldest coat first. You do not need a slot per colour; you need a short line per colour. A response's ID chooses the line, and order within the line chooses the partner.

    The picture

    Sequence diagram: four read requests with IDs 2, 5, 5 and 1 are predicted; responses return as ID 5, 1, 2, 5; each response is paired with the oldest pending prediction for its own ID.
    Four reads, two of them on ID 5. Different IDs return in any order; the two ID 5 reads return in issue order, so each response pops the front of its ID's queue.

    Build it: the scoreboard

    The DMA has no IDs, so this example is AXI-style: a response item with an ID, an address and data, and a scoreboard fed by two analysis ports. It was built and run on a scratch bench; the scoreboard is shown in full.

    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
    57
    58
    59
    // snippet: verified in a scratch run (Verilator + the open-source UVM library)
    class axi_rsp extends uvm_sequence_item;
      bit [3:0]  id;                              // match the DUT's ID width exactly
      bit [31:0] addr;
      bit [31:0] data;
      `uvm_object_utils(axi_rsp)
      function new(string name = "axi_rsp");
        super.new(name);
      endfunction
      function string convert2string();
        return $sformatf("id=%0h addr=%08h data=%08h", id, addr, data);
      endfunction
    endclass
    
    `uvm_analysis_imp_decl(_exp)
    `uvm_analysis_imp_decl(_act)
    
    class axi_id_scoreboard extends uvm_scoreboard;
      `uvm_component_utils(axi_id_scoreboard)
      uvm_analysis_imp_exp #(axi_rsp, axi_id_scoreboard) exp_export;
      uvm_analysis_imp_act #(axi_rsp, axi_id_scoreboard) act_export;
      axi_rsp      pending[bit [3:0]][$];          // one in-order queue per ID
      int unsigned n_match, n_mismatch;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        exp_export = new("exp_export", this);
        act_export = new("act_export", this);
      endfunction
    
      virtual function void write_exp(axi_rsp t);
        pending[t.id].push_back(t);                // same ID again: queue behind the first
      endfunction
    
      virtual function void write_act(axi_rsp t);
        axi_rsp exp;
        if (!pending.exists(t.id)) begin
          `uvm_error("SB_UNEXP", $sformatf("response with nothing outstanding: %s", t.convert2string()))
          n_mismatch++;
          return;
        end
        exp = pending[t.id].pop_front();           // the oldest outstanding for this ID
        if (pending[t.id].size() == 0) pending.delete(t.id);
        if (t.addr !== exp.addr || t.data !== exp.data) begin
          `uvm_error("SB_MISMATCH", $sformatf("expected %s, got %s", exp.convert2string(), t.convert2string()))
          n_mismatch++;
        end else n_match++;
      endfunction
    
      function void check_phase(uvm_phase phase);
        foreach (pending[id])
          `uvm_error("SB_LEFTOVER", $sformatf("id %0h: %0d response(s) never returned", id, pending[id].size()))
        if (n_match == 0) `uvm_error("SB_EMPTY", "nothing was compared")
      endfunction
    
      function void report_phase(uvm_phase phase);
        `uvm_info("SB", $sformatf("%0d matched, %0d mismatched", n_match, n_mismatch), UVM_LOW)
      endfunction
    endclass
    • Delete an ID's queue when it empties. Then pending.exists(id) means "something is outstanding on this ID", and the leftover report at the end names only IDs that really owe a response.

    • Store the item handle only because the producer publishes a fresh object per transaction. If a VIP monitor reuses its item, copy before push_back.

    • The key is the full ID width. A key narrower than the bus's ID silently merges unrelated IDs into one queue and turns legal reordering into false mismatches.

    Watch it run

    The scratch test predicts the four reads in the picture and returns them in a legal order: ID 5 (the first one), ID 1, ID 2, then ID 5 again.

    Diagram
    (scratch run: axi_id_test)
    UVM_INFO scratch_tb.sv(64) @ 50000: uvm_test_top.sb [SB] 4 matched, 0 mismatched
    UVM_ERROR :    0

    Then a broken interconnect: the two ID 5 responses come back swapped, and ID 1 never answers.

    Diagram
    (scratch run: axi_id_bug_test)
    UVM_ERROR scratch_tb.sv(52) @ 10000: uvm_test_top.sb [SB_MISMATCH] expected id=5 addr=00000200 data=cafe0200, got id=5 addr=00000300 data=cafe0300
    UVM_ERROR scratch_tb.sv(52) @ 30000: uvm_test_top.sb [SB_MISMATCH] expected id=5 addr=00000300 data=cafe0300, got id=5 addr=00000200 data=cafe0200
    UVM_ERROR scratch_tb.sv(59) @ 40000: uvm_test_top.sb [SB_LEFTOVER] id 1: 1 response(s) never returned
    UVM_INFO scratch_tb.sv(64) @ 40000: uvm_test_top.sb [SB] 1 matched, 2 mismatched
    UVM_ERROR :    3

    Both failures are real protocol violations and both are caught for the right reason. The same-ID swap is reported as two mismatches on ID 5, because within one ID order is part of the contract. The missing response is named by ID at the end. A scoreboard that searched an ID's queue for any matching address would have passed the swap, and so would have hidden a real ordering bug.

    What breaks: one slot per ID

    The interviewer's trap is a common first design, and it fails on legal traffic. The model below is plain SystemVerilog: three reads with IDs 5, 2, 5, returned legally as 2, 5, 5. A one-slot-per-ID map overwrites the first ID 5 prediction with the second, then reports a false mismatch when the first ID 5 response arrives. The queue-per-ID map matches all three.

    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
    module tb_same_id;
      int unsigned slot[int unsigned];          // one slot per ID: the trap
      int unsigned q[int unsigned][$];          // one queue per ID: the fix
      int unsigned iss_id[3]   = '{5, 2, 5};
      int unsigned iss_addr[3] = '{32'h200, 32'h100, 32'h300};
      int unsigned ret_id[3]   = '{2, 5, 5};      // legal: same-ID responses in issue order
      int unsigned ret_addr[3] = '{32'h100, 32'h200, 32'h300};
      int slot_bad = 0, q_bad = 0;
      int unsigned id;
      initial begin
        foreach (iss_id[i]) begin
          slot[iss_id[i]] = iss_addr[i];          // the second ID 5 overwrites the first
          q[iss_id[i]].push_back(iss_addr[i]);    // the second ID 5 waits behind the first
        end
        foreach (ret_id[i]) begin
          id = ret_id[i];
          if (!slot.exists(id) || slot[id] != ret_addr[i]) slot_bad++;
          else slot.delete(id);
          if (!q.exists(id) || q[id].size() == 0 || q[id].pop_front() != ret_addr[i]) q_bad++;
          else if (q[id].size() == 0) q.delete(id);
        end
        $display("one slot per ID: %0d wrong pairing(s) on legal traffic", slot_bad);
        $display("queue per ID:    %0d wrong pairing(s), %0d queue(s) left", q_bad, q.num());
        if (slot_bad == 1 && q_bad == 0 && q.num() == 0)
          $display("PASS: a queue per ID matched legal same-ID traffic that one slot per ID got wrong");
        else $display("FAIL: unexpected result");
        $finish;
      end
    endmodule

    Look at how the slot design fails. The first ID 5 response is compared against the second read's address, so the error message says the DUT returned the wrong data for address 0x300, and debugging it leads you to a DUT that did nothing wrong. The first prediction, overwritten at issue time, is simply gone: had that response really been lost, no check would ever have noticed. The alternative "fix", rejecting a second prediction on a busy ID as an error, fails differently but just as wrongly, on the first test that issues two reads with one ID. A queue per ID is not an optimisation; it is the only structure that matches the protocol's rule.

    Details that bite in real interconnects

    Situation

    What the scoreboard must do

    Reads and writes

    AXI read IDs (ARID/RID) and write IDs (AWID/BID) are separate spaces. Keep separate maps, or key on {direction, ID}.

    Several masters into one slave

    The interconnect widens the ID with port bits so responses can be routed back. Key the slave-side scoreboard on the widened ID, or translate it back to {master, ID} first.

    Multi-beat reads

    Read beats of different IDs may interleave. Assemble beats per ID until RLAST, then compare the whole burst against the front of that ID's queue.

    Same ID, different masters

    Two masters may both use ID 3. Key on {master, ID}; a bare ID would pair one master's response with another's prediction.

    Response codes

    Predict RRESP/BRESP too. A SLVERR or DECERR for an unmapped address is an expected outcome, not an excuse to skip the compare.

    Interview

    "Design a scoreboard for an AXI slave that may return responses out of order."

    Weak: "An associative array keyed by ID: store the expected transaction under its ID and look it up when the response arrives."

    Strong: "An associative array of queues keyed by the full ID, separate for reads and writes. Predictions push to the back of their ID's queue; each response pops the front of its own ID's queue, because AXI keeps same-ID responses in issue order while different IDs may reorder. An empty or missing queue is an unexpected response; a non-empty queue at the end names the IDs that never answered. For bursts I assemble beats per ID until LAST before comparing, and at an interconnect I key on master and ID."

    Follow-ups: Why not search the queue for a matching address? (it would hide same-ID reordering, which is a real bug) and How do you catch a response that never comes? (leftovers at the end, plus an age check while running; see the messy-traffic lesson).

    Check yourself A response arrives with RID = 7 and pending has no entry for 7. Name two causes, one in the DUT and one in the testbench.

    DUT: the slave or interconnect produced a response nobody asked for, or corrupted the ID. Testbench: the request monitor missed the request (for example it did not sample ARID correctly), or the scoreboard's key is narrower than the bus ID and 7 is really a different ID. Check the request monitor's log for an ARID = 7 request first: that splits the two.

    Check yourself Why does the scoreboard delete an ID's entry from pending when its queue becomes empty?

    So that the existence of a key means "something is outstanding on this ID". The unexpected-response check can then be a simple exists, and the leftover report in check_phase lists only IDs that still owe responses, rather than every ID ever used with an empty queue.

    Check yourself A team's scoreboard for an AXI-Stream DMA keys a map on TDEST and keeps one queue per TDEST. Is that the same pattern?

    Yes, if the DUT keeps order within each destination but may interleave destinations. The key is whatever the protocol says preserves order: an AXI ID, a TDEST, a flow or virtual-channel number. The rule is the same: one queue per ordering domain, and the front of the right queue is the partner.

    Next: pairing rules decide which expected item an actual one meets. When the numbers still look wrong, Debugging a Scoreboard shows how to read them.

    Related topics

    • Debugging a scoreboard

    • Scoreboards for messy traffic

    • Pipelined and out-of-order drivers

    Previous

    130 / 280

    Next: Debugging a Scoreboard: Silence, Shapes and Cascades

    Up next

    Up nextDebugging a Scoreboard: Silence, Shapes and Cascades

    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