UVM Basics

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)
endclass

y 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
endclass

A few things worth noticing:

  • uvm_monitor adds nothing to uvm_component — it's a pure naming convention, exactly the reason uvm_test exists (chapter 3). Using it instead of plain uvm_component just marks this component's role.
  • ap = new("ap", this), not type_id::create(...). TLM ports aren't uvm_component/uvm_object subclasses that go through the factory (chapter 6) — they're plain constructed objects, so an ordinary new(...) call is the correct, idiomatic way to build one.
  • The monitor reuses the exact same virtual mux2_if.tb_mp view and the exact same config_db::get() pattern the driver uses — chapter 5's "*"-wildcarded set() 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 #1 gives the combinational logic the same settling time the driver's own drive() task waits — after which vif.y reflects the result of whatever was just driven.
  • Reading vif.sel/vif.a/vif.b through the tb_mp modport, even though tb_mp declares them output, is legal — a modport's output direction 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's mux2_if doesn't bother, since reusing tb_mp is 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. Calling write() on an unconnected analysis_port isn'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
endclass

uvm_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
endclass

my_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 hands t to every connected receiver, with no queuing — the real reason it replaces SV ch14's mailbox, not just a rename of the same idea.
  • A monitor observes without driving: same virtual interface and config_db::get() pattern the driver uses, but only reading signals, never assigning them.
  • uvm_monitor adds nothing over uvm_component — like uvm_test, it's a naming convention marking a component's role.
  • TLM ports are constructed with plain new(...), not type_id::create(...) — they aren't factory-registered types.
  • uvm_agent bundles a sequencer, driver, and monitor; get_is_active() (backed by a built-in is_active field) 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?