The topology table at the start of the run says sb dma_scoreboard_eot. The factory override worked. The test is dma_drain_test, which drops its objection early and relies on that scoreboard to hold the run phase open until every owed word is out. Yet the run ends early, and a debug message you put in the derived write_out never prints. Not once. The object is the right class; its methods are not being called. How can a component receive traffic and still skip the code you wrote for it?
A component that subscribes to an analysis port must provide a function called write. A scoreboard listens to three monitors, so it needs three different write methods, one per stream, and SystemVerilog does not allow three methods with the same name in one class. The analysis imp is the fix: a small object that owns the write the port calls, and forwards it to a method of the component. The macro uvm_analysis_imp_decl(_SUFFIX) declares an imp class that forwards to write_SUFFIX instead of write.
Think of an office with three mail slots in the door, labelled registers, input and output. The postman (the analysis port) only knows how to push letters through a slot. Behind each slot a clerk (the imp) hands the letter to the right desk. The forwarding rule is the whole trick, and it is also where the opening bug lives: the clerk was told the desk's name when the office was built, and if the desk is not marked as replaceable, the new occupant never gets the mail.
The three macros sit at package scope, before the class, in the bench. Each one expands into a class declaration, so package or file scope is the portable convention; nested class declarations are legal SystemVerilog, but not every tool is happy with a macro-generated class inside another class.
// snippet: from the course DMA bench
`uvm_analysis_imp_decl(_apb)
`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)Inside dma_scoreboard, each imp is declared with two parameters, the item type and the class that owns the method, and constructed in new():
// snippet: from 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;
...
apb_export = new("apb_export", this); // 'this' becomes the imp's m_imp handleStripped of plumbing, uvm_analysis_imp_decl(_out) produces this. The line to look at is the last one inside write:
// snippet: roughly what uvm_analysis_imp_decl(_out) expands to
class uvm_analysis_imp_out #(type T = int, type IMP = int) extends uvm_port_base #(uvm_tlm_if_base #(T, T));
local IMP m_imp; // set from the constructor's second argument
function new(string name, IMP imp);
super.new(name, imp, UVM_IMPLEMENTATION, 1, 1);
m_imp = imp;
endfunction
function void write(input T t);
m_imp.write_out(t); // a call through a handle of type IMP
endfunction
endclassIMP is dma_scoreboard, the class written in the declaration. When the factory builds a dma_scoreboard_eot in its place, m_imp points at the derived object, but the call m_imp.write_out(t) is made through a base-typed handle. SystemVerilog then calls the derived version only if the method is virtual. That is why every write_* in the bench is declared virtual function void.
Several ports may connect to one imp, and an analysis port may feed many imps; a second connection never replaces the first. So wiring the input and output monitors to one plain write would deliver both streams, into the same method, with nothing to say which monitor sent which item. The suffix imps keep the streams apart by construction.
The bench's monitors create a fresh item for every transaction and never touch it after write. A subscriber that stores an item can then keep the handle. dma_scoreboard goes further and stores only a computed 32-bit value, so it is immune either way. A scoreboard that stores whole items from VIP you do not control, where a monitor might reuse one object, should copy before storing; that copy is defence in depth, not the norm.
Suffix imps ( | One | |
|---|---|---|
Where the compare runs | Inside the | In a |
Can it wait? | No: | Yes: it can wait for the partner item, a clock, or a timeout |
Typical use | Predict on input, compare on output, as in | Pair items that arrive at different times, latency checks, built-in comparators |
Cost | Least code | A thread per pairing, and FIFOs to drain at the end |
The library's comparators are built on FIFOs; they are taught, with their traps, in Built-in comparators. The FIFO mechanics are in Analysis FIFOs and debug.
With virtual methods, dma_drain_test drops its objection while the last word is still inside the DMA, the derived scoreboard holds the phase open, and every word is compared:
+UVM_TESTNAME=dma_drain_test
UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0This happened while the bench was being built. The first version of dma_scoreboard declared its write_* methods without virtual. dma_drain_test overrode it with dma_scoreboard_eot, which extends write_apb to learn how many words each job owes and write_out to count them down. The topology table printed the derived type, so everyone assumed the override had worked.
It had, for the object. But every imp called m_imp.write_apb(t) and m_imp.write_out(t) through a dma_scoreboard handle, so the base methods ran and the derived ones never did. The owed count stayed at zero, phase_ready_to_end saw nothing owed, and the extension the test depended on never happened. Nothing in the log said so: no error, no warning, just a scoreboard quietly behaving like its parent.
The fix is one keyword per method: virtual. The rule to take away is wider than scoreboards: a factory override replaces the object; whether your new code runs still depends on virtual dispatch. Any method a derived class might extend, and every method an imp or a callback calls, should be virtual in the base class from day one. Making it virtual later means editing the base class, which is exactly what the override was meant to avoid.
uvm_analysis_imp_decl(_irq) inside the scoreboard class body. It compiles on one simulator and fails on another. What do you tell them?Move the macro to package or file scope, next to the other three. The macro expands to a class declaration; a nested class is legal SystemVerilog, but tool support for a macro-generated class inside another class varies, and the package-scope form is the portable convention everyone reads.
write_out and forgets to call super.write_out(t). The base methods are virtual. What happens to the compare?The derived method runs instead of the base one, so the base compare never happens: no pops from expected, no matches, no mismatches. The run ends with every prediction left over (SB_LEFTOVER) and SB_EMPTY. Virtual dispatch got the right method called; calling super is what keeps the parent's behaviour.
dma_scoreboard keep its predictions safely even if a monitor reused one item object?It never stores an item. write_in computes t.data ^ key and pushes a 32-bit value, and write_out reads t.data immediately inside the call. Nothing refers to the item after write returns, so later changes to that object cannot reach the queue.
Next: the scoreboard has three inputs; something must connect them. Env Wiring wires the block scoreboard and then an end-to-end checker across two chained DMAs.