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.
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
endtaskQuick 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
⚠️ 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.
// ❌ 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 copiesBug 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.
// ❌ 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
Beginner Level
Intermediate Level
Senior / Architect Level
§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 |
§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 |