An interviewer leans back: your DMA scoreboard learns the XOR key from the APB monitor, not from the test. Good. So what happens when software writes KEY in the middle of a job? You start to answer that the monitor sees the write, so the scoreboard has the new key... and stop. The DMA's register spec says KEY is ignored while the block is busy. Your scoreboard does not know that. Seven words later it would be reporting seven mismatches against a DUT that did exactly what its spec says.
This lesson is about the two halves of every compare: where expected comes from, where actual comes from, and why getting either one from the wrong place produces failures that are not bugs, or worse, passes that are not passes.
Actual is what the DUT did, as seen by a monitor on the interface where the DUT's result appears. Expected is what the DUT should have done, computed by a model from the DUT's inputs. A prediction is one expected item, made at the moment the input that causes it is observed. An observation point is the place on the pins where a monitor watches: in the DMA bench there are three, the APB bus, the input stream and the output stream.
Think of an exam marker. The answer key must be worked out from the paper the student was actually handed, not from the exam setter's plan (a misprinted question changes the right answer), and certainly not from the student's own sheet. In testbench terms: expected comes from observed inputs, never from the sequence's intent; actual comes from the output monitor, never from the driver; and the two never come from the same place, or the check proves nothing.
Every scoreboard in this topic is a variation on this picture. Later lessons change how the queue is organised (one per ID, one per flow), what the predict step does (split, merge, drop), and what happens at the end. The arrows stay: nothing flows into the scoreboard from a sequence or a driver.
// snippet: from the course DMA bench
// Predicts every output word from the input word and the KEY register the
// DUT was programmed with, then compares in order. The write_* methods are
// virtual so a derived scoreboard (a factory override) can extend them.
class dma_scoreboard extends uvm_scoreboard;
`uvm_component_utils(dma_scoreboard)
uvm_analysis_imp_apb #(apb_item, dma_scoreboard) apb_export;
uvm_analysis_imp_in #(axis_item, dma_scoreboard) in_export;
uvm_analysis_imp_out #(axis_item, dma_scoreboard) out_export;
bit [31:0] key;
bit [31:0] expected[$];
int unsigned n_match, n_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
apb_export = new("apb_export", this);
in_export = new("in_export", this);
out_export = new("out_export", this);
endfunction
virtual function void write_apb(apb_item t);
if (t.write && !t.slverr && t.addr == KEY) key = t.data;
endfunction
virtual function void write_in(axis_item t);
expected.push_back(t.data ^ key);
endfunction
virtual function void write_out(axis_item t);
bit [31:0] exp;
if (expected.size() == 0) begin
`uvm_error("SB_UNEXP", $sformatf("output word %08h with nothing expected", t.data))
n_mismatch++;
return;
end
exp = expected.pop_front();
if (t.data !== exp) begin
`uvm_error("SB_MISMATCH", $sformatf("expected %08h, got %08h", exp, t.data))
n_mismatch++;
end else begin
n_match++;
end
endfunction
function void check_phase(uvm_phase phase);
if (expected.size() != 0)
`uvm_error("SB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
if (n_match == 0)
`uvm_error("SB_EMPTY", "no words were compared: the test checked nothing")
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("SB", $sformatf("%0d words matched, %0d mismatched", n_match, n_mismatch), UVM_LOW)
endfunction
endclasswrite_apb updates the scoreboard's copy of KEY only for a write, to the KEY address, that completed without PSLVERR. Each condition matters. A read of KEY must not change the model. A write that the DUT rejected with an error never reached the register. And because the value comes from the APB monitor, a sequence that meant to write KEY but used the wrong address leaves the model unchanged, exactly like the DUT.
write_in runs once per beat the input monitor saw, that is, once per clock where TVALID and TREADY were both high. It pushes t.data ^ key onto expected. It stores a 32-bit value, not the item handle, which also means nothing a monitor does later can change a stored prediction.
write_out pops the front of the queue and compares with !==, the four-state inequality. In this bench that buys nothing against X: the monitor copies TDATA into the item's 2-state bit [31:0] data, so an X or Z on the bus has already become 0 before the compare runs. To catch X, test $isunknown(vif.mon_cb.tdata) in the monitor before the copy, or keep the item's data as logic, and run on a four-state simulator (Verilator is two-state). An output word with nothing expected is SB_UNEXP. The two end-of-test checks in check_phase (SB_LEFTOVER, SB_EMPTY) get a lesson of their own: End-of-Test Checks.
dma_truncate_test makes the point with a real run. A factory override swaps the stream driver for axis_truncating_driver, which forces TLAST on beat 3 and silently drops the rest of each packet. The sequence still generated eight items. Only four ever appeared on the bus.
+UVM_TESTNAME=dma_truncate_test
UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
UVM_ERROR : 0Four words in, four predictions, four matches, nothing left over. A scoreboard fed from the sequence would have held eight predictions and reported four as never delivered, blaming the DUT for words it never received. Prediction from the observed input followed the bus, so the scoreboard needed no knowledge of the fault the driver injected.
Source | How it works | Where it is taught |
|---|---|---|
Model inside the scoreboard | The | This topic |
Separate predictor component | A component turns observed inputs into expected items and publishes them; the scoreboard or a comparator only compares. Reusable, swappable by factory override. | |
C or C++ model through DPI-C | The predictor calls an existing algorithm model instead of re-coding it in SystemVerilog. | |
Golden RTL | A trusted earlier design gets the same stimulus; its output monitor becomes expected. Rare: it needs a trusted copy and identical stimulus. | Side note only |
Whatever the source, the inputs to the model are monitor observations. The model is the only place where the spec's rules live, which is why the next section matters.
This is the interviewer's question, run for real on a scratch copy of the bench. The test programs KEY = 0x11111111 and an 8-word job, starts the packet, and 60 ns later writes KEY = 0x22222222 while the DMA is busy. The DUT's RTL has if (!busy) key <= pwdata;, as its spec says. The bench scoreboard updates its key on every successful KEY write.
(scratch run on a copy of the bench)
UVM_INFO scratch_tb.sv(135) @ 255000: uvm_test_top [KEY] KEY rewritten in the middle of the job
UVM_ERROR dma_tb.sv(396) @ 265000: uvm_test_top.env.sb [SB_MISMATCH] expected 5ba2c25b, got 6891f168
UVM_ERROR dma_tb.sv(396) @ 345000: uvm_test_top.env.sb [SB_MISMATCH] expected e9eeeafc, got daddd9cf
... four more like these ...
UVM_ERROR dma_tb.sv(396) @ 525000: uvm_test_top.env.sb [SB_MISMATCH] expected 8cc33683, got bff005b0
UVM_INFO scratch_tb.sv(138) @ 555000: uvm_test_top [KEY] KEY reads back 11111111
UVM_INFO dma_tb.sv(411) @ 695000: uvm_test_top.env.sb [SB] 1 words matched, 7 mismatched
UVM_ERROR : 7Seven errors, and the DUT is right: KEY reads back 0x11111111. XOR any expected value with the value it got and you get 0x33333333, which is the old key XOR the new key. That pattern says the model used the wrong key, not the data path corrupted a word. The scoreboard was fed from the right place; what it lacked was a rule. Observation tells the model that a write happened; only the spec says whether the write took effect.
The fix is to model the rule. A derived scoreboard tracks BUSY from the observed GO write and the observed input words, and ignores LEN and KEY writes while a job is running:
// snippet: the DUT ignores LEN and KEY writes while BUSY, so the model must too
bit busy;
int unsigned len_reg, taken;
virtual function void write_apb(apb_item t);
if (!t.write || t.slverr) return;
if (t.addr == KEY && !busy) key = t.data;
if (t.addr == LEN && !busy) len_reg = t.data[15:0];
if (t.addr == CTRL && t.data[0] && !busy && len_reg != 0) begin
busy = 1; // GO: a job of len_reg words starts
taken = 0;
end
endfunction
virtual function void write_in(axis_item t);
super.write_in(t); // predict with the key in force
taken++;
if (taken == len_reg || t.last) busy = 0; // the last word, or a short packet (ERR)
endfunctionOne boundary remains: a KEY write that completes on the same clock edge as the job's last input word. The DUT sees BUSY still high on that edge and ignores the write. The scoreboard's answer depends on which monitor's write runs first in that timestep, and the standard does not say. That is a real corner, and the right response is a directed test for it, not hope.
bench/dma_busy_edge_tb.sv holds it. dma_key_edge_test runs eleven rounds. Each round starts a 4-word job and writes a new KEY one clock later than the round before, so the write sweeps across the end of the job. A 2-word job with no KEY write follows, and its words show whether the scoreboard and the DUT agree on the key. A watcher on the pins counts the rounds where the KEY write and the last word share an edge, and the test fails if none did: a pass has to mean the corner ran. Two plusargs pick the model (+SB=naive for the scoreboard above) and the callback order (+IN_FIRST delays the APB monitor's write by #0, still at the same $time, so the input word arrives first, as another simulator may order it).
Scoreboard | APB | Input |
|---|---|---|
Busy-aware model above | 66 matched, 0 mismatched | 2 mismatched |
With the same-edge rule below | 66 matched, 0 mismatched | 66 matched, 0 mismatched |
+UVM_TESTNAME=dma_key_edge_test +SB=naive +IN_FIRST (run.sh scripts/data/uvm-course/bench/dma_busy_edge_tb.sv)
UVM_INFO dma_busy_edge_tb.sv(162) @ 1835000: uvm_test_top [EDGE] KEY write and the job's last word on the same edge
UVM_ERROR dma_tb.sv(396) @ 1985000: uvm_test_top.env.sb [SB_MISMATCH] expected b0e824a0, got 83db1793
UVM_ERROR dma_tb.sv(396) @ 2005000: uvm_test_top.env.sb [SB_MISMATCH] expected f4ab6d1f, got c7985e2c
UVM_INFO dma_busy_edge_tb.sv(199) @ 4745000: uvm_test_top [EDGE] 1 round(s) hit the same edge
UVM_INFO dma_tb.sv(411) @ 4745000: uvm_test_top.env.sb [SB] 64 words matched, 2 mismatched
UVM_ERROR : 2Expected XOR actual is 0x33333333 for both words: old key XOR new key, the same signature as the story above. The busy-aware model passed on this simulator only because of the order Verilator happens to choose. The fix replaces the order with a rule: every observation in one timestep was sampled on the same edge, so a register write must see BUSY as it was before that edge. When the last word clears busy, the model remembers the time, and a write stamped with the same time still counts as a write while busy:
// snippet: from dma_scoreboard_busy_edge in bench/dma_busy_edge_tb.sv
time busy_ended_at = -1;
virtual function void end_job(); // called by write_in on the last word
super.end_job(); // busy = 0
busy_ended_at = $time;
endfunction
virtual function bit busy_at_edge(); // write_apb tests this instead of busy
return busy || ($time == busy_ended_at);
endfunctionThe same rule covers GO and LEN, which write_apb also gates on BUSY. The lesson generalises: when two monitors sample the same edge, decide which state each observation sees, write that down in the model, and test both delivery orders on purpose.
Anything that changes what reaches the pins: a driver that drops or modifies beats (like axis_truncating_driver), a sequence killed mid-packet before every item was driven (as in dma_reset_test), or a DUT that stops accepting words when LEN is reached so the rest of the packet is never taken. In each case the sequence's items and the observed beats differ, and only the observed beats are what the DUT processed.
Most likely a DUT data-path bug on bit 10: a single, fixed bit flipping in otherwise correct data points at a stuck or swapped bit in the RTL, not at model state. Compare with the KEY story, where the XOR was the difference between two keys. The shape of the difference is the first clue to which side is wrong.
write_apb ignore writes that completed with PSLVERR, and what would break if it did not?A write that ended with PSLVERR did not reach any register (the DMA answers PSLVERR only for unmapped addresses). If the model accepted it, a test that wrote to a wrong address with KEY's data would change the model's key but not the DUT's, and every following word would mismatch. Filtering keeps the model to what the DUT actually stored.
Next: the scoreboard takes three different streams. Three Streams, One Scoreboard shows how one class gets three write methods, and the one keyword that decides whether a derived scoreboard's methods ever run.