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.