UVM Monitor: Sampling the Interface and Analysis Port
A UVM monitor is the passive observer of a verification environment. It watches the pins of the design-under-test (DUT), reconstructs what those pins mean as high-level transactions, and hands those transactions to anyone who cares, such as a scoreboard or a coverage collector. Unlike a driver, a monitor never forces a value onto the bus; it only listens. If you understand four verbs, you understand the whole component: extend, sample, build, and broadcast. This tutorial walks through each from first principles.
A note on running the code: the canonical UVM blocks below depend on the UVM base-class library, so they are written for a UVM-capable simulator such as industry-standard simulation tools, VCS, or industry-standard simulators (or EDA Playground in the browser). They are shown to illustrate real UVM structure, not to run under free tools like Icarus or Verilator. The one plain-SystemVerilog example near the end is self-contained and does run anywhere.
What a UVM monitor actually does
Think of a bus protocol: a clock, a valid signal, an address, and some data. During a cycle where the handshake completes, a meaningful event occurred on the wires. A human reviewing a waveform would say “a write to address 0x10 happened.” The monitor’s job is to make that same judgement in software, package it into an object, and announce it. Everything downstream, checking and coverage, works on those tidy objects instead of re-decoding raw pins. This separation is what keeps a UVM testbench scalable.
Because it is passive, a well-written monitor can be dropped into any environment, active or reactive, without changing stimulus behaviour. That reusability is the entire reason the role exists as a distinct component.
Extending uvm_monitor
A monitor is a uvm_component, so it lives for the whole simulation and participates in UVM phases. You derive it from the uvm_monitor base class, register it with the factory, and give it two things it cannot work without: a handle to the DUT interface, and an analysis port to publish through.
class mem_monitor extends uvm_monitor;
`uvm_component_utils(mem_monitor)
// Broadcast port: the monitor publishes, subscribers subscribe.
uvm_analysis_port #(mem_txn) ap;
// Virtual interface handle to the DUT pins.
virtual mem_if vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);
if (!uvm_config_db #(virtual mem_if)::get(this, "", "vif", vif))
`uvm_fatal("MON", "virtual interface not set for monitor")
endfunction
endclassTwo details matter here. First, the analysis port ap is constructed in build_phase, not declared and forgotten; a null port will crash the moment you call write. Second, the virtual interface is pulled from the configuration database. The monitor does not know which physical interface instance it is watching until the test wires it up, which is exactly what makes the component reusable.
Sampling the interface and building a transaction
The real work lives in run_phase, a task that runs for the life of the simulation. The classic shape is a forever loop that waits for a clock edge, checks whether a meaningful event occurred, and, if it did, captures the pins into a fresh transaction object. Sampling and building are two steps of one idea: translate electrical state into a software record.
task run_phase(uvm_phase phase);
mem_txn tr;
forever begin
// 1. Wait for a sampling event on the bus.
@(posedge vif.clk);
if (vif.valid && vif.ready) begin
// 2. Build a fresh transaction object via the factory.
tr = mem_txn::type_id::create("tr");
// 3. Sample the pins into the transaction fields.
tr.addr = vif.addr;
tr.data = vif.data;
tr.write = vif.we;
// 4. Broadcast it to every connected subscriber.
ap.write(tr);
end
end
endtaskCreate a new object each time through the loop. Reusing a single handle and overwriting its fields is a common and painful bug: subscribers hold a reference, so mutating the object after broadcasting corrupts data that others are still reading. A fresh create per transaction keeps each record immutable from the monitor’s side.
Notice the sampling point is deliberate. Sampling on the active clock edge, only when the handshake asserts, mirrors how the protocol defines a valid beat. If you sample on the wrong edge or without the qualifier, you capture metastable or meaningless values. The transaction object itself is a plain data carrier, typically derived from uvm_sequence_item:
class mem_txn extends uvm_sequence_item;
`uvm_object_utils(mem_txn)
rand bit [31:0] addr;
rand bit [31:0] data;
bit write;
function new(string name = "mem_txn");
super.new(name);
endfunction
endclassBroadcasting via uvm_analysis_port
The call ap.write(tr) is the heart of the component. A uvm_analysis_port implements a one-to-many, non-blocking publish mechanism. The monitor does not know or care how many subscribers are listening; it could be zero, one, or five. Each connected subscriber receives its own call to its write method, synchronously, in the same simulation time step. There is no handshake back, so a slow or absent subscriber never stalls the monitor.
Connection happens one level up, usually in the agent’s connect_phase, where the monitor’s port is wired to a subscriber’s analysis export or implementation:
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
monitor.ap.connect(scoreboard.analysis_export);
endfunctionThis loose coupling is why a scoreboard and a functional-coverage collector can both consume the same transaction stream without the monitor ever mentioning either of them.
Monitor versus driver at a glance
| Aspect | Monitor | Driver |
|---|---|---|
| Direction | Reads pins (input only) | Writes pins (drives DUT) |
| Interface access | Samples, never forces | Forces values on signals |
| Main method | run_phase sampling loop | run_phase drive loop |
| Output | Analysis port broadcast | Consumes sequence items |
| Reusability | Works in active and passive agents | Active agent only |
The broadcast pattern without any UVM library
The analysis-port idea, one producer fanning out to many subscribers, is just the observer pattern. You can see the whole mechanism in plain SystemVerilog, with no UVM at all: a tiny class keeps a queue of subscriber handles and calls write on each. The following testbench runs under any simulator and prints exactly what a real ap.write does under the hood.
// Plain SystemVerilog: one producer, many subscribers (no UVM library).
// This models the idea behind uvm_analysis_port.write().
class transaction;
int addr;
int data;
function new(int addr, int data);
this.addr = addr;
this.data = data;
endfunction
endclass
// Any subscriber simply provides a write() method.
class subscriber;
string name;
int count;
function new(string name);
this.name = name;
endfunction
function void write(transaction t);
count++;
$display(" [%s] got addr=0x%0h data=0x%0h (total=%0d)",
name, t.addr, t.data, count);
endfunction
endclass
// A miniature analysis port: keeps subscriber handles and fans out write().
class analysis_port;
subscriber subs[$];
function void connect(subscriber s);
subs.push_back(s);
endfunction
function void write(transaction t);
foreach (subs[i]) subs[i].write(t);
endfunction
endclass
module tb;
analysis_port ap;
subscriber scb;
subscriber cov;
transaction t;
initial begin
ap = new();
scb = new("scoreboard");
cov = new("coverage");
ap.connect(scb);
ap.connect(cov);
for (int i = 0; i < 3; i++) begin
t = new(16 + i, 160 + i);
$display("Broadcasting transaction %0d:", i);
ap.write(t);
end
$display("Done: scoreboard saw %0d, coverage saw %0d", scb.count, cov.count);
$finish;
end
endmoduleBroadcasting transaction 0: [scoreboard] got addr=0x10 data=0xa0 (total=1) [coverage] got addr=0x10 data=0xa0 (total=1) Broadcasting transaction 1: [scoreboard] got addr=0x11 data=0xa1 (total=2) [coverage] got addr=0x11 data=0xa1 (total=2) Broadcasting transaction 2: [scoreboard] got addr=0x12 data=0xa2 (total=3) [coverage] got addr=0x12 data=0xa2 (total=3) Done: scoreboard saw 3, coverage saw 3
Swap analysis_port for the real uvm_analysis_port, let the subscribers be a scoreboard and a coverage collector, and move the loop into run_phase, and you have an industrial UVM monitor. The underlying mechanic never changed.
Practical habits that save debugging hours
- Create a new transaction object every loop iteration; never recycle a handle you have already broadcast.
- Construct the analysis port in
build_phasebefore anyone connects to it. - Sample on the protocol’s defined edge and only when the handshake qualifies the data.
- Keep the monitor strictly passive; if you find yourself assigning to an interface signal, that logic belongs in the driver.
- Fetch the virtual interface through
uvm_config_dband fail loudly withuvm_fatalif it is missing. - Give the monitor no knowledge of its subscribers; let the agent or environment make the connections.
Quick check
Why is a monitor called a passive component?
Because it only reads the DUT interface to observe behaviour and never drives or forces any signal. This lets the same monitor run in both active and passive agents without affecting stimulus.
Why create a fresh transaction object on every loop iteration?
Subscribers receive a handle to the object, not a copy. If you reuse one handle and overwrite its fields, you corrupt data that a scoreboard or coverage collector may still be reading. A new object per transaction keeps each record stable.
What does uvm_analysis_port.write() do when three subscribers are connected?
It calls each subscriber’s write method once, synchronously, in the same time step. The call is non-blocking and one-to-many, so no subscriber can stall the monitor, and the monitor needs no knowledge of how many are listening.
Where is the monitor’s analysis port typically connected to a subscriber?
In the connect_phase of the enclosing agent or environment, for example monitor.ap.connect(scoreboard.analysis_export). Keeping connection outside the monitor preserves its reusability.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses