A UVM TLM analysis port is a broadcast connection. A component calls write() on its uvm_analysis_port, and the port calls the write() function of every component connected to it, whether that is none, one or many. It is how monitors share what they observe with scoreboards, coverage collectors and any other component that wants a copy, without knowing who those components are.

Analysis ports are the most widely used TLM connection in real testbenches. Almost every agent has one on its monitor, and almost every scoreboard and coverage collector is fed by one.

7 min read | Intermediate Level

What You'll Learn

  • Explain how an analysis port differs from put and get connections
  • Declare an analysis port and broadcast transactions with write()
  • Receive transactions in a subscriber through analysis_export
  • Connect one analysis port to several subscribers

How an Analysis Port Differs

Put and get ports expect exactly one connection, and the call can block. An analysis port is different in two ways:

  • Any number of connections. The port keeps a list of everything connected to it, and write() calls each one in turn. With nothing connected, write() simply does nothing, so a monitor works the same whether or not anyone is listening.
  • Never blocks. write() is a void function, so it completes in zero simulation time. A monitor can broadcast and immediately go back to sampling its interface.

Syntax


uvm_analysis_port #(T)          ap;        // in the broadcaster
uvm_analysis_imp  #(T, IMP)     imp;       // in a receiver; IMP implements write()

ap.write(t);                               // broadcast t to all connected receivers
virtual function void write(T t);          // what each receiver implements

Receivers derived from uvm_subscriber #(T) already have a uvm_analysis_imp called analysis_export, so they only need to implement write().

Example: One Broadcaster, Three Subscribers

This example extends the blocking put example. compA sends five packets to compB through a put port. Each time compB receives a packet in its put() task, it prints it and broadcasts it through an analysis port to three subscribers.

The Broadcaster


class componentB extends uvm_component;
  `uvm_component_utils(componentB)

  uvm_blocking_put_imp #(simple_packet, componentB) put_export;   // receives from compA
  uvm_analysis_port    #(simple_packet)             ap;           // broadcasts to subscribers

  function new(string name = "componentB", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    put_export = new("put_export", this);
    ap         = new("analysis_port", this);    // instance name used in the topology
  endfunction

  // Called for every packet compA puts
  virtual task put(simple_packet pkt);
    `uvm_info("COMPB", "Packet received from CompA", UVM_LOW)
    pkt.print();
    ap.write(pkt);                               // broadcast; returns in zero time
  endtask
endclass

compA is the sender from UVM TLM Blocking Put Port, with a uvm_blocking_put_port named put_port and a loop that sends five randomized packets.

The Subscriber

Each subscriber extends uvm_subscriber for the packet type and implements write(). The base class already declares the analysis imp:


// How uvm_subscriber is declared in the library (simplified)
virtual class uvm_subscriber #(type T = int) extends uvm_component;
  typedef uvm_subscriber #(T) this_type;
  uvm_analysis_imp #(T, this_type) analysis_export;
  pure virtual function void write(T t);
endclass

class sub extends uvm_subscriber #(simple_packet);
  `uvm_component_utils(sub)

  function new(string name = "sub", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  // Called once for every ap.write() in componentB
  virtual function void write(simple_packet t);
    `uvm_info(get_full_name(), "Sub got transaction", UVM_MEDIUM)
  endfunction
endclass

The argument is named t on purpose. write() overrides a virtual method declared in uvm_subscriber, and SystemVerilog requires an override to use identical argument names. Some simulators only warn about a different name, but others reject it.

Connecting Everything

All five components live in the environment, so it makes every connection. The same analysis port is connected three times, once to each subscriber.


class my_env extends uvm_env;
  `uvm_component_utils(my_env)

  componentA compA;
  componentB compB;
  sub        sub1, sub2, sub3;

  function new(string name = "my_env", uvm_component parent = null);
    super.new(name, parent);
  endfunction

  virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    compA = componentA::type_id::create("compA", this);
    compB = componentB::type_id::create("compB", this);
    sub1  = sub::type_id::create("sub1", this);
    sub2  = sub::type_id::create("sub2", this);
    sub3  = sub::type_id::create("sub3", this);
  endfunction

  virtual function void connect_phase(uvm_phase phase);
    compA.put_port.connect(compB.put_export);

    // One analysis port, three subscribers
    compB.ap.connect(sub1.analysis_export);
    compB.ap.connect(sub2.analysis_export);
    compB.ap.connect(sub3.analysis_export);
  endfunction
endclass
Output
----------------------------------------------------------------
CDNS-UVM-1.1d (15.10-s004)
(C) 2007-2013 Mentor Graphics Corporation
(C) 2007-2013 Cadence Design Systems, Inc.
(C) 2006-2013 Synopsys, Inc.
(C) 2011-2013 Cypress Semiconductor Corp.
----------------------------------------------------------------
UVM_INFO @ 0: reporter [RNTST] Running test base_test...
UVM_INFO @ 0: reporter [UVMTOP] UVM testbench topology:
-------------------------------------------------------
Name                 Type                   Size  Value
-------------------------------------------------------
uvm_test_top         base_test              -     @2602
  m_top_env          my_env                 -     @2668
    compA            componentA             -     @2699
      put_port       uvm_blocking_put_port  -     @3015
    compB            componentB             -     @2729
      analysis_port  uvm_analysis_port      -     @3115
      put_export     uvm_blocking_put_imp   -     @3066
    sub1             sub                    -     @2759
      analysis_imp   uvm_analysis_imp       -     @2808
    sub2             sub                    -     @2839
      analysis_imp   uvm_analysis_imp       -     @2888
    sub3             sub                    -     @2869
      analysis_imp   uvm_analysis_imp       -     @2966
-------------------------------------------------------

UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3205) { addr: 'h2f  data: 'h64  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(91) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from CompA
----------------------------------
Name    Type           Size  Value
----------------------------------
pkt     simple_packet  -     @3205
  addr  integral       8     'h2f
  data  integral       8     'h64
  rwb   integral       1     'h0
----------------------------------
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub1 [uvm_test_top.m_top_env.sub1] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub2 [uvm_test_top.m_top_env.sub2] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub3 [uvm_test_top.m_top_env.sub3] Sub got transaction
UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3198) { addr: 'hb8  data: 'h27  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(91) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from CompA
----------------------------------
Name    Type           Size  Value
----------------------------------
pkt     simple_packet  -     @3198
  addr  integral       8     'hb8
  data  integral       8     'h27
  rwb   integral       1     'h0
----------------------------------
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub1 [uvm_test_top.m_top_env.sub1] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub2 [uvm_test_top.m_top_env.sub2] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub3 [uvm_test_top.m_top_env.sub3] Sub got transaction
UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3170) { addr: 'he3  data: 'h29  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(91) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from CompA
----------------------------------
Name    Type           Size  Value
----------------------------------
pkt     simple_packet  -     @3170
  addr  integral       8     'he3
  data  integral       8     'h29
  rwb   integral       1     'h0
----------------------------------
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub1 [uvm_test_top.m_top_env.sub1] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub2 [uvm_test_top.m_top_env.sub2] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub3 [uvm_test_top.m_top_env.sub3] Sub got transaction
UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3175) { addr: 'h85  data: 'hbd  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(91) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from CompA
----------------------------------
Name    Type           Size  Value
----------------------------------
pkt     simple_packet  -     @3175
  addr  integral       8     'h85
  data  integral       8     'hbd
  rwb   integral       1     'h0
----------------------------------
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub1 [uvm_test_top.m_top_env.sub1] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub2 [uvm_test_top.m_top_env.sub2] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub3 [uvm_test_top.m_top_env.sub3] Sub got transaction
UVM_INFO ./tb/tlm.sv(61) @ 0: uvm_test_top.m_top_env.compA [COMPA] Packet sent to CompB
pkt: (simple_packet@3192) { addr: 'hdf  data: 'hab  rwb: 'h0  }
UVM_INFO ./tb/tlm.sv(91) @ 0: uvm_test_top.m_top_env.compB [COMPB] Packet received from CompA
----------------------------------
Name    Type           Size  Value
----------------------------------
pkt     simple_packet  -     @3192
  addr  integral       8     'hdf
  data  integral       8     'hab
  rwb   integral       1     'h0
----------------------------------
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub1 [uvm_test_top.m_top_env.sub1] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub2 [uvm_test_top.m_top_env.sub2] Sub got transaction
UVM_INFO ./tb/tlm.sv(122) @ 0: uvm_test_top.m_top_env.sub3 [uvm_test_top.m_top_env.sub3] Sub got transaction

--- UVM Report catcher Summary ---


Number of demoted UVM_FATAL reports  :    0
Number of demoted UVM_ERROR reports  :    0
Number of demoted UVM_WARNING reports:    0
Number of caught UVM_FATAL reports   :    0
Number of caught UVM_ERROR reports   :    0
Number of caught UVM_WARNING reports :    0

--- UVM Report Summary ---

** Report counts by severity
UVM_INFO :   27
UVM_WARNING :    0
UVM_ERROR :    0
UVM_FATAL :    0
** Report counts by id
[COMPA]     5
[COMPB]     5
[RNTST]     1
[UVMTOP]     1
[uvm_test_top.m_top_env.sub1]     5
[uvm_test_top.m_top_env.sub2]     5
[uvm_test_top.m_top_env.sub3]     5
Simulation complete via $finish(1) at time 0 FS + 179

For each of the five packets, compA sends, compB prints it, and then all three subscribers report receiving it, in the order they were connected. The report summary confirms 5 transactions per subscriber. The topology shows each subscriber's imp under the name analysis_imp: that is the instance name uvm_subscriber gives its analysis_export handle. This log was produced with UVM 1.1d, so the banner differs from UVM 1.2.

Quick Check

Q: If sub2 took 10 ns to process each transaction, could it implement that delay inside write()?

Show Answer

No. write() is a function, so it cannot contain a delay. A slow subscriber should store the transaction, for example in a queue or a uvm_tlm_analysis_fifo, and process it later in its own run_phase.

Where Analysis Ports Are Used

The classic use is the monitor in every agent. It reconstructs transactions from pin activity and writes each one to its analysis port, which is connected to a scoreboard, a coverage subscriber, and perhaps a transaction recorder. Adding a new checker later only needs one more connect() call in the environment; the monitor never changes. See UVM Monitor and Subscriber.

A receiver that needs two different analysis inputs cannot define write() twice. Use the `uvm_analysis_imp_decl macro to create imps with distinct method names, as shown in Using _decl Macro in TLM.

Analysis Exports and Analysis FIFOs

Two related classes complete the picture. A uvm_analysis_export forwards analysis transactions down the hierarchy to an imp in a child, in the same way a put export forwards put() calls. On the broadcasting side, a component such as an agent usually declares its own uvm_analysis_port and connects its monitor's port to it (port to port), so that the environment can connect subscribers to the agent without knowing the monitor inside.


// Inside an agent: expose the monitor's analysis port at the agent boundary
uvm_analysis_port #(simple_packet) ap;               // created in build_phase
virtual function void connect_phase(uvm_phase phase);
  m_monitor.ap.connect(this.ap);                     // child port to parent port
endfunction

A uvm_tlm_analysis_fifo is an unbounded FIFO with an analysis_export on its input side. Connecting an analysis port to it stores every written transaction, and a component can then pull them out with get() at its own pace. That is the standard way to feed a subscriber that needs time to process each transaction, as described in UVM TLM Fifo.

Common Mistakes

Mistake #1: Modifying a transaction after writing it

All subscribers receive the same handle. If the broadcaster changes or reuses the object after write(), every subscriber that stored the handle sees the change. Write a new object each time, or let subscribers that keep transactions store a clone.

Mistake #2: Putting time-consuming code in write()

write() must return immediately. Any processing that needs time belongs in the subscriber's own thread, fed by a queue or an analysis FIFO.

Mistake #3: Registering a parameterized subscriber with `uvm_component_utils

A subscriber declared as class sub #(type T = simple_packet) extends uvm_subscriber #(T) must use `uvm_component_param_utils(sub #(T)). Using `uvm_component_utils on a parameterized class is a registration error. Extending a concrete specialization, as above, avoids the issue entirely.

Mistake #4: Expecting write() to report whether anyone received the transaction

write() returns nothing and succeeds even with zero subscribers. If a scoreboard seems to receive nothing, the analysis port is probably not connected to it. Check the connect() calls in the environment, or print the topology, rather than looking for an error from the broadcaster.

Summary

In this article, you learned:

  • An analysis port broadcasts with write() to zero, one or many connected receivers
  • write() is a function, so it never blocks and receivers must not consume time in it
  • Subscribers derived from uvm_subscriber already provide analysis_export and only implement write(T t)
  • One analysis port is connected to each receiver with a separate connect() call
  • All receivers get the same object handle, so transactions should not be modified after they are written
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5