UVM
Analysis Ports
Broadcast model, write() protocol, one-to-many fan-out, uvm_analysis_imp_decl.
UVM Fundamentals · Module 13
The analysis port is how a monitor publishes what it observed — the one-to-many, non-blocking channel that feeds scoreboards, coverage, and loggers without any of them knowing about each other. This page is the mechanism; the transactions flowing through it are built in transaction reconstruction.
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.
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.
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;
endclassThe 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.
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.
// ── 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 subscribersMultiple 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.
// ── 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 pathAnalysis 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.
// ── 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
endclassThe 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.
| Rule | Reason | Consequence of Violation |
|---|---|---|
write() must be a function, not a task | Analysis ports call write() inside a function call chain — tasks cannot be called from functions in SystemVerilog | Compilation error: "cannot call a task from within a function context" |
write() cannot consume simulation time | It is a function — @(posedge clk), #delay, wait() are illegal inside functions | Compilation error — time-consuming statements in functions are disallowed |
| Store a clone of the transaction, not the handle | The analysis port passes the same object reference to all subscribers. If a subscriber modifies it, the modification is visible to subsequent subscribers | Silent 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 subscribers | Simulation deadlock if any blocking construct is used |
// ── 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
endtaskThe Broadcast, Drawn
Fan-out is the defining property of an analysis port, and it is the one thing the code above cannot show at a glance: write() is a single call in the monitor and N calls at run time, in connection order, on the same object handle.
Two properties of that picture drive most analysis-port bugs. The fan-out is synchronous — the monitor's write() does not return until every subscriber's write() has run, so a slow subscriber slows the monitor. And every arrow carries the same handle, not a copy, which is the entire content of the handle-aliasing bug below.
When a Subscriber Must Consume Time — uvm_tlm_analysis_fifo
The write()-must-be-a-function rule creates a real problem: some subscribers genuinely need to consume time. A scoreboard that must wait for a reference model, drive a response, or arbitrate against another stream cannot do that work inside a function.
The mailbox pattern shown in Bug 3 solves this, and it is worth knowing because it explains the mechanism. But UVM ships the component built for exactly this, and it is what production code uses:
class slow_scoreboard extends uvm_component;
`uvm_component_utils(slow_scoreboard)
// An analysis FIFO IS an analysis subscriber: it exposes `analysis_export`
// and implements write() internally as a non-blocking put into an unbounded
// FIFO. The monitor's write() therefore always returns immediately, however
// slow this component is.
uvm_tlm_analysis_fifo #(apb_txn) fifo;
function new(string name, uvm_component parent);
super.new(name, parent);
fifo = new("fifo", this);
endfunction
// Time-consuming work lives in run_phase, where it is legal.
task run_phase(uvm_phase phase);
apb_txn txn;
forever begin
fifo.get(txn); // blocking - waits until a transaction arrives
@(posedge vif.clk); // legal here; would be illegal inside write()
check_against_reference(txn);
end
endtask
endclass
// Connection: the FIFO's analysis_export is the subscriber endpoint.
function void connect_phase(uvm_phase phase);
mon.ap.connect(sb.fifo.analysis_export);
endfunctionWhy this is the idiomatic answer rather than a hand-rolled mailbox. The FIFO is a uvm_component, so it appears in the hierarchy and in uvm_top.print_topology() — a mailbox is invisible. It is unbounded by default, so write() can never block or drop, which is the property the analysis contract requires. And it comes with try_get, used(), and is_empty(), so a drain check at end of test is one line: a non-empty analysis FIFO means the subscriber never finished processing what the monitor observed.
Quick Reference
| Task | Declaration / 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 transaction | ap.write(txn); — function, non-blocking |
| Connect subscriber | mon.ap.connect(scb.ap_imp) — in connect_phase |
| Connect multiple subscribers | Call .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 callback | function void write(T txn); — must be function, no time-consuming code |
| Safe transaction storage | $cast(copy, txn.clone()) before pushing to queue |
| Check subscriber count | ap.size() — returns number of connected subscribers |
// ── 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.
// ── 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
endclassExample 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.
// ── 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
endfunctionExample 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.
// ── 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()
endfunctionExample 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.
// ❌ 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
Two subscribers, one handle — the scoreboard's stored transactions mutate after it stored them
ANALYSIS-HANDLE-ALIASINGA scoreboard stores expected transactions from the monitor and compares them later. Comparisons fail with fields that were correct when they arrived. Removing the coverage subscriber — a component that does no checking at all — makes the failures disappear. Reordering the two connect() calls changes which transactions are wrong.
Expected. Each subscriber receives the transaction the monitor observed.
Actual. Every subscriber receives the same object handle, in connection order, synchronously. Whatever the first subscriber does to that object, the second one sees.
// monitor
ap.write(txn); // ONE handle, broadcast to N subscribers
// subscriber A (connected first) - stores the handle, not a copy
function void write(apb_txn t);
expected_q.push_back(t); // ← stores a reference to the live object
endfunction
// subscriber B (connected second) - normalises for coverage
function void write(apb_txn t);
t.addr = t.addr & 32'hFFFF_FFFC; // ← mutates the object A just stored
cg.sample();
endfunctionThe dependence on unrelated components is what identifies this, and it is worth learning as a signature because no data-path theory explains it:
- Does removing a non-checking subscriber change the result? A coverage collector cannot affect correctness — unless it is mutating a shared object. If it can, the object is shared.
- Does connection order matter? The port iterates subscribers in connect order, so a mutating subscriber only corrupts those connected before it. Swapping the calls moves the damage.
- Are the stored values wrong, or did they become wrong? Print the field inside
write()and again at comparison time. Same handle, two different values, no intervening write from your own code — that is aliasing, conclusively.
uvm_analysis_port::write() broadcasts the handle it was given. Nothing in UVM copies the transaction, and the fan-out is a synchronous function-call chain, so all subscribers operate on one object within a single call. A subscriber that stores that handle has stored a pointer into shared, still-mutable state.
Note which component is at fault. Subscriber B is doing something entirely reasonable for a coverage collector. It is subscriber A — the one that stored a handle it does not own — that broke the contract.
Any subscriber that stores or mutates takes a private copy first:
function void write(apb_txn t);
apb_txn c;
if (!$cast(c, t.clone())) // clone() returns uvm_object; cast to the type
`uvm_fatal("SB", "clone/cast failed")
expected_q.push_back(c); // now private - nobody else can reach it
endfunctionclone() depends on do_copy (or field macros) being correct — a clone() that silently skips a field produces the same symptom with a different cause, so see do_copy and do_compare. A subscriber that only reads — a logger printing one line — needs no clone, though cloning is cheap insurance.
The rule is about the verb, not the component: store or mutate ⇒ clone. Read and discard ⇒ no clone needed.
The alternative — having the monitor clone before each write() — guarantees independence but allocates one object per subscriber per transaction, which is real cost in a long run. Most environments push the obligation to the subscriber instead, so make it a review item: any push_back, any assignment to a class member, any field mutation inside write() needs a clone above it. And treat "the bug moves when I reorder connect() calls" as diagnostic of this and nothing else.
Bug 2 — The `uvm_analysis_imp_decl Macro Inside a Class
// ❌ 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
endpackageBug 3 — Using a Task Instead of a Function for write()
// ❌ 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: 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
Three key differences. (1) Fan-out: analysis port supports one-to-many — any number of subscribers via multiple connect() calls; blocking put_port is point-to-point — exactly one target. (2) Blocking: analysis port's write() is a function — non-blocking, returns immediately; blocking put_port's put() is a task — the caller waits until the target's put() task returns. (3) Zero connections: an unconnected analysis port is valid — write() does nothing; an unconnected blocking_put_port causes a uvm_fatal at start_of_simulation_phase. Analysis ports are for broadcast observation; TLM 1.0 ports are for request-response communication.
§13 — Best Practices
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Always name the analysis port ap in monitors; name imps ap_imp or ap_imp_SUFFIX | Consistent naming across a VIP library means engineers don't need to read header files to find the port name |
| BP-2 | Always 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-3 | write() must be a function with no time-consuming code | Compilation enforces this, but understanding why prevents the pattern of pushing time-consuming work incorrectly into write() |
| BP-4 | Place ``uvm_analysis_imp_decl` at package scope, never inside a class | The macro generates a class definition — nested class definitions are illegal in SystemVerilog |
| BP-5 | Use uvm_subscriber as the base class for subscriber-only components | uvm_subscriber auto-declares analysis_export — one less boilerplate; the write() hook is the primary extension point |
| BP-6 | Expose the monitor's analysis port at the agent boundary | Allows env-level connections without the env needing to know the monitor's path — clean hierarchy boundary, VIP stays self-contained |
| BP-7 | Use the mailbox-in-write() + run_phase-task pattern for async-heavy processing | Keeps write() fast (non-blocking function), hands off time-consuming logic to a task thread without violating function rules |
| BP-8 | In check_phase, verify the scoreboard's expected queue is empty at end of test | An unmatched entry in the expected queue means a transaction was predicted but never observed — a silent miss that only shows up if you check |
Where This Is Specified
uvm_analysis_port, uvm_analysis_imp, uvm_analysis_export, and uvm_tlm_analysis_fifo are defined in the UVM standard, IEEE Std 1800.2, with the class reference published by Accellera. The TLM-1 analysis interface is where the two properties this page turns on are normative: write() is declared as a function (hence cannot consume time), and an analysis port permits zero or more subscribers (hence an unconnected port is legal and silent).
The detail worth reading in the class reference rather than assuming: the port broadcasts the handle it was given. Nothing in UVM copies the transaction on the way out, so every subscriber shares one object, and the ordering of the fan-out is connection order. Both facts are why clone() is a discipline rather than a suggestion — see do_copy and do_compare for the copy machinery that makes cloning correct.
The base language rule behind write() being a function — that a function may not call a task or consume time — is IEEE Std 1800 clause 13.
Related lessons. The publisher side is monitor analysis ports; the hierarchical plumbing is analysis exports; the wider TLM family is TLM ports and exports and TLM FIFOs. For what travels through the port see transaction reconstruction and sequence items; for the subscribers, coverage-driven verification. When a connection silently delivers nothing, monitor debugging and scoreboard debugging are the triage pages.
§14 — Summary
| Aspect | Analysis Port | TLM 1.0 Blocking Port |
|---|---|---|
| Fan-out | One-to-many — unlimited subscribers | Point-to-point — exactly one target |
| Method type | function void write(T txn) | task put(T txn) / task get(output T txn) |
| Blocking | Non-blocking — fire-and-forget | Blocking — caller waits for response |
| Zero connections | Valid — write() does nothing silently | Fatal — unconnected port causes uvm_fatal |
| Response | One-way — no response back to publisher | Bidirectional possible (get/peek) |
| Canonical use | Monitor → scoreboard, coverage, logger | Driver ↔ sequencer, producer ↔ consumer |
| Multiple imps | Needs ``uvm_analysis_imp_decl` macro | Needs ``uvm_blocking_put_imp_decl` macro |
Continue learning
Related tutorials
- Recommended next
UVM Callbacks
Three-actor model, callback classes, pool registration, uvm_do_callbacks, ordering, error injection.
- Recommended next
Report Server
The central engine that processes every report — the uvm_report_server that receives reports after the catchers, applies actions per severity (display, log, count, exit), tallies the error and fatal counts that decide pass/fail, bounds a flood with max_quit_count, formats and routes messages, and is customizable to tailor the whole testbench's reporting — and why the count, not the log, is the verdict.