A nightly regression reports SB_EMPTY on a handful of seeds: no words were compared. On the other seeds the same test passes with hundreds of matches. The DUT waveform shows data moving on every failing seed. Somewhere between the monitor's pins and the scoreboard's compare, transactions are disappearing. Where do you look first, and in what order, so the search takes minutes instead of a day?
A transaction that the DUT really produced can be lost in exactly five places: it was never sampled, never published, published to nobody, received but never checked, or still waiting when the test ended. Each place has a cheap, specific test. Triage is just asking them in order, from the cheapest and most structural to the most dynamic, like a plumber who checks that the pipes are connected before measuring the water pressure.
Where it can be lost | Cheapest evidence | Lesson |
|---|---|---|
Elaboration refused a link | A Connection Error or BUILDERR at time 0 | |
Published to nobody |
| |
Never sampled or never published | Monitor's published count is lower than the bus activity | |
Received, never checked | Received count above checked count; FIFO | This lesson |
Still queued at end of test | Leftover or unread errors in check_phase |
The bench already prints the topology from dma_base_test::end_of_elaboration_phase. Add the connection dump for the publisher you suspect, guarded by a plusarg so it costs nothing in normal runs:
// snippet: in a test or env
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
if ($test$plusargs("TLM_DEBUG")) begin
env.out_agt.ap.debug_connected_to(); // who will this port call?
env.sb.out_export.debug_provided_to(); // who feeds this imp?
end
endfunctionGive the monitor a published count and each checker a received and a checked count, and compare them in report_phase, where every count is final. A gap between published and received means the connection or the subscriber's filter lost items; a gap between received and checked means items arrived and were never compared.
// snippet
// apb_monitor: int unsigned n_published; // n_published++ next to ap.write(it)
// scoreboard: int unsigned n_received; // n_received++ first thing in write_out
// (n_match + n_mismatch) is the checked count
function void report_phase(uvm_phase phase);
int unsigned n_checked = sb.n_match + sb.n_mismatch;
if (out_mon.n_published != sb.n_received)
`uvm_error("TLM_AUDIT", $sformatf("published %0d, received %0d", out_mon.n_published, sb.n_received))
if (sb.n_received != n_checked)
`uvm_error("TLM_AUDIT", $sformatf("received %0d, checked %0d", sb.n_received, n_checked))
endfunctionA FIFO's used() tells you how far the consumer is behind. Recording the peak is one line in the consumer loop: if (fifo.used() > max_used) max_used = fifo.used(); just before each get. The peak is the most useful number you can have when sizing a bounded FIFO, and a peak that keeps rising across a long test is the signature of a consumer that cannot keep up.
The slow FIFO scoreboard from imp_decl vs FIFOs records its peak. With an 80 ns compare and the DMA producing a word roughly every clock (10 ns) with occasional idle cycles, it reported:
UVM_INFO tlm_tb.sv(334) @ 855000: uvm_test_top.env.fsb [FSB] out_fifo peaked at 5 waiting item(s)That number is worth comparing with the first-pass estimate. The excess of arrival rate over service rate across a burst is the standard starting point:
excess = (arrival_rate - service_rate) * burst_window
depth >= excess + margin
8-word packet, about 1 word per 10 ns, 1 compare per 80 ns:
all 8 words arrive within about 80-100 ns, while only 1 compare finishes
estimate: about 6-7 waiting, measured peak: 5 (idle beats stretched the burst)The estimate was pessimistic because the input stream has random idle beats. Measure on your real traffic, take the worst peak across a regression, and add margin. For an analysis FIFO, which is unbounded, the same peak tells you how much memory a long test will use and how late a mismatch can be reported: an item behind five others waits about five service times before it is checked.
Policy | Behaviour | Use it for |
|---|---|---|
Block when full | Producer waits; nothing is lost | Flow-controlled paths between components (put/get through |
Unbounded | Producer never waits; memory grows | Analysis paths: a monitor must never be slowed (analysis FIFO) |
Drop and count | Producer continues; every drop is counted and reported | Best-effort telemetry, never correctness checks |
A monitor-to-checker path must never block (the monitor would miss bus activity) and must never drop silently (the checker would pass on partial data). That leaves unbounded, with a check that it drained.
A team's reset handler stopped and restarted the scoreboard's threads with disable fork when the DUT was reset mid-test. The disable fork was not isolated, so it also killed the scoreboard's out_fifo.get loop, which was a sibling in the same process tree. The monitors kept writing into the analysis FIFO; nobody read it. Because the FIFO was unbounded, nothing blocked and nothing errored. The scoreboard compared nothing after the reset, and its only check was "no mismatches", so the test passed. Long tests used gigabytes of memory, which was the only symptom anyone noticed.
Two habits would each have caught it on the first run. A check_phase that fails when any analysis FIFO is not empty, as dma_fifo_scoreboard does with FSB_UNREAD. And received-versus-checked counters. The root cause, disable fork killing every child of the current process, has its own fix: wrap it in fork begin ... end join so it only reaches the threads you meant.
published == received but received > checked, and the gap is always the last few items of the test. What is the likely cause?The checker's threads were still working, or items were still queued, when the run phase ended. The scoreboard must drain before the phase ends (phase_ready_to_end with an objection while busy), and check_phase should report unread items rather than letting them vanish.
uvm_tlm_fifo between two components reaching its depth of 16. Is that a bug?Not by itself. Reaching full means the producer was throttled, which is what a bounded FIFO is for. It is a bug only if the producer must never stall (a monitor, for example) or if the stall breaks a protocol timing requirement. Then fix the consumer's rate or the architecture, not just the depth.
A monitor must never block, or it misses bus activity, and a checker must never silently lose items. Unbounded is the only policy that satisfies both. The memory risk is handled by checking at end of test that the FIFO drained and by watching its peak occupancy.
Next: the TLM 1 families carry any transaction type with direct flow control. TLM 2.0 sockets standardize one memory-mapped transaction and add timing annotation, which is what you meet when a UVM bench talks to a SystemC model.