The complete UVM analysis communication framework — analysis_port, analysis_export, analysis_imp, analysis_fifo, uvm_subscriber, hierarchical exports, and how to handle multiple write() implementations in a single component using the imp macros.
The UVM analysis framework implements the observer pattern: one broadcaster (the monitor’s analysis port) can deliver the same transaction to any number of observers (scoreboards, coverage collectors, metric analyzers) with a single ap.write(txn) call. Each observer registers itself with the broadcaster by calling ap.connect() in the env’s connect_phase.
Three design requirements drove the analysis framework’s design:
| Object | Role | Where declared | Key method |
|---|---|---|---|
uvm_analysis_port #(T) | Broadcaster — source of transactions. One-to-many. Called by the monitor. | Monitor (or any source component) | ap.write(txn) — calls write() on all connected exports |
uvm_analysis_export #(T) | Hierarchical forwarder — passes write() calls down to a child’s imp | Parent components that forward to a child’s imp | Connected via ap.connect(export) and then export.connect(imp) |
uvm_analysis_imp #(T, C) | Implementation — actually calls the component’s write() function | Any component that implements write() | write() function on the component class C |
tlm_analysis_fifo #(T) | Buffering subscriber — receives via write(), provides blocking get() for consumers | Any component that needs to decouple receive from process | analysis_export for input; get_port or try_get for output |
The uvm_analysis_port is declared in the broadcaster component (typically the monitor or agent). It maintains an internal list of connected exports and calls write() on each when broadcast is called.
// ── Declaration and construction ───────────────────────── class my_monitor extends uvm_monitor; `uvm_component_utils(my_monitor) // Parameterised on the transaction type uvm_analysis_port #(my_seq_item) ap; function void build_phase(uvm_phase phase); super.build_phase(phase); // Constructed with new() — NOT type_id::create() ap = new("ap", this); endfunction task run_phase(uvm_phase phase); my_seq_item txn; forever begin // ... detect and assemble transaction ... txn = my_seq_item::type_id::create("txn"); // ... populate txn fields ... ap.write(txn); // broadcasts to ALL connected exports end endtask endclass
new("name", this). Using type_id::create() on a port will fail.A subscriber implements write() to receive transactions. The mechanism depends on whether the subscriber directly implements write() (imp) or forwards to a child component (export).
// ── analysis_imp: direct write() implementation ─────────── // Template: uvm_analysis_imp #(T, ComponentClass) class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) // The imp declares where write() lives (this component) uvm_analysis_imp #(my_seq_item, my_scoreboard) analysis_export; function void build_phase(uvm_phase phase); super.build_phase(phase); analysis_export = new("analysis_export", this); endfunction // write() is called by the analysis_port when ap.write(txn) function void write(my_seq_item txn); // Process transaction: compare, check, log `uvm_info("SB", txn.convert2string(), UVM_MEDIUM) endfunction endclass // ── In env connect_phase: port → imp ───────────────────── function void connect_phase(uvm_phase phase); m_monitor.ap.connect(m_sb.analysis_export); endfunction
All analysis connections are made in connect_phase. The connection direction is always port to export — you call connect() on the port and pass the export as the argument.
// ── All connections in env connect_phase ───────────────── function void connect_phase(uvm_phase phase); // Monitor ap → scoreboard's analysis_export m_agent.ap.connect(m_sb.analysis_export); // Monitor ap → coverage collector's analysis_export m_agent.ap.connect(m_cov.analysis_export); // Monitor ap → FIFO (for decoupled processing) m_agent.ap.connect(m_fifo.analysis_export); // Sub-env's analysis_export → parent's scoreboard // (hierarchical: export on sub-env boundary) m_sub_env.ap.connect(m_parent_sb.analysis_export); endfunction
A tlm_analysis_fifo is a pre-built component that bridges the gap between the non-blocking analysis write() world and the blocking task-based processing world. The monitor calls write() (non-blocking, fast), and a scoreboard or other consumer calls blocking get() to retrieve transactions one at a time as it processes them.
This decoupling is essential when the scoreboard needs time to process each transaction and cannot keep up with the monitor’s broadcast rate.
// ── Scoreboard using analysis_fifo for decoupling ──────── class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) // FIFO holds incoming transactions until SB is ready tlm_analysis_fifo #(my_seq_item) m_fifo; function void build_phase(uvm_phase phase); super.build_phase(phase); m_fifo = new("m_fifo", this); endfunction function void connect_phase(uvm_phase phase); // Agent's analysis port → FIFO's write() input // Done from env: m_agent.ap.connect(m_sb.m_fifo.analysis_export) endfunction task run_phase(uvm_phase phase); my_seq_item txn; forever begin // Blocking get — waits until FIFO has a transaction m_fifo.get(txn); check_transaction(txn); // can take as long as needed end endtask task check_transaction(my_seq_item txn); // ... comparison logic ... endtask function void check_phase(uvm_phase phase); // Warn if FIFO still has unprocessed transactions at end if (m_fifo.size() > 0) `uvm_error("SB", $sformatf( "%0d unprocessed transactions in FIFO!", m_fifo.size())) endfunction endclass
| Method | Blocks? | Use case |
|---|---|---|
fifo.analysis_export | — | Connect analysis_port to this in connect_phase |
fifo.get(txn) | Yes — waits if empty | Scoreboard run_phase — process one at a time |
fifo.try_get(txn) | No — returns 0 if empty | Non-blocking check |
fifo.peek(txn) | Yes — waits if empty, does not remove | Look ahead without consuming |
fifo.size() | No | check_phase — verify all transactions were consumed |
fifo.flush() | No | Clear FIFO on reset or between test phases |
uvm_subscriber #(T) combines a uvm_component, a built-in analysis_imp named analysis_export, and an abstract write() function. Extending it is the simplest way to build a subscriber — no manual port declaration or construction needed.
// ── Simplest subscriber: extend uvm_subscriber ─────────── class apb_cov_collector extends uvm_subscriber #(apb_seq_item); `uvm_component_utils(apb_cov_collector) apb_seq_item cov_txn; covergroup apb_cg; cp_addr: coverpoint cov_txn.addr; cp_rw: coverpoint cov_txn.read_not_write; cx: cross cp_addr, cp_rw; endgroup function new(string name, uvm_component parent); super.new(name, parent); apb_cg = new(); endfunction // write() is abstract in uvm_subscriber — must be implemented function void write(apb_seq_item t); cov_txn = t; apb_cg.sample(); endfunction endclass // ── Connect: analysis_export is built-in ───────────────── m_agent.ap.connect(m_cov.analysis_export); // analysis_export is automatic
When an analysis_port inside a sub-component needs to be exposed at a higher level in the hierarchy — for example, a monitor’s ap forwarded through the agent’s boundary — use a uvm_analysis_export as a pass-through. The export is connected inward (child’s port → parent’s export) in connect_phase.
// ── Agent exposes its monitor's port at agent boundary ─── class my_agent extends uvm_agent; `uvm_component_utils(my_agent) my_monitor m_mon; my_driver m_drv; my_sequencer m_seqr; // Public analysis port — env connects subscribers to this uvm_analysis_port #(my_seq_item) ap; function void build_phase(uvm_phase phase); super.build_phase(phase); ap = new("ap", this); m_mon = my_monitor::type_id::create("m_mon", this); // ... endfunction function void connect_phase(uvm_phase phase); // Forward monitor's port through agent's port boundary // Analysis port forwarding: monitor.ap → agent.ap m_mon.ap.connect(this.ap); // Now env connects: agent.ap.connect(sb.analysis_export) // Env does NOT reach inside to agent.m_mon.ap endfunction endclass
agent.ap — not to agent.m_mon.ap. If the agent is refactored internally (monitor renamed, additional monitors added), the env connection does not change. This is the same encapsulation principle that makes the agent a self-contained VIP unit.A component with a single uvm_analysis_imp can only implement one write() function. When a component needs to receive transactions from multiple analysis ports of the same type, use the `uvm_analysis_imp_decl macro to create uniquely-named imp classes, each calling a differently-named write function.
// ── Declare custom imp types (in package or header) ────── // Each generates a class with a unique write() call `uvm_analysis_imp_decl(_expected) // creates uvm_analysis_imp_expected `uvm_analysis_imp_decl(_actual) // creates uvm_analysis_imp_actual // ── Scoreboard with two inputs ──────────────────────────── class my_scoreboard extends uvm_scoreboard; `uvm_component_utils(my_scoreboard) // Two distinct analysis inputs uvm_analysis_imp_expected #(my_seq_item, my_scoreboard) expected_export; uvm_analysis_imp_actual #(my_seq_item, my_scoreboard) actual_export; function void build_phase(uvm_phase phase); super.build_phase(phase); expected_export = new("expected_export", this); actual_export = new("actual_export", this); endfunction // write_expected() — called when predictor writes to expected_export function void write_expected(my_seq_item txn); expected_q.push_back(txn); endfunction // write_actual() — called when monitor writes to actual_export function void write_actual(my_seq_item txn); compare_with_expected(txn); endfunction my_seq_item expected_q[$]; // queue of predicted transactions function void compare_with_expected(my_seq_item actual); my_seq_item expected; if (expected_q.size() == 0) begin `uvm_error("SB", "Actual arrived but no expected queued!") return; end expected = expected_q.pop_front(); if (!actual.compare(expected)) `uvm_error("SB", $sformatf( "MISMATCH\n expected: %s\n actual: %s", expected.convert2string(), actual.convert2string())) else `uvm_info("SB", "MATCH ✓", UVM_HIGH) endfunction endclass // ── In env connect_phase ────────────────────────────────── m_predictor.ap.connect(m_sb.expected_export); // predicted stream m_monitor.ap.connect(m_sb.actual_export); // actual stream
| Item | Key fact |
|---|---|
| analysis_port purpose | One-to-many broadcast. ap.write(txn) delivers to all connected exports. |
| Zero subscribers | Silent no-op — no error. Any number of subscribers is valid. |
| analysis_port construction | ap = new("ap", this) in build_phase — NOT type_id::create() |
| Connection direction | port.connect(export) — always port → export, never reversed |
| Connection where | Always in connect_phase — never in build_phase |
| analysis_imp | Direct implementation — component’s write() called when port broadcasts |
| analysis_imp template | uvm_analysis_imp #(T, ComponentClass) |
| analysis_export | Hierarchical pass-through — forwards write() to a child component’s imp |
| tlm_analysis_fifo | Buffers transactions from write() — consumer retrieves via blocking get() |
| FIFO check_phase rule | Check fifo.size() == 0 — leftover entries indicate unprocessed transactions |
| uvm_subscriber | Built-in analysis_export + abstract write() — simplest subscriber base class |
| Port forwarding | m_mon.ap.connect(this.ap) in agent — exposes monitor port at agent boundary |
| Multiple write() methods | `uvm_analysis_imp_decl(_suffix) — creates imp class calling write_suffix() |
| write() constraint | Must be a void function — cannot block, cannot use @(event) or #delay |