Skip to main content

The Monitor & Subscriber

The driver only knows what it was told to drive. Whether the DUT actually responded correctly is a separate question entirely — answering it starts with a component that watches the interface from the outside and reconstructs what happened, without ever driving a single signal itself.

uvm_monitor: watch, don't drive​

class fifo_monitor extends uvm_monitor;
`uvm_component_utils(fifo_monitor)
virtual fifo_if vif;
uvm_analysis_port #(fifo_txn) 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 fifo_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "Virtual interface not set for fifo_monitor")
endfunction

task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (vif.wr_en && !vif.full) begin
fifo_txn txn = fifo_txn::type_id::create("txn");
txn.op = fifo_txn::WRITE;
txn.wr_data = vif.wr_data;
ap.write(txn);
end
if (vif.rd_en && !vif.empty) begin
fifo_txn txn = fifo_txn::type_id::create("txn");
txn.op = fifo_txn::READ;
txn.rd_data = vif.rd_data;
ap.write(txn);
end
end
endtask
endclass

Structurally, this looks a lot like the driver from the previous page — same build_phase/vif pattern — but two differences define what a monitor actually is:

  • It samples signals; it never assigns them. Every reference to vif here is a read (vif.wr_en, vif.full, vif.wr_data...); nowhere does this class write to the interface. A monitor that starts driving signals has quietly turned into a second driver, which is a bug, not a feature.
  • It reconstructs fifo_txn instances independently, from raw pin activity, rather than receiving them from anywhere. This independence is exactly what makes the checking that comes later meaningful — the Scoreboard compares what the monitor observed against what should have happened, and that comparison only means something if the monitor's view didn't come from the same transactions the driver already had.
  • uvm_analysis_port #(fifo_txn) ap, constructed with new() in the constructor — not build_phase, and not type_id::create(). Ports are lightweight TLM objects, not factory-registered components; they're conventionally built the moment the containing component exists. ap.write(txn) broadcasts the reconstructed transaction to any number of listeners, including zero — the monitor has no idea who's listening (or whether anyone is), which is exactly the point: nothing about this class needs to change no matter what gets connected to ap later, or how many things do.

The !vif.full/!vif.empty guards matter for a subtle reason: the DUT is defined to ignore a write while full (or a read while empty), so a monitor that reconstructed a transaction anyway would report activity that never actually took effect — the same gotcha called out for the DUT-design side back in Interface & Transaction.

uvm_subscriber: the other consumer of a monitor's output​

The monitor's ap isn't limited to feeding a scoreboard — anything interested in observed traffic can connect to it, most commonly a coverage collector. uvm_subscriber #(T) exists specifically for that case:

class fifo_coverage extends uvm_subscriber #(fifo_txn);
`uvm_component_utils(fifo_coverage)

covergroup fifo_cg;
coverpoint sampled_op;
endgroup

fifo_txn::op_e sampled_op;

function new(string name, uvm_component parent);
super.new(name, parent);
fifo_cg = new();
endfunction

// The one method uvm_subscriber requires — called automatically
// whenever anything connected to this subscriber's analysis_export fires
function void write(fifo_txn t);
sampled_op = t.op;
fifo_cg.sample();
endfunction
endclass

Two things make uvm_subscriber worth reaching for instead of hand-rolling another uvm_analysis_imp-based component like the scoreboard will: it comes with its analysis_export already built, so there's no new() call to remember in the constructor, and it makes write(T t) a pure virtual method — the class simply won't compile until it's implemented, which rules out the specific silent failure of connecting a coverage collector and forgetting to give it anything to do.

Wiring it in is one line in the environment's connect_phase (covered on the Agent & Environment page) — agent.monitor.ap.connect(coverage.analysis_export) — added independently of, and without touching, either the monitor or the scoreboard.

ap.write() broadcasts to any number of listeners, and never blocks​

ap.write(txn) doesn't call one specific listener — an uvm_analysis_port can be connected to zero, one, or many analysis_exports (or uvm_subscribers) at once, and calling write() fires every connected listener's write() method in turn, in the order they were connected. The monitor above never checks how many things are connected, or whether anything is — it broadcasts unconditionally, and adding a second, third, or fourth listener later (another coverage collector, a logger, a protocol checker) never requires touching this class again. This is also why write() is a void function rather than a task: it cannot consume simulation time, so the monitor never blocks waiting on whatever the scoreboard or coverage collector does with the transaction — it drops the packet and immediately goes back to sampling the interface.

Passive vs. active: what a monitor's presence signals about an agent​

An agent built around only a monitor (no driver, no sequencer) is a passive agent — it observes traffic on an interface without ever generating any, useful for watching a bus segment the testbench doesn't control directly, or for a protocol that's driven by something outside this agent's responsibility. An active agent adds a driver and sequencer alongside the monitor, so it both generates and observes traffic on the same interface. The monitor's own code is identical either way — the passive/active distinction lives in what else is built alongside it, covered on the Agent & Environment page.

What's next​

A monitor that reconstructs transactions is only useful once something generates the stimulus for it to observe in the first place — the sequencer and sequence, covered next.