Part 6 · Testbench Architecture · Intermediate
Compare Discipline & Mismatch Debug
Designing txn compare() with field masks and don't-cares, first-mismatch context dumps, error thresholds, and triage flow.
Designing compare()
The compare method is where checking policy lives, so make it explicit. Not every field participates in every compare: timestamps differ by construction, debug ids are TB-internal, and some fields are don't-care in specific modes (a read transaction's write-data, an errored response's payload). Encode that as a field mask rather than scattering if-statements through the scoreboard.
class bus_txn;
typedef enum int unsigned {
F_ADDR = 'h1, F_DATA = 'h2, F_RESP = 'h4, F_LEN = 'h8
} field_e;
localparam int unsigned F_ALL = 'hF;
logic [31:0] addr;
logic [31:0] data;
logic [1:0] resp;
int unsigned len;
time observed_t; // never compared — metadata
int unsigned dbg_id; // never compared — TB bookkeeping
// mask selects which fields participate; mismatch_desc explains failures
function bit do_compare(bus_txn rhs,
int unsigned mask = F_ALL,
output string mismatch_desc);
bit ok = 1;
mismatch_desc = "";
if ((mask & F_ADDR) && addr !== rhs.addr) begin
ok = 0;
mismatch_desc = {mismatch_desc,
$sformatf(" addr(exp=0x%08h act=0x%08h)", addr, rhs.addr)};
end
if ((mask & F_DATA) && data !== rhs.data) begin
ok = 0;
mismatch_desc = {mismatch_desc,
$sformatf(" data(exp=0x%08h act=0x%08h)", data, rhs.data)};
end
if ((mask & F_RESP) && resp !== rhs.resp) begin
ok = 0;
mismatch_desc = {mismatch_desc,
$sformatf(" resp(exp=%0d act=%0d)", resp, rhs.resp)};
end
if ((mask & F_LEN) && len != rhs.len) begin
ok = 0;
mismatch_desc = {mismatch_desc,
$sformatf(" len(exp=%0d act=%0d)", len, rhs.len)};
end
return ok;
endfunction
function string convert2string();
return $sformatf("addr=0x%08h data=0x%08h resp=%0d len=%0d t=%0t",
addr, data, resp, len, observed_t);
endfunction
endclass
// Scoreboard call site: payload is don't-care on an error response
// mask = (exp.resp != 0) ? (bus_txn::F_ALL & ~bus_txn::F_DATA)
// : bus_txn::F_ALL;Use !== (4-state compare) on logic fields — an X in DUT output must fail, not silently match.
The mask is decided by the scoreboard per the spec ("data is undefined on SLVERR"), not hard-coded into the transaction.
mismatch_desc names every differing field with both values — the report writes itself.
First-mismatch reporting: dump everything, once
The first mismatch is the one closest to the bug; the hundreds after it are usually the same failure cascading. So invest the report budget at the first failure: dump both complete transactions, the differing fields, the position in the stream, and the simulation time — enough for an engineer to start debugging without re-running the simulation .
class scoreboard;
int unsigned match_count, mismatch_count;
int unsigned max_mismatches = 10; // stop the cascade
function void check_pair(bus_txn exp, bus_txn act, int unsigned mask);
string desc;
if (exp.do_compare(act, mask, desc)) begin
match_count++;
return;
end
mismatch_count++;
$error({"[SCB] MISMATCH #%0d at compare index %0d, time %0t\n",
" fields :%s\n",
" expect : %s\n",
" actual : %s"},
mismatch_count, match_count + mismatch_count, $time,
desc, exp.convert2string(), act.convert2string());
if (mismatch_count >= max_mismatches)
$fatal(1, "[SCB] mismatch threshold (%0d) reached — aborting",
max_mismatches);
endfunction
endclassWhy an error threshold
One real bug typically corrupts every subsequent compare (especially in-order queues that fall out of step) — 50,000 identical errors hide the one that matters.
$fatal after N mismatches keeps the log readable and the regression farm fast; the first report has everything needed.
Keep N configurable (plusarg) — set it higher when hunting an intermittent secondary failure.
Mismatch triage flow
MISMATCH TRIAGE FLOW
first MISMATCH report
│
▼
Which fields differ? (from mismatch_desc)
│
├─ ALL fields, queue out of step
│ └─► alignment bug: dropped/duplicated txn upstream
│ → check monitor txn counts on both sides first
│
├─ data only, addr/len match
│ └─► real datapath candidate OR model formula bug
│ → recompute by hand from the spec
│ ├─ spec agrees with model → suspect RTL → waveform at exp time
│ └─ spec agrees with DUT → fix the model
│
└─ X/Z in actual fields
└─► uninitialized RTL or monitor sampling race
→ check reset coverage and clocking-block usageInterview angle
A favorite scenario question: "Your scoreboard reports 10,000 mismatches. What do you do?" The expected answer: look only at the first one; check whether the streams fell out of alignment (compare monitor counts) before suspecting data; recompute the first failing case against the spec by hand; and only then open waveforms at the recorded time. Mentioning the mismatch threshold and the don't-care mask shows production experience.
Key takeaways
Encode compare policy as an explicit field mask — don't-cares are spec decisions, not hacks.
Use !== so X/Z propagation fails compares instead of slipping through.
Make the first mismatch report self-sufficient: both txns, differing fields, index, time.
Cap mismatches with a threshold — the first failure is the signal, the cascade is noise.
Common pitfalls
Comparing TB metadata (timestamps, debug ids) — guaranteed false mismatches.
Using == on 4-state fields — X compares as unknown and the mismatch vanishes.
Printing only "expected X got Y" without the full transactions — forces a re-run to debug.
No mismatch cap — one alignment bug produces a gigabyte log and a wedged regression.