A team replaced their scoreboard's immediate compare with analysis FIFOs, so output words could be matched later. The first run reported eight mismatches out of eight. Every stored output word had the same value: the last one the DMA produced. The output monitor was sampling correctly; you could see eight different words in its own debug messages. Something between the monitor and the FIFO had turned eight transactions into one.
What does a monitor owe the components that listen to it?
A monitor watches an interface and reports every completed transfer as a transaction. It is a court reporter: it writes down what was actually said, in order, and never speaks. It does not know what anyone meant to say, and it never changes the record once it is handed over. Because it never drives, the same monitor works in an active agent (watching its own driver's output) and in a passive one (watching a bus some other block drives).
The monitor's real job is deciding when a transfer has completed. For APB that is a clock edge where PSEL, PENABLE and PREADY are all 1. For AXI-Stream it is an edge where TVALID and TREADY are both 1. On that edge it creates a new item, fills it from the sampled pins, and calls ap.write(item). Every subscriber connected to the port gets the item, in zero simulation time, before write returns.
The fan-out at the end is the reason for most monitor rules. Three components hold the same handle after write returns. If any one of them, or the monitor itself, changes that object, the others see the change.
// snippet: from the course DMA bench
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
virtual apb_if vif;
uvm_analysis_port #(apb_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "apb_if not set for this monitor")
endfunction
// A transfer completes on the edge where PSEL, PENABLE and PREADY are all 1.
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
apb_item it = apb_item::type_id::create("it"); // a fresh object per transfer
it.addr = vif.mon_cb.paddr;
it.write = vif.mon_cb.pwrite;
it.data = vif.mon_cb.pwdata;
it.rdata = vif.mon_cb.prdata;
it.slverr = vif.mon_cb.pslverr;
`uvm_info("APB_MON", it.convert2string(), UVM_HIGH)
ap.write(it);
end
end
endtask
endclassFour things to notice. The analysis port is created in the constructor, so it exists as soon as the monitor does; the agent's ap = mon.ap depends on that. The virtual interface comes from config_db with a fatal if missing: a monitor with no interface would otherwise sit silently and report nothing. Every pin is read through vif.mon_cb, the interface's clocking block, so the values are the ones from just before the edge: the values the DUT sampled. And the item is created inside the if, once per completed transfer.
// snippet: from the course DMA bench
class axis_monitor extends uvm_monitor;
`uvm_component_utils(axis_monitor)
virtual axis_if vif;
uvm_analysis_port #(axis_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axis_if not set for this monitor")
endfunction
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.tvalid && vif.mon_cb.tready) begin
axis_item it = axis_item::type_id::create("it");
it.data = vif.mon_cb.tdata;
it.last = vif.mon_cb.tlast;
`uvm_info("AXIS_MON", it.convert2string(), UVM_HIGH)
ap.write(it);
end
end
endtask
endclassIdentical shape, different completion rule: TVALID and TREADY both 1. It publishes one item per beat, which is what the DMA scoreboard wants, because each input word predicts exactly one output word. A packet-level monitor would instead collect beats into a working list and publish one packet item on the beat with TLAST, and it would need a rule for reset: a packet half-collected when reset asserts must be thrown away, not published. Choose the level your subscribers need; some monitors publish both, on two ports.
Creating a new item per transfer is the rule that makes fan-out safe: once published, the object belongs to the subscribers, and the monitor never touches it again. A monitor that builds a transaction over several beats in one working object must clone() it before write (and $cast the result, because clone() returns a uvm_object). Subscribers have a matching duty: they must not modify what they receive, because another subscriber holds the same handle. A subscriber that needs to change an item makes its own copy first.
ap.write(it) calls each subscriber's write() function in turn, synchronously, in the monitor's thread. That gives three guarantees: each subscriber sees transactions in the order the monitor published them, none of them can block (write is a function), and all of them have seen the item before the monitor samples the next edge. It does not promise an order between subscribers. The reference library happens to call them in alphabetical order of their full names; code that depends on the scoreboard running before coverage is a bug waiting for a rename.
The timing details behind mon_cb, what to do about X on a control line, and where protocol checks belong are big enough for their own lesson: Sampling, races and X.
Run any test with +UVM_VERBOSITY=UVM_HIGH and both monitors print each transaction they publish (APB_MON and AXIS_MON messages, from the uvm_info calls in the listings). At the default verbosity the monitors are silent, and the evidence that they worked is downstream:
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0Three monitors fed that line: the APB monitor published the KEY write the scoreboard used for prediction, the input monitor published eight beats (eight predictions), and the output monitor published eight beats (eight compares). Change any of them to publish a reused object, and this particular scoreboard would still pass, because it compares the moment each output item arrives. That is exactly why the bug in the next section survived until someone stored items.
The FIFO scoreboard from the opening was correct. The output monitor had been "optimised" to create its item once:
// bad-example: one item reused for every publish
task run_phase(uvm_phase phase);
axis_item it = axis_item::type_id::create("it"); // created once, outside the loop
forever begin
@(vif.mon_cb);
if (vif.mon_cb.tvalid && vif.mon_cb.tready) begin
it.data = vif.mon_cb.tdata; // overwrites what subscribers hold
it.last = vif.mon_cb.tlast;
ap.write(it);
end
end
endtaskA uvm_tlm_analysis_fifo stores the handle it receives. Eight writes stored eight copies of one handle, and by the time the scoreboard drained the FIFO, that one object held the last word. The immediate-compare scoreboard had hidden the bug for months, because it finished with each item before the monitor overwrote it. The fix is moving create inside the if, as the bench does. The general rule: a monitor publishes a fresh object per transaction; a subscriber that stores items can then trust them, and one that modifies items copies them first.
it.data equal to whatever PWDATA held, often a stale write value. Is that a bug?Not in the monitor: it reports the pins faithfully, and PWDATA is undefined during a read. It is a trap for subscribers. A scoreboard or coverage model must look at it.write and use rdata for reads and data for writes. A cleaner monitor could zero data on reads, but it must never invent values that were not on the bus.
it.addr = 0 after sampling, to "clean up". The scoreboard later reports address mismatches only on some runs. Why only some?Subscribers are called in an unspecified order (alphabetical by full name in the reference library). When coverage runs before the scoreboard, the scoreboard sees the modified address; when the order differs, after a rename or on another library, it does not. Subscribers must never modify received items.
So it exists as early as possible. The agent copies mon.ap into its own ap, and anything that touches the port before the monitor's build_phase has run would otherwise see null. Creating ports in the constructor is the common convention for exactly this reason.
Next: the monitor has subscribers; the driver has a supplier. In The sequencer you will see what sits between the sequences and the driver, why the bench never writes a sequencer class, and what happens when someone starts a sequence on a passive agent.