UVM-17: TLM Analysis Ports — VLSI Trainers
VLSI Trainers UVM Series · UVM-17
UVM Series · UVM-17

TLM Analysis Ports

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 Observer Pattern

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:

Analysis Framework — Observer Pattern in UVM Monitor (broadcaster) ap.write(txn) → analysis_port one call broadcasts to all connected exports Scoreboard analysis_imp → write() Cov Collector analysis_imp → write() Metric Analyzer analysis_imp → write() Connect in env.connect_phase: m_agent.ap.connect(m_sb.analysis_export) m_agent.ap.connect(m_cov.analysis_export) m_agent.ap.connect(m_met.analysis_export) Zero subscribers: ap.write() is silent no-op No maximum subscriber limit vlsitrainers.com
Figure 1 — Observer pattern in the UVM analysis framework. The monitor’s analysis_port broadcasts to all connected analysis_imp exports. The env’s connect_phase wires the connections. Subscribers implement write() to receive and process each transaction. Zero, one, or many subscribers are all valid.

📋 Three Analysis Objects

ObjectRoleWhere declaredKey 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 impParent components that forward to a child’s impConnected via ap.connect(export) and then export.connect(imp)
uvm_analysis_imp #(T, C)Implementation — actually calls the component’s write() functionAny component that implements write()write() function on the component class C
tlm_analysis_fifo #(T)Buffering subscriber — receives via write(), provides blocking get() for consumersAny component that needs to decouple receive from processanalysis_export for input; get_port or try_get for output

📋 analysis_port — The Broadcaster

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
analysis_port uses new() not type_id::create(). TLM ports and exports are infrastructure objects, not UVM factory objects. They are always constructed directly with new("name", this). Using type_id::create() on a port will fail.

📋 analysis_export and analysis_imp

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
analysis_imp vs analysis_export — When to Use Each analysis_imp — direct (most common) analysis_port Scoreboard analysis_imp → write() analysis_export — hierarchical (forwarding) analysis_port Env border analysis_export SB vlsitrainers.com
Figure 2 — analysis_imp (left) vs analysis_export (right). Use analysis_imp when the component directly implements write(). Use analysis_export as a hierarchical pass-through when the write() implementation is in a child component — for example, a sub-env exposes an analysis_export that forwards to a child scoreboard’s analysis_imp.

📋 Connecting Port to Export

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
Never connect export to port — always port.connect(export). The connection is directional: the port calls connect() and passes the export as argument. Reversing this will compile in some tools but silently produce no connection, causing the scoreboard to never receive any transactions.

📋 analysis_fifo — Decoupled Subscriber

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
MethodBlocks?Use case
fifo.analysis_exportConnect analysis_port to this in connect_phase
fifo.get(txn)Yes — waits if emptyScoreboard run_phase — process one at a time
fifo.try_get(txn)No — returns 0 if emptyNon-blocking check
fifo.peek(txn)Yes — waits if empty, does not removeLook ahead without consuming
fifo.size()Nocheck_phase — verify all transactions were consumed
fifo.flush()NoClear FIFO on reset or between test phases

📋 uvm_subscriber Shortcut

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

📋 Hierarchical Exports

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
Port forwarding hides internal structure from the environment. The env connects to 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.

📋 Multiple write() — imp Macros

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

📋 Quick Reference

ItemKey fact
analysis_port purposeOne-to-many broadcast. ap.write(txn) delivers to all connected exports.
Zero subscribersSilent no-op — no error. Any number of subscribers is valid.
analysis_port constructionap = new("ap", this) in build_phase — NOT type_id::create()
Connection directionport.connect(export) — always port → export, never reversed
Connection whereAlways in connect_phase — never in build_phase
analysis_impDirect implementation — component’s write() called when port broadcasts
analysis_imp templateuvm_analysis_imp #(T, ComponentClass)
analysis_exportHierarchical pass-through — forwards write() to a child component’s imp
tlm_analysis_fifoBuffers transactions from write() — consumer retrieves via blocking get()
FIFO check_phase ruleCheck fifo.size() == 0 — leftover entries indicate unprocessed transactions
uvm_subscriberBuilt-in analysis_export + abstract write() — simplest subscriber base class
Port forwardingm_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() constraintMust be a void function — cannot block, cannot use @(event) or #delay
Scroll to Top