The interviewer draws a monitor with two subscribers, a coverage collector and a scoreboard, and asks: in what order do they receive the write? "In the order you connected them," you say. Here is the stream monitor of the DMA bench with two subscribers: the scoreboard sb connected first, and a second, FIFO-based scoreboard fsb (built in imp_decl vs FIFOs; for now it is just another subscriber) connected after it. The library's own list of where the port's writes go:
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)fsb is called first, although it was connected second. So what does decide the order, what else does a single write() promise, and how does the bench's scoreboard take three streams at once?
An analysis port is a publisher. Its write(t) is a void function: it hands the same transaction handle to every connected imp, one after another, in the caller's own thread, in zero simulated time, and returns. There is no return value, no acknowledgement and no backpressure. A subscriber is any component that owns an analysis imp and implements write(). It is a radio broadcast: the station transmits whether ten radios are on or none, and no radio can ask it to slow down.
That design is deliberate. A monitor reports what the DUT did; it must never wait for a checker, or the testbench would stop sampling the bus. The price is that subscribers must be quick inside write(), and must not change the shared object (each subscriber gets the same handle, which is also why a monitor creates a fresh object per transaction, as in your first testbench).
The order rule explains the opening log: uvm_test_top.env.fsb... sorts before uvm_test_top.env.sb.... Rename sb to a_sb and the order flips. A checker that only works if the coverage collector has already seen the item, or has not yet, is a checker that breaks when someone renames a component.
A component can implement only one method called write(). To receive three streams, the bench declares three imp classes with ` uvm_analysis_imp_decl ``. Each call of the macro generates a class like this one (from the library's macro, trimmed):
// snippet: what `uvm_analysis_imp_decl(_out) generates
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
// constructor as in uvm_analysis_imp
function void write(input T t);
m_imp.write_out(t); // the suffix becomes part of the method name
endfunction
endclassThe macros sit at package level, outside the class, because they declare classes. Use them once per suffix per package: two packages may each declare _in, but one package cannot declare it twice.
// 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
endclassEach imp is declared with the scoreboard class as its IMP parameter and constructed with this.
write_apb tracks the KEY register from observed writes; write_in turns every observed input word into a prediction; write_out compares. All three finish in zero time and never block.
The three methods are virtual. That one word is war story 2, below.
Within one stream, items arrive in the order the monitor wrote them. Across streams there is no promise: if the APB monitor and the input monitor both sample on the same clock edge, which write() runs first depends on the simulator's scheduling of the two monitor threads. The DMA scoreboard is safe because the DUT gives it slack: KEY is written several clocks before any data, and an output word always appears at least one clock after its input word. A scoreboard for a DUT without that slack must not assume an order between streams; imp_decl vs FIFOs discusses the options.
Two APB agents in a subsystem env could both connect to sb.apb_export; the imp accepts any number of ports and each write simply lands in write_apb. What the scoreboard can then no longer tell is which agent the item came from. If that matters, give each source its own suffix, or carry the source in the item.
The opening list came from dma_fifo_sb_test in tlm_tb.sv: the smoke test with an env that adds a second, FIFO-based scoreboard fsb and connects every monitor port to both. The full debug_connected_to() output, and that second scoreboard, are in imp_decl vs FIFOs. Both scoreboards received every word, because a second connection is added to the port's list rather than replacing the first:
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 mismatchedThis bug happened while the bench was being built. dma_drain_test overrides the scoreboard 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 printed sb dma_scoreboard_eot, so the override had worked. Yet the owed count stayed at zero and the run ended with a word still inside the DMA. The new write_apb and write_out never ran.
At that time the base class declared its methods without virtual:
// bad-example: the base scoreboard as first written
class dma_scoreboard extends uvm_scoreboard;
uvm_analysis_imp_out #(axis_item, dma_scoreboard) out_export;
// ...
function void write_out(axis_item t); // not virtual
// compare
endfunction
endclassLook at the imp class again. m_imp is declared as IMP, and IMP is dma_scoreboard, the class named in the declaration. The factory built a dma_scoreboard_eot, and m_imp pointed at it, but m_imp.write_out(t) was resolved through a dma_scoreboard handle. For a non-virtual method, SystemVerilog picks the method from the handle's type, so the base write_out ran every time. The fix is the one in the bench: declare every write_* method virtual in any scoreboard that may be extended or overridden, and call super.write_out(t) in the derived version.
The factory side of this story, and why an override replaces the object, not the calls, is told in Override timing, safety and debug.
t.data in place inside its write(). The scoreboard is on the same port. Does the scoreboard see the original or the normalized value?It depends on the alphabetical order of the two subscribers' full names, because both get the same handle and are called one after the other. That makes the scoreboard's result depend on component names, which is the bug. A subscriber must never modify the received object; it clones first if it needs a changed copy.
virtual function void write_out(axis_item t), but the base class's write_out is not virtual. Does the override work?No. Virtual dispatch is decided by the method in the handle's class. The imp calls through a dma_scoreboard handle, and in that class write_out is not virtual, so the base version runs. virtual must be on the base declaration.
sb.apb_export. Is the second connection an error, and does it replace the first?Neither. An imp accepts any number of ports, and each connection is added, never replaced. Both monitors' items arrive in write_apb, which cannot tell them apart unless the item carries its source or each monitor gets its own suffix.
Next: every port in this lesson had a home. In TLM1 channels and port cardinality you will see which ports fail the run when they are left unconnected, and the ready-made FIFOs and channels that sit between producers and consumers.