Skip to content

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.

Analysis port broadcast architecture
Analysis port broadcast architecture

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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

The 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.

A monitor's analysis port broadcasts one transaction handle synchronously to a scoreboard, a coverage collector, and a logger, in connection order.Monitorap.write(txn) - one calluvm_analysis_portiterates subscriber listScoreboard impclones - storesCoverage impsamples - no storeLogger impreads only12
Figure — one write(), N subscribers. The monitor calls ap.write(txn) once; the analysis port iterates its subscriber list in connection order and invokes each subscriber's write() as a plain function call, synchronously, before write() returns to the monitor. Every subscriber receives the SAME handle, which is why a subscriber that stores or mutates the transaction must clone it first. Zero subscribers is legal and does nothing. Because the whole fan-out is a function-call chain, no subscriber may consume time.

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:

uvm_tlm_analysis_fifo — the standard non-blocking-to-blocking bridge
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
endfunction

Why 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

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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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

1

Two subscribers, one handle — the scoreboard's stored transactions mutate after it stored them

ANALYSIS-HANDLE-ALIASING
Observed Symptom

A 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 vs Actual

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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();
endfunction
Diagnostic

The dependence on unrelated components is what identifies this, and it is worth learning as a signature because no data-path theory explains it:

  1. 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.
  2. 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.
  3. 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.
Root Cause

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.

Fix

Any subscriber that stores or mutates takes a private copy first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endfunction

clone() 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.

Prevention

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

SystemVerilog — wrong vs correct placement of imp_decl macro
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

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

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

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

Continue learning