In code review, a senior engineer asks you to rewrite the DMA scoreboard with analysis FIFOs: cleaner, and the threads can wait for things. You do it, run both side by side, and both report 8 words matched. Then you give the new one a realistic, slower compare, and it fails a test the old one still passes:
UVM_ERROR tlm_tb.sv(325) @ 655000: uvm_test_top.env.fsb [FSB_UNREAD] items left unread: apb=0 in=0 out=2
UVM_ERROR tlm_tb.sv(327) @ 655000: uvm_test_top.env.fsb [FSB_LEFTOVER] 3 expected word(s) never came out
UVM_INFO tlm_tb.sv(332) @ 655000: uvm_test_top.env.fsb [FSB] 5 words matched, 0 mismatched
UVM_INFO tlm_tb.sv(334) @ 655000: uvm_test_top.env.fsb [FSB] out_fifo peaked at 5 waiting item(s)
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatchedSame traffic, same DUT, same moment. What exactly do you trade when you move a scoreboard from imp_decl callbacks to FIFOs?
With `imp_decl`, the monitor's write() calls straight into the scoreboard's write_out(). The check runs in the monitor's thread, at the moment of sampling, in zero time. It cannot wait for anything, and when the monitor returns, the check is done. With analysis FIFOs, the monitor's write() drops the item into a uvm_tlm_analysis_fifo and returns; the scoreboard's own threads get() items whenever they are ready. Those threads may wait, call a slow reference model, or synchronize with a DUT event.
It is the difference between a food critic standing at the kitchen pass, judging each plate as it leaves, and plates going onto a rack that the critic works through at their own pace. The rack lets the critic take as long as needed per plate. It also means the kitchen can close with plates still on the rack.
The predictor and comparator are the same as dma_scoreboard's. What changes is where they run: three forever loops in run_phase, one per FIFO. check_phase gains a check the imp_decl version never needed, that every FIFO was read to the end.
// snippet: from tlm_tb.sv in the course bench
class dma_fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(dma_fifo_scoreboard)
uvm_tlm_analysis_fifo #(apb_item) apb_fifo;
uvm_tlm_analysis_fifo #(axis_item) in_fifo, out_fifo;
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_fifo = new("apb_fifo", this);
in_fifo = new("in_fifo", this);
out_fifo = new("out_fifo", this);
endfunction
task run_phase(uvm_phase phase);
fork
forever begin apb_item t; apb_fifo.get(t); if (t.write && !t.slverr && t.addr == KEY) key = t.data; end
forever begin axis_item t; in_fifo.get(t); expected.push_back(t.data ^ key); end
forever begin axis_item t; out_fifo.get(t); compare(t); end
join
endtask
virtual function void compare(axis_item t);
// same as dma_scoreboard::write_out: pop expected, compare, count
endfunction
function void check_phase(uvm_phase phase);
if (!apb_fifo.is_empty() || !in_fifo.is_empty() || !out_fifo.is_empty())
`uvm_error("FSB_UNREAD", $sformatf("items left unread: apb=%0d in=%0d out=%0d",
apb_fifo.used(), in_fifo.used(), out_fifo.used()))
if (expected.size() != 0)
`uvm_error("FSB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
if (n_match == 0) `uvm_error("FSB_EMPTY", "no words were compared")
endfunction
endclassAn env subclass builds the second scoreboard and adds a second subscriber to every monitor port. A test swaps the env in with a factory override and prints both ends of one link at end of elaboration:
// snippet: from tlm_tb.sv in the course bench
class dma_fifo_env extends dma_env;
`uvm_component_utils(dma_fifo_env)
dma_fifo_scoreboard fsb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
fsb = dma_fifo_scoreboard::type_id::create("fsb", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase); // ap -> sb imps, as before
apb.ap.connect(fsb.apb_fifo.analysis_export); // a second subscriber per port
in_agt.ap.connect(fsb.in_fifo.analysis_export);
out_agt.ap.connect(fsb.out_fifo.analysis_export);
endfunction
endclass
class dma_fifo_sb_test extends dma_smoke_test;
`uvm_component_utils(dma_fifo_sb_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_env::type_id::set_type_override(dma_fifo_env::get_type());
super.build_phase(phase);
endfunction
function void end_of_elaboration_phase(uvm_phase phase);
dma_fifo_env fenv;
$cast(fenv, env);
env.in_agt.ap.debug_connected_to(); // fan-out of the publisher
fenv.fsb.in_fifo.analysis_export.debug_provided_to(); // fan-in of one subscriber
endfunction
endclassdebug_connected_to() prints a port's fan-out network and the resolved list of imps it calls; debug_provided_to() prints what feeds a connector. Call them at or after end of elaboration, when the bindings are resolved. They are part of the Accellera library (1.2 and the 1800.2 reference implementation) rather than the IEEE 1800.2 standard text.
For a component with a single input, such as a coverage collector, uvm_subscriber #(T) declares the imp for you. Its field is analysis_export (an imp despite the name, whose instance name is analysis_imp), and write() is pure virtual, so the compiler makes you implement it. It cannot take a second stream; for that you are back to imp_decl or FIFOs.
// snippet
class apb_cov extends uvm_subscriber #(apb_item);
`uvm_component_utils(apb_cov)
int unsigned n_key_writes;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void write(apb_item t);
if (t.write && t.addr == KEY) n_key_writes++; // sample a covergroup here
endfunction
endclass
// env connect_phase: apb.ap.connect(cov.analysis_export);dma_fifo_sb_test on the bench plus these additions. The debug calls print the publisher's fan-out, in the order write() will call it, and the FIFO's single source:
UVM_INFO @ 0: uvm_test_top.env.in_agt.mon.ap [debug_connected_to] This port's fanout network:
uvm_test_top.env.in_agt.mon.ap (uvm_analysis_port)
|
|_uvm_test_top.env.fsb.in_fifo.analysis_export (uvm_analysis_imp)
|
|_uvm_test_top.env.sb.in_export (uvm_analysis_imp_in)
Resolved implementation list:
0: uvm_test_top.env.fsb.in_fifo.analysis_export (uvm_analysis_imp)
1: uvm_test_top.env.sb.in_export (uvm_analysis_imp_in)
UVM_INFO @ 0: uvm_test_top.env.fsb.in_fifo.analysis_export [debug_provided_to] This port's fanin network:
uvm_test_top.env.fsb.in_fifo.analysis_export (uvm_analysis_imp)
|
|_uvm_test_top.env.in_agt.mon.ap (uvm_analysis_port)
UVM_INFO tlm_tb.sv(332) @ 655000: uvm_test_top.env.fsb [FSB] 8 words matched, 0 mismatched
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatchedThe port is named in_agt.mon.ap: the agent's ap is the monitor's object, as in connect() rules.
fsb is first in the resolved list although it was connected second: the alphabetical order from Analysis broadcast.
Both scoreboards matched all 8 words. With an instant compare the two designs are indistinguishable.
Now make each output compare take 80 ns (dma_fifo_slow_test), as it would if the FIFO scoreboard called a slow reference model (the imp_decl scoreboard could not do this at all: a function cannot wait). The smoke test drops its objection 100 ns after its final STATUS read, which is plenty for the DUT and nowhere near enough for the slow consumer. The run phase ends, the scoreboard's get loops are killed with it, and check_phase finds the backlog: two words still in out_fifo, one more taken out but never finished, so three predictions never matched. The imp_decl scoreboard, which compares inside write(), has nothing pending and matches all 8.
The fix is the same idea as the bench's dma_scoreboard_eot: a scoreboard with work in flight holds the run phase open itself. Here the slow variant (dma_fifo_drain_test, scoreboard dma_fifo_scoreboard_eot) raises an objection in phase_ready_to_end while any FIFO is non-empty or a compare is running (idle() checks the three FIFOs and a busy flag):
// snippet: from tlm_tb.sv in the course bench
function void phase_ready_to_end(uvm_phase phase);
if (phase.get_imp() != uvm_run_phase::get() || idle()) return;
phase.raise_objection(this, "analysis fifos not drained");
fork begin
while (!idle()) #10ns;
phase.drop_objection(this, "analysis fifos drained");
end join_none
endfunctionUVM_INFO tlm_tb.sv(332) @ 855000: uvm_test_top.env.fsb [FSB] 8 words matched, 0 mismatched
UVM_INFO tlm_tb.sv(334) @ 855000: uvm_test_top.env.fsb [FSB] out_fifo peaked at 5 waiting item(s)
UVM_INFO dma_tb.sv(411) @ 855000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0The run now ends at 855 ns instead of 655 ns, when the last compare finishes. Draining FIFOs covers only what the monitors have already published; for words the DUT still owes, the stronger rule from war story 3 applies: decide "done" from what the DUT was asked to do.
| Analysis FIFOs + get() loops | |
|---|---|---|
When the check runs | Inside the monitor's | Later, in the scoreboard's thread (same timestep if it is idle) |
Can the check wait or consume time? | No: | Yes: call models, wait for events, delay |
Order across streams | Follows the order the monitors call | Lost between separate FIFOs and threads; merge it yourself if it matters |
End of test | Nothing pending when the monitor returns | Must drain: check |
Memory | Only what you store | Unbounded FIFOs grow if a thread stalls or dies |
Extending by override | Make | Override virtual helpers such as |
Debug | Counters only |
|
A practical rule: use imp_decl when every check can be finished in zero time from what has been observed so far, which covers most in-order scoreboards. Use FIFOs when the checker must wait, for a reference model, a DUT side-band, or a matching transaction on a slower interface, and then own the draining at end of test. Mixing is fine: a scoreboard can take register writes through an imp_decl callback, so the key is always current, and data through a FIFO.
The FIFO design, or at least a FIFO for that stream. The wait needs a task running in its own thread; a write_in callback is a function in the monitor's thread and cannot wait. The scoreboard must then drain that FIFO and hold the phase at end of test.
The two get loops are separate threads, so the input word may be processed before the KEY write and predicted with the old key. Fixes: take the register stream through an imp_decl callback (processed in the monitor's call order, before the data thread runs), or merge both streams into one ordered queue, or timestamp items and order by time.
uvm_subscriber, while the DMA scoreboard cannot?uvm_subscriber provides exactly one analysis imp and one write(). A coverage collector usually listens to one monitor. The scoreboard needs three streams with three different methods, which takes imp_decl (or three FIFOs).
Next: when a transaction goes missing, you now know every place it can hide. Connection triage and FIFO sizing turns that into a playbook, and uses the peak occupancy above to size a FIFO.