UVM-16: The Monitor — VLSI Trainers
VLSI Trainers UVM Series · UVM-16
UVM Series · UVM-16

The Monitor

The monitor as the sole observer of DUT activity — the golden rules of monitoring, transaction assembly from signals, the analysis port broadcast, coverage sampling hooks, multi-stream monitors, and the uvm_subscriber shortcut pattern.

📋 What the Monitor Does

The monitor is the only analysis component that has direct access to the DUT’s signals via the virtual interface. Every other analysis component — the scoreboard, coverage collector, metric analyzer — receives abstract transactions from the monitor, not raw signals. This makes the monitor the single point of conversion between the signal world and the transaction world on the observation side.

Unlike the driver, which actively drives signals, the monitor is entirely passive. It samples signals but never drives them. It assembles what it sees into transaction objects and broadcasts them through its analysis port. It never blocks the DUT, never modifies signals, and never introduces delays into the interface.

Monitor — Signal World to Transaction World, Passively DUT PCLK, PADDR PSEL, PENABLE PRDATA, PREADY PSLVERR Monitor ★ sample signals at posedge detect transfer completion assemble seq_item ap.write(txn) → observe (no drive) Scoreboard Coverage Collector Metric Analyzer Zero subscribers: write() is a no-op, no error raised. Any number OK. vlsitrainers.com
Figure 1 — Monitor position in the analysis flow. The monitor passively samples the DUT interface (no driving), assembles completed transactions, and broadcasts them via ap.write(). Any number of subscribers can connect — zero, one, or many. If no subscribers are connected, write() is a silent no-op.

📋 Golden Rules of Monitoring

Reusing a transaction handle across write() calls corrupts all subscribers’ data. After ap.write(txn), every subscriber holds a reference to the same object. If the monitor reuses txn and writes new signal values into it, every subscriber’s previously-received handle now points to the updated data. Always call type_id::create() for each new transaction.

📋 Monitor Anatomy

The monitor extends uvm_monitor which extends uvm_component. Its structure mirrors the driver — build_phase retrieves the virtual interface; run_phase loops forever detecting and assembling transactions.

class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)

  virtual apb_if                    vif;
  uvm_analysis_port #(apb_seq_item) ap;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // ① Create the analysis port
    ap = new("ap", this);
    // ② Get virtual interface
    if (!uvm_config_db #(virtual apb_if)::get(
          this, "", "vif", vif))
      `uvm_fatal("MON", "No virtual interface!")
  endfunction

  task run_phase(uvm_phase phase);
    forever begin
      collect_transaction();
    end
  endtask

  task collect_transaction();
    apb_seq_item txn;
    // ③ Wait for transfer completion signal
    @(posedge vif.PCLK);
    if (vif.PSELx && vif.PENABLE && vif.PREADY) begin
      // ④ Create a NEW item — never reuse
      txn = apb_seq_item::type_id::create("txn");
      // ⑤ Sample all fields from the interface
      txn.addr           = vif.PADDR;
      txn.read_not_write = ~vif.PWRITE;
      txn.byte_en        = vif.PSTRB;
      txn.pprot          = vif.PPROT;
      txn.error          = vif.PSLVERR;
      if (vif.PWRITE) txn.write_data = vif.PWDATA;
      else            txn.read_data  = vif.PRDATA;
      // ⑥ Broadcast to all subscribers
      ap.write(txn);
    end
  endtask
endclass

📋 Transaction Assembly

The monitor must reconstruct a complete transaction from the individual clock-cycle signal snapshots. For protocols with multi-cycle handshakes (like APB’s SETUP→ACCESS→READY sequence), the monitor must track state across cycles to know when a complete transaction has occurred.

The transaction completion trigger depends on the protocol:

ProtocolCompletion triggerWhat to capture
APBPSELx & PENABLE & PREADY at posedge PCLKPADDR, PWRITE, PWDATA/PRDATA, PSTRB, PPROT, PSLVERR
AHBHREADY at end of data phaseHADDR, HWRITE, HWDATA/HRDATA, HTRANS, HRESP
SPICS deasserted after N bits shiftedMOSI data shifted in, MISO data shifted out, CS width
I2CSTOP condition detectedDevice address, direction, all data bytes, ACK/NACK
AXILast beat: VALID & READY & LASTID, address, length, data beats, BRESP/RRESP

Partial-transaction accumulation example

// ── AHB-style: address and data on different cycles ──────
task collect_transaction();
  ahb_seq_item txn;
  logic [31:0] addr_latch;
  logic         write_latch;

  // Phase 1: capture address phase
  @(posedge vif.HCLK);
  if (vif.HTRANS != IDLE && vif.HREADY) begin
    addr_latch  = vif.HADDR;
    write_latch = vif.HWRITE;

    // Phase 2: capture data phase (next cycle)
    @(posedge vif.HCLK);
    while (!vif.HREADY) @(posedge vif.HCLK);  // wait out wait-states

    txn = ahb_seq_item::type_id::create("txn");
    txn.addr       = addr_latch;
    txn.read_write = write_latch;
    txn.hwdata     = vif.HWDATA;
    txn.hrdata     = vif.HRDATA;
    txn.hresp      = vif.HRESP;
    ap.write(txn);
  end
endtask

📋 Complete APB Monitor

class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)

  virtual apb_if                    vif;
  uvm_analysis_port #(apb_seq_item) ap;

  // Optional: transaction count for reporting
  int unsigned txn_count = 0;

  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 apb_if)::get(
          this, "", "vif", vif))
      `uvm_fatal("MON", "No virtual interface!")
  endfunction

  task run_phase(uvm_phase phase);
    // Wait for reset before monitoring
    @(posedge vif.PRESETn);
    forever collect_transfer();
  endtask

  task collect_transfer();
    apb_seq_item txn;
    @(posedge vif.PCLK);
    if (vif.PSELx && vif.PENABLE && vif.PREADY) begin
      txn                = apb_seq_item::type_id::create("txn");
      txn.addr           = vif.PADDR;
      txn.read_not_write = ~vif.PWRITE;
      txn.byte_en        = vif.PSTRB;
      txn.pprot          = vif.PPROT;
      txn.error          = vif.PSLVERR;
      if (vif.PWRITE) txn.write_data = vif.PWDATA;
      else            txn.read_data  = vif.PRDATA;
      txn_count++;
      `uvm_info("MON", txn.convert2string(), UVM_HIGH)
      ap.write(txn);
    end
  endtask

  function void report_phase(uvm_phase phase);
    `uvm_info("MON",
      $sformatf("Monitored %0d APB transfers", txn_count),
      UVM_LOW)
  endfunction
endclass

📋 Coverage Sampling Hook

One common pattern is to embed a functional coverage sampling call directly into the monitor. This keeps the coverage sampling tightly coupled to the exact moment a transaction is observed. The coverage group is declared inside the monitor class and sampled after every ap.write().

class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)

  virtual apb_if                    vif;
  uvm_analysis_port #(apb_seq_item) ap;

  // ── Embedded coverage group ────────────────────────────
  apb_seq_item txn_for_cov;     // handle for covergroup sampling

  covergroup apb_cov;
    cp_addr: coverpoint txn_for_cov.addr {
      bins gpio_regs[] = {12'h000, 12'h004, 12'h008};
    }
    cp_rw: coverpoint txn_for_cov.read_not_write {
      bins rd = {1}; bins wr = {0};
    }
    cp_err: coverpoint txn_for_cov.error;
    cx_rw_addr: cross cp_rw, cp_addr;
  endgroup

  function new(string name, uvm_component parent);
    super.new(name, parent);
    apb_cov = new();  // instantiate covergroup in constructor
  endfunction

  task collect_transfer();
    apb_seq_item txn;
    @(posedge vif.PCLK);
    if (vif.PSELx && vif.PENABLE && vif.PREADY) begin
      txn = apb_seq_item::type_id::create("txn");
      txn.addr = vif.PADDR; txn.read_not_write = ~vif.PWRITE;
      txn.byte_en = vif.PSTRB; txn.error = vif.PSLVERR;
      if (vif.PWRITE) txn.write_data = vif.PWDATA;
      else            txn.read_data  = vif.PRDATA;
      txn_for_cov = txn;
      apb_cov.sample();   // sample covergroup
      ap.write(txn);      // broadcast to subscribers
    end
  endtask
endclass
Alternative: dedicated coverage subscriber. Embedding coverage in the monitor ties the coverage model to the monitor class. A cleaner separation is to create a dedicated apb_cov_collector component that extends uvm_subscriber, receives transactions via its analysis export, and samples coverage in its write() function. This allows the coverage model to be enabled or disabled independently of the monitor.

📋 Multiple Analysis Ports

A single monitor can expose multiple analysis ports when different subscribers need different transaction views — for example, one port for the scoreboard (full transaction) and another for a timing analyzer (timestamps only).

class apb_monitor extends uvm_monitor;
  `uvm_component_utils(apb_monitor)

  virtual apb_if                    vif;

  // Port 1: full transaction for scoreboard
  uvm_analysis_port #(apb_seq_item) ap;

  // Port 2: error transactions only — for error tracker
  uvm_analysis_port #(apb_seq_item) error_ap;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ap       = new("ap",       this);
    error_ap = new("error_ap", this);
    if (!uvm_config_db #(virtual apb_if)::get(
          this, "", "vif", vif))
      `uvm_fatal("MON", "No vif!")
  endfunction

  task collect_transfer();
    apb_seq_item txn;
    @(posedge vif.PCLK);
    if (vif.PSELx && vif.PENABLE && vif.PREADY) begin
      txn = apb_seq_item::type_id::create("txn");
      txn.addr = vif.PADDR; txn.error = vif.PSLVERR;
      ap.write(txn);                      // all transactions
      if (txn.error) error_ap.write(txn); // errors only
    end
  endtask
endclass

📋 uvm_subscriber Pattern

uvm_subscriber #(T) is a convenience base class that combines a uvm_component with a built-in uvm_analysis_imp export and an abstract write() function. Any class extending it only needs to implement write() — the port and connection machinery is handled automatically.

// ── Coverage collector using uvm_subscriber ──────────────
class apb_cov_collector extends uvm_subscriber #(apb_seq_item);
  `uvm_component_utils(apb_cov_collector)

  apb_seq_item txn;

  covergroup apb_xfer_cg;
    cp_addr: coverpoint txn.addr {
      bins regs[] = {12'h000, 12'h004, 12'h008};
    }
    cp_rw:  coverpoint txn.read_not_write;
    cp_err: coverpoint txn.error;
    cx:     cross cp_rw, cp_addr;
  endgroup

  function new(string name, uvm_component parent);
    super.new(name, parent);
    apb_xfer_cg = new();
  endfunction

  // ── write() called by monitor via ap.write(txn) ──────────
  function void write(apb_seq_item t);
    txn = t;
    apb_xfer_cg.sample();
  endfunction
endclass

// ── In env connect_phase: one line to subscribe ───────────
m_agent.ap.connect(m_cov.analysis_export);

📋 Common Pitfalls

PitfallSymptomFix
Reusing the same txn handleScoreboard sees same data for every transaction. Coverage sampled with last value only.Create a new item with type_id::create() for every completed transfer.
Driving signals in monitorDUT stimulus corrupted. Unexpected behaviour during simulation.The monitor is read-only on vif. Never use <= or = on interface signals.
Missing reset detectionMonitor assembles partial transactions across a reset boundary. Spurious transactions reported.Add a reset detection thread that flushes state when PRESETn deasserts.
Sampling at the wrong clock edgeSignals captured before they have settled — race conditions at fast clocks.Use clocking block with input skew, or sample at @(posedge PCLK) after #1step.
Blocking inside write()Simulator error — write() is a function and cannot block. Using @(event) inside write() will fail to compile.write() must be a pure function. Move time-consuming analysis to run_phase using a FIFO or event.
Analysis port not created in build_phaseNull handle crash when ap.write() is called in run_phase.Always create the analysis port in build_phase with ap = new("ap", this).

📋 Quick Reference

ItemKey fact
Base classuvm_monitor extends uvm_component
Registration`uvm_component_utils(ClassName)
Monitor rolePassive observer — samples signals, assembles transactions, broadcasts via analysis port. Never drives.
Analysis port declarationuvm_analysis_port #(T) ap; created in build_phase with new("ap", this)
Broadcast callap.write(txn) — function, never blocks, delivers to all connected subscribers
Zero subscribersAllowed — ap.write() is a no-op, no error raised
New txn per transferAlways type_id::create("txn") — never reuse same handle
APB completion triggerPSELx & PENABLE & PREADY at posedge PCLK
Clocking block input skewUse vif.monitor_cb.SIGNAL to sample at defined offset — avoids races
Coverage samplingAssign handle, sample covergroup, then ap.write() — or use dedicated subscriber
uvm_subscriber purposeCombines component + analysis_imp + write() stub — simplest way to build a subscriber
Multiple analysis portsDeclare multiple ports; write to each selectively based on transaction content
write() constraintMust be a function — no blocking, no time consumption, no @(event) inside
report_phasePrint total transaction count, coverage summary, timing stats
Scroll to Top