Skip to content

Analysis Ports

Broadcast model, write() protocol, one-to-many fan-out, uvm_analysis_imp_decl.

UVM Fundamentals · Module 13

Analysis Ports vs TLM 1.0 — The Key Differences

Module 12 covered TLM 1.0 ports — point-to-point connections where the initiator and target collaborate on timing. Analysis ports are a completely different model: fire-and-forget, non-blocking, one-to-many broadcast. They serve a different purpose and follow different rules. TLM 1.0 Ports (put, get, peek)

  • Point-to-point — one port, one target
  • Methods are tasks — can consume simulation time
  • Blocking by default — caller waits for response
  • Request-response protocol possible (get / peek)
  • Used for: driver ↔ sequencer, producer ↔ consumer pipelines
  • Connection required — unconnected port causes fatal Analysis Ports (write)
  • One-to-many — one port, any number of subscribers (including zero)
  • Method is a function — cannot consume simulation time
  • Non-blocking — write() returns immediately
  • One-way only — no response from subscriber to publisher
  • Used for: monitor → scoreboard, monitor → coverage, monitor → logger
  • Zero connections is valid — no fatal if no subscriber is connected

The Analysis Port — What the Publisher Declares

The publisher (monitor) declares a uvm_analysis_port and calls write(txn) when it has an observed transaction. The port is responsible for calling every connected subscriber's write() function. apb_monitoruvm_analysis_port #(T)analysis_port.write(txn)Calls write() on allconnected subscriberswrite(txn)write(txn)write(txn)my_scoreboarduvm_analysis_imp #(T, scb)implements write(T txn)my_coverageuvm_analysis_imp #(T, cov)my_loggeruvm_analysis_imp #(T, log)connect_phase:mon.ap.connect (scb.ap_imp)mon.ap.connect (cov.ap_imp)mon.ap.connect (log.ap_imp)Each connect() adds onesubscriber to the fanout list Figure 1 — Analysis port broadcast. One monitor writes one transaction. All three subscribers (scoreboard, coverage, logger) receive it simultaneously via separate write() calls. The monitor has no knowledge of its subscribers.

SystemVerilog — declaring and using an analysis port in a monitor
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
 
// Analysis port: "I will broadcast transactions to whoever is listening"
uvm_analysis_port #(apb_seq_item) analysis_port;
// Convention: name it "analysis_port" or "ap" — be consistent across your VIP
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
analysis_port = new("analysis_port", this);
endfunction
 
task run_phase(uvm_phase phase);
apb_seq_item txn;
forever begin
// Observe the bus and build a transaction object
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
txn = apb_seq_item::type_id::create("txn");
txn.addr  = vif.paddr;
txn.data  = vif.prdata;
txn.write = vif.pwrite;
 
// Broadcast to all connected subscribers — function call, non-blocking
analysis_port.write(txn);
// If no subscribers: does nothing, no error
// If 3 subscribers: calls write(txn) on each, in connection order
end
endtask
 
virtual apb_if vif;
endclass

The Analysis Imp — What the Subscriber Declares

Each subscriber declares a uvm_analysis_imp and implements the write() function. The second parameter of the imp is the component class that will provide the write() implementation — just like uvm_blocking_put_imp.

SystemVerilog — scoreboard with analysis_imp
class my_scoreboard extends uvm_scoreboard;
`uvm_component_utils(my_scoreboard)
 
// Analysis imp: "I receive transactions from an analysis port"
// Second parameter = this class (write() is a method of my_scoreboard)
uvm_analysis_imp #(apb_seq_item, my_scoreboard) ap_imp;
 
apb_seq_item expected_q[$];   // queue of expected transactions
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap_imp = new("ap_imp", this);
endfunction
 
// write() IS the callback — called by the analysis_port on each transaction
// Must be a FUNCTION — cannot have @(posedge) or any time-consuming code
function void write(apb_seq_item txn);
apb_seq_item exp;
 
if (expected_q.size() == 0) begin
`uvm_error("SCB", "Received transaction but expected queue is empty")
return;
end
exp = expected_q.pop_front();
 
if (txn.addr != exp.addr || txn.data != exp.data) begin
`uvm_error("SCB", $sformatf(
"MISMATCH: got addr=0x%0h data=0x%0h, expected addr=0x%0h data=0x%0h",
txn.addr, txn.data, exp.addr, exp.data))
end else
`uvm_info("SCB", "Transaction match ✓", UVM_HIGH)
endfunction
 
endclass
 
// Connection in my_env.connect_phase:
// mon.analysis_port.connect(scb.ap_imp);

One-to-Many Fan-Out — Multiple Subscribers

Each connect() call in connect_phase adds one subscriber to the analysis port's internal list. When write(txn) is called, the port iterates through that list and calls each subscriber's write() function.

SystemVerilog — connecting multiple subscribers to one analysis port
// ── Three subscribers, one analysis port ──────────────────────────────
class my_env extends uvm_env;
apb_monitor   mon;
my_scoreboard scb;
my_coverage   cov;
my_logger     log_comp;
 
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
 
// Each connect() call adds one subscriber to the analysis_port's list
mon.analysis_port.connect(scb.ap_imp);       // subscriber 1
mon.analysis_port.connect(cov.ap_imp);       // subscriber 2
mon.analysis_port.connect(log_comp.ap_imp);  // subscriber 3
 
// When mon calls analysis_port.write(txn), it calls in sequence:
//   scb.write(txn)      ← subscriber 1
//   cov.write(txn)      ← subscriber 2
//   log_comp.write(txn) ← subscriber 3
// All three are called synchronously, in the order they were connected
endfunction
endclass
 
// ── Adding and removing subscribers dynamically (advanced) ────────────
// Analysis port maintains an internal array of subscriber handles
// You can query the subscriber count:
int n = mon.analysis_port.size();   // returns number of connected subscribers

Multiple Imps in One Component — The `uvm_analysis_imp_decl Macro

A scoreboard often needs to receive from two monitors: the request monitor (what was sent to the DUT) and the response monitor (what the DUT returned). A component cannot have two uvm_analysis_imp declarations with different types — a single class can only have one write() method signature.

The solution is the ``uvm_analysis_imp_declmacro. It generates a newuvm_analysis_imp_<SUFFIX>class with awrite_<SUFFIX>()callback instead ofwrite()`. Each suffix creates an entirely distinct TLM class.

SystemVerilog — multiple analysis imps using uvm_analysis_imp_decl
// ── Step 1: Declare the suffix macros OUTSIDE any class ───────────────
// (typically at the top of the scoreboard package file)
`uvm_analysis_imp_decl(`REQ)   // creates: uvm_analysis_imp_REQ class
`uvm_analysis_imp_decl(`RSP)   // creates: uvm_analysis_imp_RSP class
 
// ── Step 2: Declare both imps in the scoreboard ───────────────────────
class apb_scoreboard extends uvm_scoreboard;
`uvm_component_utils(apb_scoreboard)
 
// REQ imp: receives request transactions (what driver sent)
uvm_analysis_imp_REQ #(apb_req_txn, apb_scoreboard) req_imp;
 
// RSP imp: receives response transactions (what monitor observed back)
uvm_analysis_imp_RSP #(apb_rsp_txn, apb_scoreboard) rsp_imp;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
req_imp = new("req_imp", this);
rsp_imp = new("rsp_imp", this);
endfunction
 
// ── Step 3: Implement write_REQ() and write_RSP() — NOT write() ───
function void write_REQ(apb_req_txn txn);   // called for REQ side
`uvm_info("SCB", $sformatf("REQ: addr=0x%0h", txn.addr), UVM_MEDIUM)
req_q.push_back(txn);
endfunction
 
function void write_RSP(apb_rsp_txn txn);   // called for RSP side
if (req_q.size() == 0) begin
`uvm_error("SCB", "Response received but no pending request")
return;
end
compare(req_q.pop_front(), txn);
endfunction
 
apb_req_txn req_q[$];
function void compare(apb_req_txn req, apb_rsp_txn rsp);
/* check response against request */
endfunction
endclass
 
// ── Connection in env.connect_phase ───────────────────────────────────
// req_monitor.ap.connect(scb.req_imp);   // request path
// rsp_monitor.ap.connect(scb.rsp_imp);   // response path

Analysis Exports — Hierarchical Connectivity

When a monitor is inside an agent and the analysis port needs to be accessible from outside the agent, the agent exposes it through an uvm_analysis_port export at its boundary. In connect_phase, the agent's export is connected to the monitor's actual port.

SystemVerilog — analysis port propagation through agent hierarchy
// ── Agent: exposes monitor's analysis_port at agent boundary ─────────
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
 
apb_monitor       mon;
apb_driver        drv;
uvm_sequencer #(apb_seq_item) seqr;
 
// Agent-level analysis port — a "pass-through" from monitor to outside
// Note: this is also declared as uvm_analysis_port, not uvm_analysis_export
// (UVM analysis ports act as both port and export)
uvm_analysis_port #(apb_seq_item) analysis_port;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon  = apb_monitor::type_id::create("mon", this);
drv  = apb_driver::type_id::create("drv", this);
seqr = uvm_sequencer#(apb_seq_item)::type_id::create("seqr", this);
analysis_port = new("analysis_port", this);
endfunction
 
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(seqr.seq_item_export);
 
// Wire agent's analysis_port to monitor's analysis_port
// Now env can connect scb to agent.analysis_port directly
mon.analysis_port.connect(analysis_port);
endfunction
endclass
 
// ── Env: connects to the agent's exposed analysis_port ────────────────
class my_env extends uvm_env;
apb_agent     agent;
my_scoreboard scb;
 
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Env connects to the agent's exposed port — not directly to the monitor
agent.analysis_port.connect(scb.ap_imp);
// If agent is inside another agent, this same pattern extends cleanly
endfunction
endclass

The write() Protocol — Rules Every Subscriber Must Follow

The write() function has constraints that differ from normal SystemVerilog functions. Violating them causes simulation errors that are hard to diagnose.

RuleReasonConsequence of Violation
write() must be a function, not a taskAnalysis ports call write() inside a function call chain — tasks cannot be called from functions in SystemVerilogCompilation error: "cannot call a task from within a function context"
write() cannot consume simulation timeIt is a function — @(posedge clk), #delay, wait() are illegal inside functionsCompilation error — time-consuming statements in functions are disallowed
Store a clone of the transaction, not the handleThe analysis port passes the same object reference to all subscribers. If a subscriber modifies it, the modification is visible to subsequent subscribersSilent data corruption — later subscribers see modified data
Do not block or wait inside write()All subscriber write() calls happen synchronously, one after another. A slow subscriber blocks all later subscribersSimulation deadlock if any blocking construct is used
SystemVerilog — correct write() patterns and the clone() safety rule
// ── Clone the transaction before storing — critical safety pattern ────
function void write(apb_seq_item txn);
apb_seq_item stored;
 
// ❌ WRONG: store the original handle — shared with all other subscribers
// expected_q.push_back(txn);
// If another subscriber modifies txn, expected_q[last] is silently corrupted
 
// ✓ CORRECT: clone() creates a deep copy — your private object
$cast(stored, txn.clone());   // clone() returns uvm_object, cast to apb_seq_item
expected_q.push_back(stored);   // safe — you own this copy
endfunction
 
// ── Fast write() using a mailbox for async processing ─────────────────
// If write() needs to trigger async processing (e.g., in a thread):
mailbox #(apb_seq_item) txn_mbox;
 
function void write(apb_seq_item txn);
apb_seq_item stored;
$cast(stored, txn.clone());
txn_mbox.try_put(stored);   // try_put() is a function — safe inside write()
// A separate task-based thread reads from txn_mbox and does the heavy processing
endfunction
 
// Companion task (in run_phase):
task run_phase(uvm_phase phase);
apb_seq_item txn;
forever begin
txn_mbox.get(txn);   // blocking get() in a task — fine here
process_transaction(txn);
end
endtask

Quick Reference

TaskDeclaration / Call
Declare analysis port (publisher)uvm_analysis_port #(T) ap;ap = new("ap", this)
Declare analysis imp (subscriber)uvm_analysis_imp #(T, my_comp) ap_imp;ap_imp = new("ap_imp", this)
Broadcast a transactionap.write(txn); — function, non-blocking
Connect subscribermon.ap.connect(scb.ap_imp) — in connect_phase
Connect multiple subscribersCall .connect() once per subscriber — each adds one to the fanout list
Multiple imps in one component``uvm_analysis_imp_decl(SUFFIX) → declares uvm_analysis_imp_SUFFIX → implement write_SUFFIX()
Implement the callbackfunction void write(T txn); — must be function, no time-consuming code
Safe transaction storage$cast(copy, txn.clone()) before pushing to queue
Check subscriber countap.size() — returns number of connected subscribers
SystemVerilog — analysis port cheat sheet
// ── Key differences from TLM 1.0 ─────────────────────────────────────
//   write() is a FUNCTION (not a task) — cannot consume simulation time
//   One-to-MANY — unlimited subscribers via multiple connect() calls
//   Zero connections is VALID — no fatal if nobody is listening
//   Fire-and-forget — no response back to publisher
//   ALL subscribers called synchronously in connection order
 
// ── Minimum publisher code ────────────────────────────────────────────
uvm_analysis_port #(my_txn) ap;
// in build_phase:  ap = new("ap", this);
// in run_phase:    ap.write(txn);
 
// ── Minimum subscriber code ───────────────────────────────────────────
uvm_analysis_imp #(my_txn, my_comp) ap_imp;
// in build_phase:  ap_imp = new("ap_imp", this);
// implement:       function void write(my_txn txn); /* ... */ endfunction
 
// ── Multiple imps (outside any class): ───────────────────────────────
`uvm_analysis_imp_decl(`A)   // → uvm_analysis_imp_A, callback: write_A()
`uvm_analysis_imp_decl(`B)   // → uvm_analysis_imp_B, callback: write_B()
 
// ── Connection (in parent's connect_phase): ───────────────────────────
mon.ap.connect(scb.ap_imp);
mon.ap.connect(cov.ap_imp);   // add as many as needed

§9 — Code Examples

Example 1 — Beginner: Minimal Monitor → Scoreboard Connection

Strip everything away and just see the analysis port pattern in its simplest form. One publisher, one subscriber, one connection, one write() callback.

SystemVerilog — minimal analysis port: monitor to scoreboard
// ── Transaction ────────────────────────────────────────────────────────
class apb_txn extends uvm_sequence_item;
`uvm_object_utils(apb_txn)
logic [31:0] addr;
logic [31:0] data;
logic        write;
function new(string name="apb_txn"); super.new(name); endfunction
endclass
 
// ── Monitor (Publisher) ───────────────────────────────────────────────
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
uvm_analysis_port#(apb_txn) ap;  // industry convention: name it "ap"
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);  // new(), NOT type_id::create()
endfunction
 
task run_phase(uvm_phase phase);
apb_txn txn;
forever begin
@(posedge vif.clk iff (vif.psel && vif.penable && vif.pready));
txn       = apb_txn::type_id::create("txn");
txn.addr  = vif.paddr;
txn.data  = vif.pwrite ? vif.pwdata : vif.prdata;
txn.write = vif.pwrite;
ap.write(txn);   // broadcast — calls write() on each subscriber
end
endtask
virtual apb_if vif;
endclass
 
// ── Scoreboard (Subscriber) ───────────────────────────────────────────
class apb_scoreboard extends uvm_scoreboard;
`uvm_component_utils(apb_scoreboard)
uvm_analysis_imp#(apb_txn, apb_scoreboard) ap_imp;
// ↑ second param = THIS class — write() belongs to apb_scoreboard
apb_txn expected_q[$];
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap_imp = new("ap_imp", this);
endfunction
 
// FUNCTION — not task. No time-consuming code allowed.
function void write(apb_txn txn);
apb_txn stored;
$cast(stored, txn.clone());   // clone! — own private copy
`uvm_info("SCB", $sformatf("Received: addr=0x%0h data=0x%0h",
stored.addr, stored.data), UVM_MEDIUM)
expected_q.push_back(stored);
endfunction
endclass
 
// ── Env: creates + connects ───────────────────────────────────────────
class apb_env extends uvm_env;
apb_monitor    mon;
apb_scoreboard scb;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = apb_monitor::type_id::create("mon", this);
scb = apb_scoreboard::type_id::create("scb", this);
endfunction
 
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.ap.connect(scb.ap_imp);   // THE connection — in connect_phase only
endfunction
endclass

Example 2 — Intermediate: Three Subscribers, One Broadcast

The same transaction arriving simultaneously at a scoreboard, a coverage collector, and a protocol logger — each doing something different with the same data. This is the production pattern for every monitor in a mature VIP.

SystemVerilog — fan-out: scoreboard + coverage + logger
// ── Coverage Collector ────────────────────────────────────────────────
class apb_coverage extends uvm_subscriber#(apb_txn);
`uvm_component_utils(apb_coverage)
// uvm_subscriber provides analysis_export by default — no manual ap_imp needed
 
apb_txn txn_h;   // holds current transaction for covergroup sampling
covergroup apb_cg;
cp_write: coverpoint txn_h.write;
cp_addr:  coverpoint txn_h.addr[7:0];
endgroup
 
function new(string name, uvm_component parent);
super.new(name, parent);
apb_cg = new();
endfunction
 
function void write(apb_txn txn);  // uvm_subscriber provides this hook
apb_txn stored;
$cast(stored, txn.clone());
txn_h = stored;
apb_cg.sample();  // sample coverage with this transaction's fields
endfunction
endclass
 
// ── Protocol Logger ───────────────────────────────────────────────────
class apb_logger extends uvm_component;
`uvm_component_utils(apb_logger)
uvm_analysis_imp#(apb_txn, apb_logger) ap_imp;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap_imp = new("ap_imp", this);
endfunction
 
function void write(apb_txn txn);
`uvm_info("LOG", $sformatf(
"[%0t] APB %s addr=0x%0h data=0x%0h",
$time,
txn.write ? "WRITE" : "READ",
txn.addr, txn.data), UVM_LOW)
endfunction
endclass
 
// ── Env: three connect() calls — fan-out configuration ────────────────
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.ap.connect(scb.ap_imp);        // subscriber 1: scoreboard
mon.ap.connect(cov.analysis_export); // subscriber 2: coverage (uvm_subscriber)
mon.ap.connect(log_comp.ap_imp);    // subscriber 3: logger
// When mon.ap.write(txn) fires:
// 1. scb.write(txn)      ← scoreboard checks against expected
// 2. cov.write(txn)      ← coverage samples covergroup
// 3. log_comp.write(txn) ← logger prints to transcript
// All three are called synchronously, in connection order
endfunction

Example 3 — Verification: Dual-Monitor Request-Response Scoreboard

The most common complex analysis pattern: one scoreboard receiving from two monitors simultaneously, using the imp_decl suffix macro.

SystemVerilog — dual-monitor scoreboard with imp_decl macro
// ── OUTSIDE any class — generates two distinct imp classes ────────────
`uvm_analysis_imp_decl(`REQ)
`uvm_analysis_imp_decl(`RSP)
// Macro is at package scope — NOT inside class or module
 
class mem_scoreboard extends uvm_scoreboard;
`uvm_component_utils(mem_scoreboard)
 
// Two different imp classes, two different callback methods
uvm_analysis_imp_REQ#(mem_txn, mem_scoreboard) req_imp;
uvm_analysis_imp_RSP#(mem_txn, mem_scoreboard) rsp_imp;
 
mem_txn  req_q[$];    // pending requests waiting for response
int      match_count;
int      mismatch_count;
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
req_imp = new("req_imp", this);
rsp_imp = new("rsp_imp", this);
endfunction
 
// write_REQ() — called when req_monitor writes a transaction
function void write_REQ(mem_txn txn);
mem_txn stored;
$cast(stored, txn.clone());
req_q.push_back(stored);
`uvm_info("SCB", $sformatf("REQ: addr=0x%0h data=0x%0h",
stored.addr, stored.data), UVM_HIGH)
endfunction
 
// write_RSP() — called when rsp_monitor writes a response
function void write_RSP(mem_txn rsp);
mem_txn req;
if (req_q.size() == 0) begin
`uvm_error("SCB", "Response with no pending request")
return;
end
req = req_q.pop_front();
if (rsp.data !== req.data) begin
mismatch_count++;
`uvm_error("SCB", $sformatf(
"MISMATCH [%0d]: addr=0x%0h expected=0x%0h got=0x%0h",
mismatch_count, req.addr, req.data, rsp.data))
end else
match_count++;
endfunction
 
function void check_phase(uvm_phase phase);
`uvm_info("SCB", $sformatf(
"Summary: %0d matched, %0d mismatched, %0d pending",
match_count, mismatch_count, req_q.size()), UVM_NONE)
endfunction
endclass
 
// ── Env connection ─────────────────────────────────────────────────────
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
req_mon.ap.connect(scb.req_imp);   // req path → write_REQ()
rsp_mon.ap.connect(scb.rsp_imp);   // rsp path → write_RSP()
endfunction

Example 4 — Tricky: The Subscriber Order Matters for Shared Handle

The analysis port calls subscribers in connection order and passes the same object handle. If subscriber 1 modifies the transaction fields before subscriber 2's write() executes, subscriber 2 sees the modified values — not the originals.

SystemVerilog — handle aliasing across subscribers: the subtle corruption
// ❌ DANGEROUS — subscriber 1 modifies the shared transaction ─────────
class bad_scoreboard extends uvm_scoreboard;
uvm_analysis_imp#(apb_txn, bad_scoreboard) ap_imp;
 
function void write(apb_txn txn);
// Modifying the shared transaction object!
txn.data = txn.data ^ 32'hFFFF_FFFF;  // some "normalization"
expected_q.push_back(txn);              // stores modified value
endfunction
endclass
 
// Subscriber 2 (logger) connected AFTER bad_scoreboard:
// mon.ap.connect(bad_scb.ap_imp);   ← subscriber 1: modifies txn.data
// mon.ap.connect(log_comp.ap_imp);  ← subscriber 2: sees INVERTED data!
// The logger prints wrong data. No error thrown. Silent corruption.
 
// ✓ CORRECT — clone before any modification or storage ────────────────
class good_scoreboard extends uvm_scoreboard;
uvm_analysis_imp#(apb_txn, good_scoreboard) ap_imp;
 
function void write(apb_txn txn);
apb_txn my_copy;
$cast(my_copy, txn.clone());     // deep copy — your private object
my_copy.data ^= 32'hFFFF_FFFF;   // modify YOUR copy, not the shared one
expected_q.push_back(my_copy);
endfunction
endclass
 
// Now subscriber 2 (logger) sees the original, unmodified transaction.
// Golden rule: if you need to store or modify a transaction in write(),
// ALWAYS clone() it first. Every subscriber gets a private copy.

§10 — Bugs & Debugging

Bug 1 — Not Cloning the Transaction Before Storage

⚠️ Silent Corruption — Scoreboard Compares Against Wrong Data

The scoreboard stores the transaction handle directly without cloning. The monitor creates a single apb_txn object per transaction and reuses it (or the factory recycles internal state). On the next bus transaction, the monitor updates the object's fields. The scoreboard's queue entry — pointing to the same object — now shows the new transaction's data. The scoreboard compares transaction N against transaction N+1's data. Mismatches appear random. The root cause is one missing clone() call.

SystemVerilog — handle aliasing bug and the clone() fix
// ❌ BUG — storing the raw handle ─────────────────────────────────────
function void write(apb_txn txn);
expected_q.push_back(txn);  // ← WRONG: shares handle with monitor
endfunction
 
// Monitor reuses the same txn object:
// txn.addr = 0x1000; txn.data = 0xAA; ap.write(txn); → queue[0] = handle H
// txn.addr = 0x2000; txn.data = 0xBB; ap.write(txn); → queue[1] = handle H (SAME!)
// Now queue[0].data == 0xBB, queue[1].data == 0xBB — both wrong!
 
// ✓ FIX — clone before storing ────────────────────────────────────────
function void write(apb_txn txn);
apb_txn copy;
$cast(copy, txn.clone());   // deep copy: new object with same field values
expected_q.push_back(copy); // ← CORRECT: owns an independent copy
endfunction
 
// Now queue[0].data == 0xAA (immutable), queue[1].data == 0xBB (immutable)
// Each is an independent object — monitor's changes don't affect stored copies

Bug 2 — The `uvm_analysis_imp_decl Macro Inside a Class

⚠️ Compile Error — Macro Generates a Class Definition, Cannot Be Inside Another Class

The ``uvm_analysis_imp_decl(SUFFIX) macro expands into a full class definition. Class definitions cannot be nested inside other classes in SystemVerilog. Placing the macro inside the scoreboard class body causes a parser error. Move it to the top of the file or to the package header.

SystemVerilog — wrong vs correct placement of imp_decl macro
// ❌ WRONG — macro inside a class ─────────────────────────────────────
class my_scoreboard extends uvm_scoreboard;
`uvm_analysis_imp_decl(`REQ)   // ← COMPILE ERROR: nested class definition
uvm_analysis_imp_REQ#(apb_txn, my_scoreboard) req_imp;
function void write_REQ(apb_txn t); endfunction
endclass
 
// ✓ CORRECT — macro at package scope, before any class ────────────────
// File: my_scoreboard_pkg.sv
package my_scoreboard_pkg;
`include "uvm_macros.svh"
import uvm_pkg::*;
 
`uvm_analysis_imp_decl(`REQ)  // ← HERE: outside any class, inside package
`uvm_analysis_imp_decl(`RSP)
 
class my_scoreboard extends uvm_scoreboard;
uvm_analysis_imp_REQ#(apb_txn, my_scoreboard) req_imp;
uvm_analysis_imp_RSP#(apb_txn, my_scoreboard) rsp_imp;
function void write_REQ(apb_txn t); endfunction
function void write_RSP(apb_txn t); endfunction
endclass
 
endpackage

Bug 3 — Using a Task Instead of a Function for write()

SystemVerilog — task vs function in write(), and the mailbox pattern
// ❌ COMPILE ERROR — write() declared as task ─────────────────────────
task write(apb_txn txn);   // ← WRONG: must be a function
@(posedge clk);         // ← illegal in a function even if it was one
expected_q.push_back(txn);
endtask
// Compiler: "cannot call task 'write' from within a function context"
 
// ❌ COMPILE ERROR — time-consuming code inside function write() ───────
function void write(apb_txn txn);
#10;   // ← illegal: time delay in function
@(posedge vif.clk);  // ← illegal: event wait in function
endfunction
 
// ✓ CORRECT — write() as function, async processing via mailbox ────────
mailbox#(apb_txn) mbox;
 
function void write(apb_txn txn);
apb_txn copy;
$cast(copy, txn.clone());
void'(mbox.try_put(copy));  // try_put is a function — legal inside write()
// Fires-and-forgets. The heavy processing happens in run_phase task below.
endfunction
 
task run_phase(uvm_phase phase);
apb_txn txn;
forever begin
mbox.get(txn);               // blocks — legal in task
@(posedge vif.clk);          // legal in task
do_complex_check(txn);
end
endtask

§11 — Ready-to-Run Code

A complete self-contained analysis port demo. A producer fires five transactions through an analysis port. A scoreboard and a logger both receive each one. Run this to see the broadcast in action with real output.

analysis_port_demo.sv — compile and run
// analysis_port_demo.sv
// Compile: vlog -sv analysis_port_demo.sv
// Run:     vsim -c work.tb_ap_top +UVM_TESTNAME=ap_demo_test -do "run -all; quit"
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ── Transaction ────────────────────────────────────────────────────────
class demo_txn extends uvm_sequence_item;
`uvm_object_utils(demo_txn)
int id;
int data;
function new(string name="demo_txn"); super.new(name); endfunction
endclass
 
// ── Producer: fires transactions through analysis_port ─────────────────
class txn_producer extends uvm_component;
`uvm_component_utils(txn_producer)
uvm_analysis_port#(demo_txn) 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);
endfunction
 
task run_phase(uvm_phase phase);
demo_txn txn;
phase.raise_objection(this);
for (int i = 0; i < 5; i++) begin
txn      = demo_txn::type_id::create($sformatf("txn%0d",i));
txn.id   = i;
txn.data = (i + 1) * 100;
`uvm_info("PROD", $sformatf("Broadcasting txn[%0d] data=%0d",
i, txn.data), UVM_LOW)
ap.write(txn);   // calls scb.write(txn) then log.write(txn)
end
phase.drop_objection(this);
endtask
endclass
 
// ── Scoreboard subscriber ─────────────────────────────────────────────
class demo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(demo_scoreboard)
uvm_analysis_imp#(demo_txn, demo_scoreboard) ap_imp;
int received = 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_imp = new("ap_imp", this);
endfunction
 
function void write(demo_txn txn);
received++;
`uvm_info("SCB", $sformatf("[SCB] txn[%0d] data=%0d ← scoreboard check",
txn.id, txn.data), UVM_LOW)
endfunction
 
function void check_phase(uvm_phase phase);
`uvm_info("SCB", $sformatf("Scoreboard received %0d transactions",
received), UVM_NONE)
endfunction
endclass
 
// ── Logger subscriber ─────────────────────────────────────────────────
class demo_logger extends uvm_component;
`uvm_component_utils(demo_logger)
uvm_analysis_imp#(demo_txn, demo_logger) ap_imp;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap_imp = new("ap_imp", this);
endfunction
 
function void write(demo_txn txn);
`uvm_info("LOG", $sformatf("[LOG] txn[%0d] data=%0d ← logger record",
txn.id, txn.data), UVM_LOW)
endfunction
endclass
 
// ── Test: builds and connects ──────────────────────────────────────────
class ap_demo_test extends uvm_test;
`uvm_component_utils(ap_demo_test)
txn_producer    prod;
demo_scoreboard scb;
demo_logger     log_comp;
 
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
 
function void build_phase(uvm_phase phase);
prod     = txn_producer::type_id::create("prod",     this);
scb      = demo_scoreboard::type_id::create("scb",      this);
log_comp = demo_logger::type_id::create("log_comp", this);
endfunction
 
function void connect_phase(uvm_phase phase);
prod.ap.connect(scb.ap_imp);        // subscriber 1
prod.ap.connect(log_comp.ap_imp);   // subscriber 2
endfunction
endclass
 
module tb_ap_top;
initial run_test();
endmodule
 
// Expected output (per transaction, order: SCB first, LOG second):
// UVM_INFO PROD: Broadcasting txn[0] data=100
// UVM_INFO SCB:  [SCB] txn[0] data=100 ← scoreboard check
// UVM_INFO LOG:  [LOG] txn[0] data=100 ← logger record
// UVM_INFO PROD: Broadcasting txn[1] data=200
// UVM_INFO SCB:  [SCB] txn[1] data=200 ← scoreboard check
// UVM_INFO LOG:  [LOG] txn[1] data=200 ← logger record
// ... (5 transactions total)
// UVM_INFO SCB:  Scoreboard received 5 transactions
// Key: PROD fires write() once. SCB and LOG both receive it. No monitor code changed.

§12 — Interview Questions

Beginner Level

Intermediate Level

Senior / Architect Level

§13 — Best Practices

RulePracticeWhy It Matters
BP-1Always name the analysis port ap in monitors; name imps ap_imp or ap_imp_SUFFIXConsistent naming across a VIP library means engineers don't need to read header files to find the port name
BP-2Always clone() before storing a transaction in write()Prevents silent handle aliasing — all subscribers share the same object; modifications by one are visible to subsequent subscribers
BP-3write() must be a function with no time-consuming codeCompilation enforces this, but understanding why prevents the pattern of pushing time-consuming work incorrectly into write()
BP-4Place ``uvm_analysis_imp_decl` at package scope, never inside a classThe macro generates a class definition — nested class definitions are illegal in SystemVerilog
BP-5Use uvm_subscriber as the base class for subscriber-only componentsuvm_subscriber auto-declares analysis_export — one less boilerplate; the write() hook is the primary extension point
BP-6Expose the monitor's analysis port at the agent boundaryAllows env-level connections without the env needing to know the monitor's path — clean hierarchy boundary, VIP stays self-contained
BP-7Use the mailbox-in-write() + run_phase-task pattern for async-heavy processingKeeps write() fast (non-blocking function), hands off time-consuming logic to a task thread without violating function rules
BP-8In check_phase, verify the scoreboard's expected queue is empty at end of testAn unmatched entry in the expected queue means a transaction was predicted but never observed — a silent miss that only shows up if you check

§14 — Summary

AspectAnalysis PortTLM 1.0 Blocking Port
Fan-outOne-to-many — unlimited subscribersPoint-to-point — exactly one target
Method typefunction void write(T txn)task put(T txn) / task get(output T txn)
BlockingNon-blocking — fire-and-forgetBlocking — caller waits for response
Zero connectionsValid — write() does nothing silentlyFatal — unconnected port causes uvm_fatal
ResponseOne-way — no response back to publisherBidirectional possible (get/peek)
Canonical useMonitor → scoreboard, coverage, loggerDriver ↔ sequencer, producer ↔ consumer
Multiple impsNeeds ``uvm_analysis_imp_decl` macroNeeds ``uvm_blocking_put_imp_decl` macro