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.
[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[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
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
endfunctionpop_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.
[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.
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 IDFalse 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.
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
endfunctioncompare() 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.