A router loses one packet in ten thousand. Every packet that does come out is perfect, so a scoreboard that checks each output as it arrives never reports anything. The test ends, the log shows no errors, and the regression marks it passed. The missing packet is still sitting in the scoreboard's queue of predictions, where nobody looked.
How do you catch something that never happened?
The obvious checks are triggered by events: an output arrives, compare it. Absence triggers nothing. Lost packets, stuck transactions, counters that never incremented and credits that leaked all leave no event to check. They show up only when someone asks, at the end, whether everything that should have happened did.
End-of-test check | Catches | DCRouter |
|---|---|---|
Every prediction matched (queues empty) | Lost, stuck or never-sent outputs | wait_drain() fails if flits are still pending |
The design drained (idle, nothing in flight) | Hangs and deadlocks | The same, with a cycle limit |
Counters equal what the testbench saw | Missed or extra counts | PKT_CNT against packets received; ERR_CNT against faults injected |
Resources returned (credits, buffers, IDs) | Leaks that only slow the design | The credit audit |
Final state matches the model | Wrong state left behind | Register read-back |
From the Part 4 runs, each of these failures was reported by an end-of-test check, never by a per-flit comparison:
Failure (from the Part 4 runs) | End check |
|---|---|
BUG_21 caught ERROR @439965: 92 flits still pending after 40000 cycles (lost or stuck) | Drain: predictions never matched |
BUG_11 caught ERROR @240345: PKT_CNT3: read 4a, expected 4c | Counter against packets seen |
BUG_14 caught ERROR @240435: ERR_CNT2: read 32, expected 4 | Counter against faults injected |
BUG_9 caught ERROR @310245: credit audit: 1 flits left output 0 into a stalled sink, expected 4 | Credit audit |
BUG_10 caught ERROR @216575: credit audit: 3 flits left output 0 into a stalled sink, expected 4 | Credit audit |
BUG_22 caught ERROR @255105: credit audit: 2 flits left output 0 into a stalled sink, expected 4 | Credit audit |
Six of the random hunt's fifteen catches came from checks that ran after the traffic stopped.
Stop stimulus, then drain: wait until everything predicted has arrived, with a cycle limit so a hang fails instead of running forever.
Return the environment to neutral: fast sinks, no stalls, so leftovers are real and not just held up by the testbench (lesson 4.8's audit empties every sink first).
Audit hidden state through the pins: credits by counting flits into a stalled sink; buffers by filling them.
Read every counter and status register and compare: with what the testbench counted, not with a constant.
Report leftovers in detail: which flow, how many, the oldest one's identity.
Practice | Purpose |
|---|---|
A check phase after the run phase (UVM's check and report phases) | End-of-test checks run in every test, by structure |
Objections or drain timers | Tests end only when the design is idle, with a limit |
Scoreboards that report leftovers per queue | Lost transactions named precisely |
A testbench (illustrative) ended each test with $finish as soon as the stimulus sequence completed, while the last few transactions were still in flight. Leftover checks were written but never reached. For months, any bug that lost one of the last transactions, or hung the design near the end of a test, passed. Ending on drain, with a limit, and running the checks after it closed the hole.
A stalled sink holds flits and credits legally; leftovers caused by the testbench's own stall would be false failures.
Lost credits (BUG_9, BUG_10, BUG_22): every flit that leaves is correct; only the number of flits an output can send is wrong.
Event-triggered checks cannot see what never happened.
At the end: drain with a limit, require every prediction matched, compare counters, audit resources.
Build end-of-test checks into the environment so every test runs them.