Skip to main content

The Monitor

From Simple to Layered named this as the biggest structural gap in SystemVerilog's own capstone: no independent component whose only job is watching the DUT's pins. This page builds exactly that — and, per The FIFO Testbench Plan's block diagram, it shares nothing with the driver except the DUT itself.

The monitor class​

class monitor;
virtual fifo_if vif;
mailbox #(fifo_txn) mon2sb;

function new(virtual fifo_if vif, mailbox #(fifo_txn) mon2sb);
this.vif = vif;
this.mon2sb = mon2sb;
endfunction

task run();
forever begin
@(posedge vif.clk);
if (vif.wr_en && !vif.full) begin
fifo_txn txn = new();
txn.op = fifo_txn::WRITE;
txn.wr_data = vif.wr_data;
mon2sb.put(txn);
end
if (vif.rd_en && !vif.empty) begin
fifo_txn txn = new();
txn.op = fifo_txn::READ;
txn.rd_data = vif.rd_data;
mon2sb.put(txn);
end
end
endtask
endclass

Every clock edge, the monitor checks whether a write or a read actually happened — wr_en && !vif.full, not just wr_en alone, since a write asserted while the FIFO is already full doesn't actually push anything — and if so, builds a fresh fifo_txn reconstructing what it saw, put()-ing it into mon2sb, a mailbox the driver has no handle to at all.

Two things worth being precise about​

  • The monitor holds a virtual fifo_if vif — the identical handle type the driver holds — but never once assigns to any of its fields. Nothing in the language enforces that read-only discipline; it's a convention this class follows deliberately, the same way UVM's own monitor follows it. A monitor that accidentally drove a pin would silently corrupt the very traffic it's supposed to be independently observing.
  • The monitor and the driver are two separate objects, constructed separately, connected to two separate mailboxes. Nothing about the monitor's existence depends on the driver's, and vice versa — which is exactly what makes it possible to reuse this monitor alone, in a passive-only environment that observes bus traffic without ever driving anything, a distinction UVM's active/passive agent makes explicit later.

put() is blocking — but never actually blocks here​

mailbox's put() is a blocking call: if the mailbox has a fixed capacity and is currently full, put() suspends the calling task until the consumer drains it with a get(). That matters for mon2sb in principle — but not in practice for this testbench: The Environment constructs it with mon2sb = new();, no capacity argument, which makes it unbounded. An unbounded mailbox's put() always succeeds immediately; there's no capacity to fill, so the monitor's run() loop never actually stalls waiting on the scoreboard to keep up. This is a deliberate simplicity choice for a two-component pipeline this small — a design with many producers feeding one slow consumer would need to think harder about a bounded mailbox's natural backpressure (a full mailbox blocking put() until the consumer catches up), but that tradeoff is out of scope for this page's one monitor, one scoreboard.

What's next​

Stimulus generation and DUT interaction are both done — a generator makes transactions, a driver applies them, a monitor observes the result independently. Section D builds the piece that actually judges whether any of it was correct: a reference model, and the scoreboard that checks against it.