Part 8 · Checking & Coverage · Intermediate

FIFO In-Order Matching

Queue-based expected storage, pop_front compare, and protocols where FIFO matching is correct.

When FIFO matching is correct

FIFO matching stores expected transactions in a SystemVerilog queue ($) and pairs each actual with the oldest unmatched expected via pop_front(). It works when responses arrive in the same order predictions were made — one outstanding transaction at a time, or strict in-order completion.

The key insight: FIFO assumes temporal ordering is preserved end-to-end. If the DUT can reorder responses (AXI with multiple outstanding IDs, PCIe completion reordering), pop_front() pairs the wrong expected with the wrong actual. You get false mismatches that look like DUT bugs but are scoreboard pairing bugs.

diagram
[CHECK] when FIFO works vs fails

  FIFO CORRECT (strict in-order):
    APB single outstanding
    Simple FIFO bus (one txn at a time)
    SPI/I2C single master, single slave
    AXI with max outstanding = 1

  FIFO WRONG (reordering possible):
    AXI multiple outstanding, same ID or different IDs
    PCIe completions out of order
    Any arbiter that reorders responses
diagram
[CHECK] FIFO queue state machine

  write_exp:  exp_q.push_back(exp)      queue grows at tail
  write_act:  exp = exp_q.pop_front()   compare with oldest expected
              MATCH   match_count++
              MISMATCH  UVM_ERROR
              empty queue  UVM_ERROR (unexpected actual)

Full FIFO compare implementation

systemverilog
function void compare(bus_txn act);
  bus_txn exp;
  bus_txn snap = bus_txn::type_id::create("act_snap");
  snap.copy(act);

  if (exp_q.size() == 0) begin
    unexpected_count++;
    `uvm_error("SCB_UNEXPECTED",
      $sformatf("no pending expected for act id=%0h addr=%0h:\n%s",
        snap.id, snap.addr, snap.sprint()))
    return;
  end

  exp = exp_q.pop_front();

  if (snap.compare(exp)) begin
    match_count++;
    `uvm_info("SCB", $sformatf("MATCH id=%0h addr=%0h", snap.id, snap.addr), UVM_HIGH)
  end else begin
    mismatch_count++;
    `uvm_error("SCB_MISMATCH", $sformatf(
      "FIFO pair failed — EXP (queued first):\n%s\nACT (arrived now):\n%s",
      exp.sprint(), snap.sprint()))
  end
endfunction
  • pop_front() assumes exp_q[0] is the correct pair for this act — ordering contract.

  • Clone act before compare — monitor may reuse object during UVM_ERROR reporting.

  • sprint() both sides in mismatch — fastest debug path for field-level diffs.

  • unexpected_count tracks actuals with no queued expected.


APB walkthrough — step-by-step compare

Three back-to-back APB writes, strict ordering. Walk through queue state at each step.

diagram
[CHECK] APB 3-write FIFO walkthrough

  Initial: exp_q = []

  --- Write 1: addr=0x1000, data=0xAA ---
  ref_model predicts  write_exp  exp_q = [exp1(0x1000,0xAA)]
  dut_mon samples     write_act  pop exp1, compare  MATCH
  exp_q = []

  --- Write 2: addr=0x1004, data=0xBB ---
  write_exp  exp_q = [exp2(0x1004,0xBB)]
  write_act  pop exp2, compare  MATCH
  exp_q = []

  --- Write 3: addr=0x1008, data=0xCC ---
  write_exp  exp_q = [exp3(0x1008,0xCC)]
  dut_mon delayed (slow slave) — exp_q = [exp3] waiting
  write_act  pop exp3, compare  MATCH
  exp_q = []

Out-of-order preview — why FIFO false-mismatches

Same three writes but DUT returns responses out of order (response 2 before response 1). FIFO pairs act2 with exp1 — guaranteed false mismatch.

diagram
Predict order:  exp1(0x1000)  exp2(0x1004)  exp3(0x1008)
  exp_q after all predictions: [exp1, exp2, exp3]

  Actual arrival order: act2(0x1004) arrives FIRST

  FIFO compare:
    pop_front  exp1(0x1000) vs act2(0x1004)  FALSE MISMATCH

  Fix: ID-based matching (next lesson) — key by transaction ID
  • False mismatch message shows different addr/data — looks like DUT bug.

  • Symptom: mismatch fields are 'shifted' — act N compared to exp N-1.

  • Root cause: always check protocol ordering before choosing FIFO.


Custom compare beyond field macros

uvm_field macros give you compare() for free, but error messages can be opaque. For bring-up, field-by-field checks produce clearer UVM_ERROR text. Use both: compare() for fast pass, field checks on failure for diagnosis.

systemverilog
function bit detailed_compare(bus_txn act, bus_txn exp, output string diff);
  diff = "";
  if (act.addr != exp.addr) diff = {diff, $sformatf("addr exp=%0h act=%0h ", exp.addr, act.addr)};
  if (act.data != exp.data) diff = {diff, $sformatf("data exp=%0h act=%0h ", exp.data, act.data)};
  if (act.err  != exp.err)  diff = {diff, $sformatf("err  exp=%0d act=%0d ", exp.err, exp.err)};
  return (diff == "");
endfunction

function void compare(bus_txn act);
  // ... pop_front exp ...
  if (!act.compare(exp)) begin
    string diff;
    if (!detailed_compare(act, exp, diff))
      `uvm_error("SCB_MISMATCH", diff)
  end
endfunction
  • compare() uses field macros — requires uvm_object_utils on txn class.

  • Mask don't-care bits (e.g., user sideband) before compare in write_act.

  • Tolerance for timing-dependent fields — compare data, ignore cycle count.

Key takeaways

  • FIFO: push_back on write_exp, pop_front on compare.

  • Use only for strictly in-order protocols — APB, SPI, single-outstanding AXI.

  • False mismatches on OOO traffic mean you need ID-based matching, not DUT fix.

  • Clone act, sprint both sides on mismatch, track unexpected_count.

Common pitfalls

  • FIFO on AXI/PCIe with multiple outstanding — false mismatches from reordering.

  • compare() without cloning act — race with monitor object reuse.

  • Not draining exp_q in check_phase — silent pass with unmatched expected.

  • Pushing request transactions into exp_q — compare response to request.