Monday morning, three red tests from the nightly run. The first says SB_EMPTY and nothing else. The second has one SB_MISMATCH. The third has nine mismatches and one SB_LEFTOVER. A new engineer opens the third, because it looks the worst, picks the last mismatch, and spends the morning in the waveform at the end of the test. The bug was at the first mismatch, and the other eight were echoes of it. A scoreboard's numbers are evidence. Which layer of the bench does each pattern point at, and where do you look first?
A doctor does not start with surgery. They read the blood test, and each abnormal value points at an organ. A scoreboard's summary works the same way. Silence (nothing compared) points at stimulus, end of test or connections, never at the DUT's data path. A constant difference between expected and actual points at model state, such as a key or a configuration bit. A single flipped bit points at a wire in the DUT. A cascade, many mismatches that start at one point and never recover, points at one lost or extra item. Only after you have read the shape do you open the waveform, at the first bad item.
dma_scoreboard keeps n_match and n_mismatch and counts an unexpected word as a mismatch. That is enough for a pass/fail verdict but not for triage. Keep one counter per outcome (match, mismatch, unexpected, leftover) and print them in one summary line, so the shape of a failure is visible without reading every error.
SB_LEFTOVER in the bench reports a count. When it fires, the next question is always which items, so print the first few at the same time:
// snippet: a check_phase that names what was left
function void check_phase(uvm_phase phase);
if (expected.size() != 0) begin
`uvm_error("SB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
foreach (expected[i]) begin
if (i == 4) break; // the first few are enough
`uvm_info("SB_LEFTOVER", $sformatf(" [%0d] expected %08h", i, expected[i]), UVM_NONE)
end
end
if (n_match == 0)
`uvm_error("SB_EMPTY", "no words were compared: the test checked nothing")
endfunctionThe bench's stream monitor already prints every beat at UVM_HIGH (ID AXIS_MON), so +UVM_VERBOSITY=UVM_HIGH shows every word each monitor saw. Add the same in the scoreboard: one UVM_HIGH line per prediction and per compare, with the queue depth. Off by default, it costs nothing; on, it lets you line up input, prediction and output for one word in a text log instead of a waveform.
A scoreboard that compared nothing was never given anything to compare. The bench's SB_EMPTY check exists to turn that into a failure, and its causes are always upstream of the compare:
Evidence | Cause | Where it is taught |
|---|---|---|
The run ends at time 0 | No objection raised before the first wait | |
Monitors print at UVM_HIGH; the scoreboard never sees them | A port with no subscribers, for example an agent | |
No traffic on the waves at all | Stimulus never started: a sequence on the wrong sequencer, firmware not loaded |
UVM_ERROR dma_tb.sv(407) @ 0: uvm_test_top.env.sb [SB_EMPTY] no words were compared: the test checked nothingThat line, at time 0, is the signature of a missing objection. The same error at 2 ms is a connection or stimulus problem. The time stamp alone splits the two.
When words mismatch, XOR the two values before opening anything else. The first-testbench lesson broke the DUT on purpose so that word 6 used ~key instead of key, and got exactly one error:
UVM_ERROR dma_tb.sv(396) @ ...: uvm_test_top.env.sb [SB_MISMATCH] expected 52c7d685, got ad38297a
UVM_INFO dma_tb.sv(411) @ ...: uvm_test_top.env.sb [SB] 7 words matched, 1 mismatched0x52c7d685 XOR 0xad38297a = 0xffffffff: every bit inverted, on one word only. That reads as "the DUT inverted something for this word", which is exactly the planted bug. Compare the two other shapes in this topic. In the KEY written while busy run, every mismatch XORed to 0x33333333, the difference between two keys: model state. In the miswired end-to-end checker, every mismatch XORed to 0x11112222, one DMA's key: a wrong connection. A transform other than XOR needs its own inverse, but the idea carries: compute what would turn expected into actual, and see whether it is constant, local or random.
In-order matching has one weakness: a single lost word shifts every later pair. Here is what a 6-word job looks like when the DUT drops word 2:
Output word | Paired with | Result |
|---|---|---|
w0 | e0 | match |
w1 | e1 | match |
w3 | e2 | SB_MISMATCH |
w4 | e3 | SB_MISMATCH |
w5 | e4 | SB_MISMATCH |
(end) | e5 still queued | SB_LEFTOVER: 1 |
The signature is mismatches that start at one point and never recover, plus leftovers equal to the number of lost items. Only the first mismatch is informative. A scoreboard can say so itself: on a mismatch, look ahead in the queue for the actual value before blaming the data.
// snippet: in write_out, after popping exp; classify a loss instead of cascading
if (t.data !== exp) begin
int idx[$] = expected.find_first_index(e) with (e == t.data);
if (idx.size() != 0) begin
`uvm_error("SB_LOST", $sformatf("%0d word(s) before %08h never came out", idx[0] + 1, t.data))
expected = expected[idx[0] + 1 : $]; // resynchronise after the gap
n_lost += idx[0] + 1;
end else begin
`uvm_error("SB_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
n_mismatch++;
end
endThe look-ahead is a debug aid with a cost: a word whose corrupted value happens to equal a later prediction is misclassified as a loss. Keep it reporting an error either way, never a silent resync, and prefer real sequence numbers when the protocol carries them.
The Monday-morning story is the common one. The third test's log had nine SB_MISMATCH lines and one leftover. The engineer chose the last mismatch because it was nearest the end, where the test had been "doing the interesting part". The expected and actual values there were unrelated, the waveform showed a perfectly legal transfer, and the morning went on that. The first mismatch, 400 ns into the test, sat one clock after a backpressure stall; the DUT had dropped a word when TREADY fell in the same cycle a new input word was accepted. Every later error was the queue being one word out of step.
Two habits would have found it in minutes. Read the summary first: nine mismatches and one leftover, with values that XOR to nothing in particular, is the cascade shape. Then go to the first error and look one or two items before it. In a scoreboard log, the earliest failure is almost always the real one.
SB_EMPTY at 1.2 ms, and the input monitor's UVM_HIGH trace shows every beat. Where is the problem?Between the input side and the compare. The run lasted long enough and stimulus reached the pins, so neither the objection nor the stimulus is at fault. Either the output side saw nothing (the output monitor or its connection to out_export), or the scoreboard was never connected. Check out_agt.ap's connections and the output monitor's trace next.
A single DUT data-path bit, bit 16, stuck or inverted: the same one bit on every failing word. If only some words fail, check whether they are the words where bit 16 of the input or key is 0 (a stuck-at-1) or 1 (a stuck-at-0). If the XOR varied from word to word, it would not be a single wire.
Because a lost word is a failure. Realigning quietly turns a DUT bug into a pass with fewer matches. The look-ahead's job is to make the error message accurate ("2 words lost" instead of nine mismatches), not to excuse it.
Next: not every missing word is a bug. Messy Traffic handles DUTs that are allowed to drop, reorder, split and merge, and the DMA's own two cases of legitimate loss.