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.
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.
write() is a function, not a task — it cannot block. The monitor must not perform time-consuming work inside write(). Subscribers receive the transaction and process it in their own time.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.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
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:
| Protocol | Completion trigger | What to capture |
|---|---|---|
| APB | PSELx & PENABLE & PREADY at posedge PCLK | PADDR, PWRITE, PWDATA/PRDATA, PSTRB, PPROT, PSLVERR |
| AHB | HREADY at end of data phase | HADDR, HWRITE, HWDATA/HRDATA, HTRANS, HRESP |
| SPI | CS deasserted after N bits shifted | MOSI data shifted in, MISO data shifted out, CS width |
| I2C | STOP condition detected | Device address, direction, all data bytes, ACK/NACK |
| AXI | Last beat: VALID & READY & LAST | ID, address, length, data beats, BRESP/RRESP |
// ── 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
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
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
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.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 #(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);
| Pitfall | Symptom | Fix |
|---|---|---|
| Reusing the same txn handle | Scoreboard 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 monitor | DUT stimulus corrupted. Unexpected behaviour during simulation. | The monitor is read-only on vif. Never use <= or = on interface signals. |
| Missing reset detection | Monitor 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 edge | Signals 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_phase | Null handle crash when ap.write() is called in run_phase. | Always create the analysis port in build_phase with ap = new("ap", this). |
| Item | Key fact |
|---|---|
| Base class | uvm_monitor extends uvm_component |
| Registration | `uvm_component_utils(ClassName) |
| Monitor role | Passive observer — samples signals, assembles transactions, broadcasts via analysis port. Never drives. |
| Analysis port declaration | uvm_analysis_port #(T) ap; created in build_phase with new("ap", this) |
| Broadcast call | ap.write(txn) — function, never blocks, delivers to all connected subscribers |
| Zero subscribers | Allowed — ap.write() is a no-op, no error raised |
| New txn per transfer | Always type_id::create("txn") — never reuse same handle |
| APB completion trigger | PSELx & PENABLE & PREADY at posedge PCLK |
| Clocking block input skew | Use vif.monitor_cb.SIGNAL to sample at defined offset — avoids races |
| Coverage sampling | Assign handle, sample covergroup, then ap.write() — or use dedicated subscriber |
| uvm_subscriber purpose | Combines component + analysis_imp + write() stub — simplest way to build a subscriber |
| Multiple analysis ports | Declare multiple ports; write to each selectively based on transaction content |
| write() constraint | Must be a function — no blocking, no time consumption, no @(event) inside |
| report_phase | Print total transaction count, coverage summary, timing stats |