UVM Driver wrote the half of this testbench that pushes transactions onto reg_if. This article writes the other half: the part that watches the interface without touching it, and turns what it sees back into reg_transaction objects.
8 min read | Intermediate Level
What You'll Learn
- What a UVM monitor does, and why it never drives signals
- How an analysis port broadcasts captured data to other components
- How to reconstruct a reg_transaction by watching reg_if
- Why a monitor needs to know the same timing a driver does
What Is a Monitor?
A UVM monitor captures signal activity from a design interface and turns it into transaction-level data, the same kind of object a driver consumes, except the monitor never writes to the interface - it only watches. To do that job, a monitor needs two things: a virtual interface handle to watch, and an analysis port to broadcast whatever it captures to other components, like a scoreboard.
A monitor extending uvm_monitor should generally stick to three responsibilities:
- Collect signal information through a virtual interface
- Optionally use that data for basic protocol checking and coverage
- Export the collected data through an analysis port
Keep a monitor's job limited to what's always required: capturing data, plus simple, low-level checks. Anything resembling high-level functional checking - "did this read return the value the most recent write to that address set" - belongs in a scoreboard, not the monitor. The monitor's job is to report what happened on the bus accurately, not to judge whether it was correct.
Building reg_monitor
1. Declare the Class
class reg_monitor extends uvm_monitor;
`uvm_component_utils(reg_monitor)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// Code for rest of the steps come here
endclass
This follows the same shape as reg_driver's declaration - both extend a base class built on uvm_component, both register with the factory the same way. The difference between a monitor and a driver isn't in how they're declared; it's entirely in what they do inside run_phase.
2. Declare the Analysis Port and Interface Handle
virtual reg_if vif;
uvm_analysis_port #(reg_transaction) mon_analysis_port;
mon_analysis_port is how reg_monitor tells the rest of the testbench what it saw, without needing to know who's listening. Anything connected to this port - a scoreboard, a coverage collector, more than one of either - receives a copy of every reg_transaction the monitor writes to it.
How uvm_analysis_port actually works, and how to connect one to a subscriber on the other end, is covered in full in UVM TLM Analysis Port. For now, treat it as a broadcast mechanism: write to it, and anything connected receives a copy.
3. Build the Monitor
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon_analysis_port = new("mon_analysis_port", this);
if (!uvm_config_db #(virtual reg_if)::get(this, "", "vif", vif)) begin
`uvm_error(get_type_name(), "Didn't get handle to virtual interface reg_if")
end
endfunction
Two things happen here. mon_analysis_port is constructed with plain new(), not the factory's create() - analysis ports aren't registered with the factory, since there's never a reason to override one. The virtual interface handle is retrieved the same way reg_driver retrieved it, with one difference worth noticing: a missing handle here uses uvm_error, not uvm_fatal. A driver with no interface can't do anything at all, so it stops the test immediately. A monitor missing its interface can technically keep running - it just won't capture anything useful, which is still a problem worth flagging loudly, but not necessarily one that has to halt the simulation outright.
4. Drive run_phase - Watching, Not Driving
This is where a monitor and a driver genuinely differ. reg_driver's run_phase decides what to put onto reg_if. reg_monitor's run_phase only ever reads from it - but it still needs to know the exact same timing the driver does, since it's reconstructing the same protocol from the watching side.
virtual task run_phase(uvm_phase phase);
reg_transaction tr;
forever begin
@(posedge vif.clk);
if (vif.wr_en) begin
tr = reg_transaction::type_id::create("tr", this);
tr.wr_en = 1;
tr.addr = vif.addr;
tr.wdata = vif.wdata;
mon_analysis_port.write(tr);
end
else if (vif.rd_en) begin
tr = reg_transaction::type_id::create("tr", this);
tr.rd_en = 1;
tr.addr = vif.addr;
// rdata isn't valid until one cycle after rd_en, same as the driver waits
@(posedge vif.clk);
tr.rdata = vif.rdata;
mon_analysis_port.write(tr);
end
end
endtask
Notice the second @(posedge vif.clk) inside the read branch. It's there for exactly the same reason reg_driver needed a third clock wait before sampling rdata: reg_block schedules rdata to update via a non-blocking assignment at the same edge rd_en is sampled, and that update doesn't settle until after the edge finishes. The monitor has no special insight into the DUT's internals - it has to wait out the same cycle of latency the driver does, because it's watching the same signals at the same times.
Quick Check
Q: Why does the write branch not need a second clock wait, while the read branch does?
Show Answer
A write commits inside reg_block on the very edge wr_en is sampled high - mem[addr] <= wdata happens immediately at that edge. There's nothing further to wait for, since the monitor isn't trying to observe anything beyond the request itself. A read is different: the requested data, rdata, isn't produced until that same edge's non-blocking assignment settles, which is after the edge - so capturing it correctly requires waiting for the next edge.
Optional Knobs: Checking and Coverage
A monitor can include simple, bit-controlled knobs to turn basic checks or coverage sampling on and off, without those features always being active:
bit enable_check = 1;
bit enable_coverage = 1;
virtual task run_phase(uvm_phase phase);
...
if (enable_check)
check_protocol(tr);
if (enable_coverage)
cg_trans.sample();
...
endtask
Designing functional coverage groups like cg_trans is a topic of its own. The pattern to remember here is just the knob itself - a plain bit that gates whether a piece of optional monitor behavior runs.
These knobs can be flipped from a test, without touching the monitor's code, using the same uvm_config_db mechanism that supplies the virtual interface:
uvm_config_db #(bit)::set(this, "*.agt0.monitor", "enable_check", 0);
uvm_config_db #(bit)::set(this, "*.agt0.monitor", "enable_coverage", 0);
Why Extend uvm_monitor at All?
Nothing about a monitor's behavior strictly requires extending uvm_monitor instead of plain uvm_component - uvm_monitor doesn't add new methods on top of it. The reasons to use it anyway are about communicating intent: it lets tools, base-class checks, and other engineers reading the code immediately tell monitors apart from drivers, scoreboards, and other component types by their class, and it means any future additions UVM makes to the base monitor class apply to your monitor automatically.
Common Mistakes
Mistake #1: Driving a signal from inside a monitor
A monitor that writes to the interface it's supposed to be passively observing breaks the entire point of having a separate driver and monitor - now two components can contend for the same signals. If a monitor needs something written to the interface, that's a sign the logic belongs in the driver instead.
Mistake #2: Sampling rdata on the same edge as rd_en
This is the same race reg_driver had to avoid. Capturing vif.rdata on the same edge rd_en is detected risks reading a stale value from before reg_block's own update settles. A monitor has to wait the same extra cycle a driver does - watching the bus doesn't grant any shortcut around the DUT's actual timing.
Practice Exercise
Challenge: Add a Transaction Counter
Add a counter to reg_monitor that tracks how many transactions it has captured in total, and print the count using uvm_info each time a new one is written to the analysis port.
Hint
Declare an integer field on the class, increment it right before or after each mon_analysis_port.write(tr) call, and use $sformatf to build the message string for uvm_info.
Solution
int unsigned tr_count = 0;
// Inside run_phase, right before each mon_analysis_port.write(tr) call:
tr_count++;
`uvm_info(get_type_name(), $sformatf("Captured transaction #%0d", tr_count), UVM_LOW)
mon_analysis_port.write(tr);
Since both the write and read branches call mon_analysis_port.write(tr), placing the increment and the uvm_info call right before each one covers both cases without duplicating logic into a separate function.
Summary
In this article, you learned:
- A monitor captures signal activity and exports it as transaction objects, but never drives the interface it watches
- An analysis port broadcasts captured data to whatever else is connected to it, without the monitor needing to know who's listening
- Reconstructing a transaction from the bus requires the same timing knowledge a driver needs, just from the observing side
- Optional knobs let a monitor's checking and coverage behavior be turned on or off from a test, without touching its code
Going Deeper
- Analysis ports and multiple subscribers: UVM TLM Analysis Port
- Ports, exports and imps: UVM TLM