Chapter 8 of 10
Driver and Monitor: TLM Ports in Practice
The monitor's push-style analysis_port versus the sequencer's pull-style seq_item_port, why that replaces SV ch14's hand-rolled mailbox, giving mux_transaction an observed-output field, and packaging sequencer/driver/monitor into a configurable uvm_agent.
Chapter 7 already gave the driver its seq_item_port and its final run_phase shape. What's still missing from chapter 1's fourth promise — reusable verification IP, not a bespoke mailbox handshake — is the other half of the picture: a monitor, and the standardized way it broadcasts what it observes.
Two shapes of TLM port: pull vs. push
Chapter 7's seq_item_port is a pull: the driver actively calls get_next_item() and blocks until something's available. A monitor's job is different — it doesn't consume a queue of pending work, it just watches the DUT and announces what it sees, to however many listeners happen to be interested (a scoreboard, in chapter 9; potentially more than one thing at once, though this track only ever connects one). That's a push: uvm_analysis_port#(T), with a write(T t) call that immediately hands t to every connected receiver.
This is the real reason analysis_port is the standardized replacement for the capstone's hand-rolled mailbox (SV ch14), not just a renamed version of the same thing: a mailbox queues items for exactly one process to get() later, one at a time. write() doesn't queue anything — it calls every connected receiver's own write() method immediately and synchronously, and works the same way whether zero, one, or several things are listening. A monitor doesn't need to know or care who's downstream; it just calls write() and moves on.
Giving mux_transaction an observed output
Chapter 4's mux_transaction only needed sel/a/b — everything the driver drives. The monitor also needs to capture what the DUT actually produced, so it gets one more field:
class mux_transaction extends uvm_sequence_item;
`uvm_object_utils(mux_transaction)
rand bit sel;
rand bit a;
rand bit b;
bit y; // observed DUT output -- not randomized, the monitor fills this in
// ... constructor unchanged ...
function void do_copy(uvm_object rhs);
mux_transaction rhs_;
if (!$cast(rhs_, rhs))
`uvm_fatal("DO_COPY", "rhs is not a mux_transaction")
super.do_copy(rhs);
sel = rhs_.sel;
a = rhs_.a;
b = rhs_.b;
y = rhs_.y;
endfunction
function string convert2string();
return $sformatf("sel=%0b a=%0b b=%0b y=%0b", sel, a, b, y);
endfunction
// do_compare gets the same one-line addition: && (y == rhs_.y)
endclassy isn't rand — nothing should ever randomize a field meant to record what the DUT actually did.
Writing the monitor
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
virtual mux2_if.tb_mp vif;
uvm_analysis_port #(mux_transaction) ap;
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 mux2_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("MON", "no virtual interface set for vif -- check the config_db::set() call in the top module")
endfunction
task run_phase(uvm_phase phase);
mux_transaction tr;
forever begin
@(vif.sel, vif.a, vif.b);
#1; // let the combinational logic settle, same wait the driver uses
tr = mux_transaction::type_id::create("tr");
tr.sel = vif.sel;
tr.a = vif.a;
tr.b = vif.b;
tr.y = vif.y;
ap.write(tr);
end
endtask
endclassA few things worth noticing:
uvm_monitoradds nothing touvm_component— it's a pure naming convention, exactly the reasonuvm_testexists (chapter 3). Using it instead of plainuvm_componentjust marks this component's role.ap = new("ap", this), nottype_id::create(...). TLM ports aren'tuvm_component/uvm_objectsubclasses that go through the factory (chapter 6) — they're plain constructed objects, so an ordinarynew(...)call is the correct, idiomatic way to build one.- The monitor reuses the exact same
virtual mux2_if.tb_mpview and the exact sameconfig_db::get()pattern the driver uses — chapter 5's"*"-wildcardedset()in the top module was already reaching every component that asks for"vif"; the monitor is simply a second one asking. @(vif.sel, vif.a, vif.b)waits for any of those three signals to change, then#1gives the combinational logic the same settling time the driver's owndrive()task waits — after whichvif.yreflects the result of whatever was just driven.- Reading
vif.sel/vif.a/vif.bthrough thetb_mpmodport, even thoughtb_mpdeclares themoutput, is legal — a modport'soutputdirection restricts who may write a signal, not who may read one you can already write. (A fully polished testbench would give the monitor its own read-only modport view; this track'smux2_ifdoesn't bother, since reusingtb_mpis simpler and equally legal.) ap.write(tr)has nothing connected to it yet. Nothing is listening at the end of this chapter — that's chapter 9's job. Callingwrite()on an unconnectedanalysis_portisn't an error; it's simply a broadcast with zero current listeners.
Packaging it up: uvm_agent
Chapter 1 already introduced uvm_agent as bundling a sequencer, driver, and monitor, configurable as active or passive. Now that all three exist, packaging them is close to mechanical:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
uvm_sequencer #(mux_transaction) sqr;
my_monitor mon;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
sqr = uvm_sequencer#(mux_transaction)::type_id::create("sqr", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclassuvm_agent adds exactly one thing on top of uvm_component: a built-in is_active field (an enum, UVM_ACTIVE by default), read here through get_is_active(). The monitor is built unconditionally — observing doesn't require driving. The driver and sequencer are only built, and only connected, when the agent is active; a passive agent (configurable the same config_db way chapter 5 covered, by setting the agent's is_active field before build_phase runs) drops both and keeps only the monitor — useful for a bus side that should be watched but never driven.
The test, simplified
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_agent agent;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = my_agent::type_id::create("agent", this);
endfunction
task run_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(agent.sqr);
phase.drop_objection(this);
endtask
endclassmy_test no longer manages drv/sqr individually — my_agent owns that wiring internally, and the test just reaches through it (agent.sqr) to start a sequence.
Summary
seq_item_port(chapter 7) is a pull: the driver requests, and blocks until something's available.uvm_analysis_port#(T)is a push:write(t)immediately handstto every connected receiver, with no queuing — the real reason it replaces SV ch14'smailbox, not just a rename of the same idea.- A monitor observes without driving: same
virtual interfaceandconfig_db::get()pattern the driver uses, but only reading signals, never assigning them. uvm_monitoradds nothing overuvm_component— likeuvm_test, it's a naming convention marking a component's role.- TLM ports are constructed with plain
new(...), nottype_id::create(...)— they aren't factory-registered types. uvm_agentbundles a sequencer, driver, and monitor;get_is_active()(backed by a built-inis_activefield) decides whether the driver/sequencer get built and connected at all, or whether the agent is monitor-only (passive).
What's the real functional difference between seq_item_port's pull and uvm_analysis_port's push, beyond just naming?
Why is the monitor's ap = new('ap', this); written with new(), while my_monitor itself is constructed with my_monitor::type_id::create('mon', this)?
In my_agent's build_phase, why is mon built unconditionally while drv and sqr are only built when get_is_active() == UVM_ACTIVE?
At the end of this chapter, mon.ap.write(tr) is called every time the monitor observes activity, but nothing is connected to mon.ap yet. What happens?