UVM TLM Explained: Ports, Exports, Analysis Ports and FIFOs

In short: UVM TLM lets components hand whole transactions to each other through ports instead of signals. The component that calls owns a port, the component that does the work owns an imp, and connect() joins them. A uvm_analysis_port is one-to-many: write() is a function, returns at once, and reaches every subscriber (or none). A uvm_tlm_fifo sits between two components so they can run at different speeds; a uvm_tlm_analysis_fifo is an analysis subscriber with an unbounded FIFO inside, so a scoreboard can read items later with get().

Think of it like an office

Components are people in an office. Transactions are the papers they pass around.

  • Port: the phone in the caller’s hand. The component with the port is the one who dials.
  • Imp: the person who picks up and does the work. The real put() or write() code lives here.
  • Export: the front desk of a department. It doesn’t do the work; it forwards the call to someone inside.
  • **Blocking put(): you hand a paper over the counter and wait** until the clerk says “done”.
  • **uvm_tlm_fifo:** an in-tray with a fixed number of slots. You drop a paper in and walk away, unless the tray is full.
  • Analysis port: a PA announcement. You speak once, and everyone with a speaker hears it, whether that’s 0, 1 or 10 people. You never wait for anyone.
  • **uvm_tlm_analysis_fifo:** a voicemail box on one of those speakers. It records every announcement so its owner can listen later, at their own pace.

One catch, and it’s the most common TLM bug: if the announcement says “look at the whiteboard” instead of reading the details out, anyone listening later sees whatever is on the board now. In UVM terms, the subscriber gets a handle, not a copy. Example 4 shows it.

UVM TLM overview: a port calls an imp with put; a producer and getter both call into a uvm_tlm_fifo; a monitor analysis port writes once to a scoreboard imp, a coverage subscriber and an analysis FIFO
Figure 1. UVM TLM in one picture: who calls, who does the work.

The building blocks

PieceWho owns itWhat it doesOffice picture
uvm_*_portThe callerCalls put(), get() or peek()The phone you dial with
uvm_*_impThe calleeHolds the real methodThe person who picks up
uvm_*_exportA parent componentForwards the call inwardThe department front desk
uvm_analysis_portUsually a monitorBroadcasts with write()A PA announcement
uvm_tlm_fifoSits between two componentsBuffers items, fixed sizeAn in-tray with N slots
uvm_tlm_analysis_fifoUsually a scoreboardRecords every write(), read later with get()A voicemail box

The methods

MethodKindWaits?
put(t)taskYes, until the other side accepts it
get(t)taskYes, until there is an item; removes it
peek(t)taskYes, until there is an item; leaves it there
try_put, try_get, try_peekfunctionNever. Returns 0 if it can’t
can_put, can_get, can_peekfunctionNever. Just asks
write(t)functionNever. Analysis only

write() being a function is the whole point of analysis ports. A monitor can’t pause the design while a slow scoreboard catches up, so it must never wait.

Three connect rules

  • **Call connect() on the caller’s side:** prod.put_port.connect(cons.put_imp);. The port goes on the left. All connect() calls belong in connect_phase(), which runs after every component is built (UVM phases in order).
  • Up and down the hierarchy: a child’s port connects to its parent’s port on the way up. A parent’s export connects to its child’s export or imp on the way down. At the meeting point, a port connects to an export or imp. Example 6 shows all three.
  • Every chain ends at an imp. The imp names the class that owns the method: uvm_blocking_put_imp #(pkt, consumer) makes UVM call consumer::put().

A plain port must have at least one connection, or UVM stops before the run starts. An analysis port is allowed 0 connections. The debug checklist below shows both on VCS.

Example 1: a blocking put, port to imp

The producer has a uvm_blocking_put_port. The consumer has a uvm_blocking_put_imp and a put() task that takes 10 ns per item.

class producer extends uvm_component;
  `uvm_component_utils(producer)
  uvm_blocking_put_port #(pkt) put_port;
  int n = 3;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); put_port = new("put_port", this); endfunction
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    for (int i = 0; i < n; i++) begin
      pkt p = pkt::type_id::create("p");
      p.id = i;
      $display("%0t  producer: put(%0d) called", $time, i);
      put_port.put(p);                       // blocks until the consumer returns
      $display("%0t  producer: put(%0d) returned", $time, i);
    end
    phase.drop_objection(this);
  endtask
endclass

class consumer extends uvm_component;
  `uvm_component_utils(consumer)
  uvm_blocking_put_imp #(pkt, consumer) put_imp;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); put_imp = new("put_imp", this); endfunction
  task put(pkt p);                           // the imp's real method
    $display("%0t  consumer: working on %0d", $time, p.id);
    #10;                                     // takes 10 ns per item
  endtask
endclass

class put_test extends uvm_test;
  `uvm_component_utils(put_test)
  producer prod; consumer cons;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    prod = producer::type_id::create("prod", this);
    cons = consumer::type_id::create("cons", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    prod.put_port.connect(cons.put_imp);     // caller's port -> callee's imp
  endfunction
endclass

Output (Synopsys VCS 2025.06, UVM 1.2, 12 Nov 2026):

0  producer: put(0) called
0  consumer: working on 0
10  producer: put(0) returned
10  producer: put(1) called
10  consumer: working on 1
20  producer: put(1) returned
20  producer: put(2) called
20  consumer: working on 2
30  producer: put(2) returned
  • put(0) is called at 0 and returns at 10. The producer’s put() is the consumer’s put() task, so it takes as long as the consumer does.
  • The two never overlap. Three items take 30 ns, one after another.
  • Office picture: you’re standing at the counter until the clerk says “done”. If you want to drop the paper and walk away, you need an in-tray. That’s example 2.
Example 1 timeline on VCS: put 0 waits from 0 to 10 ns while the consumer works, put 1 from 10 to 20, put 2 from 20 to 30
Figure 2. Example 1 on VCS: each put() waits for the consumer.

Example 2: an in-tray between them (uvm_tlm_fifo)

Same producer, now with 4 items. The getter pulls items with a uvm_blocking_get_port and takes 10 ns per item. A uvm_tlm_fifo of size 2 sits between them.

class getter extends uvm_component;
  `uvm_component_utils(getter)
  uvm_blocking_get_port #(pkt) get_port;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); get_port = new("get_port", this); endfunction
  task run_phase(uvm_phase phase);
    pkt p;
    forever begin
      get_port.get(p);                       // blocks while the FIFO is empty
      $display("%0t  getter:   got %0d", $time, p.id);
      #10;                                   // slow: 10 ns per item
    end
  endtask
endclass

class fifo_test extends uvm_test;
  `uvm_component_utils(fifo_test)
  producer prod; getter gtr; uvm_tlm_fifo #(pkt) fifo;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    prod = producer::type_id::create("prod", this);
    prod.n = 4;
    gtr  = getter::type_id::create("gtr", this);
    fifo = new("fifo", this, 2);             // room for 2 items
  endfunction
  function void connect_phase(uvm_phase phase);
    prod.put_port.connect(fifo.put_export);
    gtr.get_port.connect(fifo.get_export);
  endfunction
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    #50;                                     // let the getter drain the FIFO
    phase.drop_objection(this);
  endtask
endclass

Output (Synopsys VCS 2025.06, UVM 1.2, 12 Nov 2026):

0  producer: put(0) called
0  producer: put(0) returned
0  producer: put(1) called
0  producer: put(1) returned
0  producer: put(2) called
0  producer: put(2) returned
0  producer: put(3) called
0  getter:   got 0
10  getter:   got 1
10  producer: put(3) returned
20  getter:   got 2
30  getter:   got 3
  • put(0), put(1) and put(2) all return at 0. Compare example 1, where each one took 10 ns.
  • Why three and not two? The getter takes item 0 at time 0, which leaves room for 1 and 2. Now the tray is full.
  • put(3) waits until 10 ns, when the getter takes item 1 and a slot opens.
  • Both sides connect into the FIFO: put_export for the producer, get_export for the getter. Neither knows the other exists.
  • The test holds the run phase open for 50 ns with an objection, so the getter has time to drain the FIFO. More in how a UVM test ends.
Example 2 timeline on VCS: put 0, 1 and 2 return at 0 ns; the FIFO is full with 1 and 2; put 3 waits until 10 ns when the getter takes item 1; the getter takes items at 0, 10, 20 and 30 ns
Figure 3. Example 2 on VCS: the FIFO lets the producer run ahead until it is full.

Analysis port vs FIFO: which one, when?

This is the question most people land here with. Short answer: an analysis port is for broadcasting what happened; a TLM FIFO is for handing work from one component to another. And an analysis FIFO is how a broadcast gets stored for later.

uvm_analysis_portuvm_tlm_fifouvm_tlm_analysis_fifo
Callwrite(), a functionput() and get(), taskswrite() in, get() out
Does the writer wait?NeverYes, when fullNever
SizeNo storageDefault 1; set in new(); 0 = no limitNo limit
Listeners0, 1 or manyOne putter, one getter (usually)Any number of writers
Typical placeMonitor outputBetween two active componentsInside a scoreboard or checker
Office picturePA announcementIn-trayVoicemail box
Flowchart: if A is a monitor use an analysis port, to an imp if B handles items in write() or to an analysis FIFO if B reads later; otherwise use a blocking put port if A should wait, a uvm_tlm_fifo if A pushes at its own pace, or a get port if B pulls
Figure 4. Which TLM connection do I need?

Example 3: one write(), two subscribers

The monitor writes 3 items, 5 ns apart. A scoreboard (with a uvm_analysis_imp) and a coverage collector (a uvm_subscriber) both listen.

class monitor extends uvm_component;
  `uvm_component_utils(monitor)
  uvm_analysis_port #(pkt) ap;
  bit reuse = 0;                             // Example 4: reuse one object?
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); ap = new("ap", this); endfunction
  task run_phase(uvm_phase phase);
    pkt p;
    phase.raise_objection(this);
    p = pkt::type_id::create("p");
    for (int i = 1; i <= 3; i++) begin
      if (!reuse) p = pkt::type_id::create("p");   // a new object every time
      p.id = i;
      $display("%0t  monitor:  write(%0d)", $time, i);
      ap.write(p);                           // a function: returns at once
      #5;
    end
    phase.drop_objection(this);
  endtask
endclass

class scoreboard extends uvm_component;
  `uvm_component_utils(scoreboard)
  uvm_analysis_imp #(pkt, scoreboard) imp;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); imp = new("imp", this); endfunction
  function void write(pkt p);
    $display("%0t  scoreboard got %0d", $time, p.id);
  endfunction
endclass

class cov extends uvm_subscriber #(pkt);
  `uvm_component_utils(cov)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(pkt t);
    $display("%0t  coverage   got %0d", $time, t.id);
  endfunction
endclass

class ap_test extends uvm_test;
  `uvm_component_utils(ap_test)
  monitor mon; scoreboard sb; cov cv;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    mon = monitor::type_id::create("mon", this);
    sb  = scoreboard::type_id::create("sb", this);
    cv  = cov::type_id::create("cv", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    mon.ap.connect(sb.imp);                  // one port ...
    mon.ap.connect(cv.analysis_export);      // ... many subscribers
  endfunction
  function void report_phase(uvm_phase phase);
    $display("subscribers on mon.ap: %0d", mon.ap.size());
  endfunction
endclass

Output (Synopsys VCS 2025.06, UVM 1.2, 12 Nov 2026):

0  monitor:  write(1)
0  coverage   got 1
0  scoreboard got 1
5  monitor:  write(2)
5  coverage   got 2
5  scoreboard got 2
10  monitor:  write(3)
10  coverage   got 3
10  scoreboard got 3
subscribers on mon.ap: 2
  • Both subscribers get each item at the same time as the write(). No time passes; the monitor never waits.
  • Coverage printed first every time, although the scoreboard was connected first. Don’t build anything that depends on which subscriber hears first.
  • mon.ap.size() reports 2 connections. A handy check that your connect() lines ran.
Example 3 on VCS: at 0, 5 and 10 ns the monitor writes once and both coverage and scoreboard get the item in the same time step, coverage first
Figure 5. Example 3 on VCS: one write(), two subscribers, same time step.

Example 4: an analysis FIFO in a scoreboard, and the reused-object trap

The scoreboard here is busy for the first 20 ns, so it keeps a uvm_tlm_analysis_fifo and reads later with get(). The monitor from example 3 has a switch: +REUSE=1 makes it reuse one object (a plusarg, so no recompile) for every write().

class fifo_sb extends uvm_component;
  `uvm_component_utils(fifo_sb)
  uvm_tlm_analysis_fifo #(pkt) fifo;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); fifo = new("fifo", this); endfunction
  task run_phase(uvm_phase phase);
    pkt p;
    #20;                                     // busy at first, reads late
    forever begin
      fifo.get(p);
      $display("%0t  fifo_sb read %0d", $time, p.id);
    end
  endtask
endclass

class reuse_test extends uvm_test;
  `uvm_component_utils(reuse_test)
  monitor mon; fifo_sb sb;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    mon = monitor::type_id::create("mon", this);
    sb  = fifo_sb::type_id::create("sb", this);
    void'($value$plusargs("REUSE=%d", mon.reuse));
    $display("monitor reuses one object: %0d", mon.reuse);
  endfunction
  function void connect_phase(uvm_phase phase);
    mon.ap.connect(sb.fifo.analysis_export);
  endfunction
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    #30;
    phase.drop_objection(this);
  endtask
endclass

**Output, +REUSE=0** (a new object for every write; VCS 2025.06, UVM 1.2, 12 Nov 2026):

monitor reuses one object: 0
0  monitor:  write(1)
5  monitor:  write(2)
10  monitor:  write(3)
20  fifo_sb read 1
20  fifo_sb read 2
20  fifo_sb read 3

**Output, +REUSE=1** (one object, reused):

monitor reuses one object: 1
0  monitor:  write(1)
5  monitor:  write(2)
10  monitor:  write(3)
20  fifo_sb read 3
20  fifo_sb read 3
20  fifo_sb read 3
  • The analysis FIFO stored all three writes, and the monitor never waited for the busy scoreboard. That’s the voicemail box doing its job.
  • With +REUSE=1 the FIFO still holds three entries, but all three are the same handle. By the time the scoreboard reads at 20 ns, that one object says 3.
  • No error, no warning. Just a scoreboard that silently checks the wrong data. Mistake 1 below has three ways to fix it, each run on VCS.
Example 4: with a new object per write the analysis FIFO holds three objects and the scoreboard reads 1 2 3; with one reused object all three slots point to the same object and it reads 3 3 3
Figure 6. Example 4: the analysis FIFO stores handles, not copies.

Working example 5: a scoreboard with two inputs

A real scoreboard compares an expected stream (from a reference model) with an actual stream (from the output monitor). One class can’t have two write() functions, so UVM gives you a macro that makes a new imp type with its own write name.

`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)

class cmp_sb extends uvm_component;
  `uvm_component_utils(cmp_sb)
  uvm_analysis_imp_exp #(pkt, cmp_sb) exp_imp;
  uvm_analysis_imp_act #(pkt, cmp_sb) act_imp;
  pkt exp_q[$];
  int match, mismatch;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    exp_imp = new("exp_imp", this);
    act_imp = new("act_imp", this);
  endfunction
  function void write_exp(pkt t);            // called by the expected stream
    exp_q.push_back(t);
  endfunction
  function void write_act(pkt t);            // called by the actual stream
    pkt e;
    if (exp_q.size() == 0) begin
      `uvm_error("SB", $sformatf("actual %0d arrived with nothing expected", t.id)) return;
    end
    e = exp_q.pop_front();
    if (e.id == t.id) begin match++; $display("%0t  match    %0d", $time, t.id); end
    else begin mismatch++; `uvm_error("SB", $sformatf("expected %0d, got %0d", e.id, t.id)) end
  endfunction
  function void report_phase(uvm_phase phase);
    $display("matches=%0d mismatches=%0d left in exp_q=%0d", match, mismatch, exp_q.size());
  endfunction
endclass

class multi_sb_test extends uvm_test;
  `uvm_component_utils(multi_sb_test)
  mon exp_m; bad_mon act_m; cmp_sb sb;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    exp_m = mon::type_id::create("exp_m", this);  exp_m.n = 4;
    act_m = bad_mon::type_id::create("act_m", this);
    sb    = cmp_sb::type_id::create("sb", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    exp_m.ap.connect(sb.exp_imp);
    act_m.ap.connect(sb.act_imp);
  endfunction
endclass

The actual side adds 2 ns of latency and gets item 3 wrong on purpose:

class bad_mon extends mon;                   // actual side: item 3 comes out wrong
  `uvm_component_utils(bad_mon)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    #2;                                      // the design adds 2 ns of latency
    foreach (ids[i]) begin
      pkt p = pkt::type_id::create("p");
      p.id = ids[i];
      ap.write(p);
      #gap;
    end
    phase.drop_objection(this);
  endtask
  int ids[] = '{1, 2, 7, 4};
endclass

Output (Synopsys VCS 2025.06, UVM 1.2, 12 Nov 2026):

2  match    1
7  match    2
UVM_ERROR testbench.sv(122) @ 12: uvm_test_top.sb [SB] expected 3, got 7
17  match    4
matches=3 mismatches=1 left in exp_q=0
  • The uvm_analysis_imp_decl(_exp) macro creates a class uvm_analysis_imp_exp that calls write_exp(). Same for _act.
  • The expected item waits in exp_q until the matching actual item arrives 2 ns later.
  • The wrong item is caught at 12 ns, at the exact moment it arrives. left in exp_q=0 in report_phase confirms nothing was left unchecked. The uvm_error also lands in the report summary at the end of the test (UVM reporting and verbosity).
  • Office picture: two phone lines into one desk, each with its own ringtone, so the clerk knows which caller is which.
Example 5 on VCS: expected items 1 to 4 at 0, 5, 10, 15 ns; actual items 1, 2, 7, 4 at 2, 7, 12, 17 ns; the scoreboard matches 1, 2 and 4 and reports an error at 12 ns, expected 3 got 7
Figure 7. Example 5 on VCS: a two-input scoreboard catches item 3.

Working example 6: wiring through the agent and env

In a real testbench the monitor lives inside an agent, and the scoreboard sits next to the agent in the env (the full layout is in UVM testbench architecture, and a step-by-step build in how to build a UVM environment, part 3). The connection has to climb out of the agent, cross the env, and drop into the scoreboard.

class agent extends uvm_component;
  `uvm_component_utils(agent)
  mon m;
  uvm_analysis_port #(pkt) ap;               // the agent's own port
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    m  = mon::type_id::create("m", this);
    ap = new("ap", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    m.ap.connect(ap);                        // UP: child port -> parent port
  endfunction
endclass

class fifo_board extends uvm_component;
  `uvm_component_utils(fifo_board)
  uvm_analysis_export #(pkt) analysis_export;  // the scoreboard's front desk
  uvm_tlm_analysis_fifo #(pkt) fifo;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    analysis_export = new("analysis_export", this);
    fifo = new("fifo", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    analysis_export.connect(fifo.analysis_export);   // DOWN: export -> child export
  endfunction
  task run_phase(uvm_phase phase);
    pkt t;
    forever begin fifo.get(t); $display("%0t  board got %0d", $time, t.id); end
  endtask
endclass

class env extends uvm_component;
  `uvm_component_utils(env)
  agent agt; fifo_board sb;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    agt = agent::type_id::create("agt", this);
    sb  = fifo_board::type_id::create("sb", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    agt.ap.connect(sb.analysis_export);      // ACROSS: port -> export
  endfunction
endclass

class hier_test extends uvm_test;
  `uvm_component_utils(hier_test)
  env e;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); e = env::type_id::create("e", this); endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    e.agt.m.ap.debug_connected_to();         // where does the monitor's write() end up?
  endfunction
endclass

Output (Synopsys VCS 2025.06, UVM 1.2, 12 Nov 2026):

UVM_INFO @ 0: uvm_test_top.e.agt.m.ap [debug_connected_to] This port's fanout network:

  uvm_test_top.e.agt.m.ap (uvm_analysis_port)
    |
    |_uvm_test_top.e.agt.ap (uvm_analysis_port)
        |
        |_uvm_test_top.e.sb.analysis_export (uvm_analysis_export)
            |
            |_uvm_test_top.e.sb.fifo.analysis_export (uvm_analysis_imp)

  Resolved implementation list:
  0: uvm_test_top.e.sb.fifo.analysis_export (uvm_analysis_imp)

0  board got 1
5  board got 2
10  board got 3
  • Up: inside the agent, m.ap.connect(ap). Child port to parent port.
  • Across: inside the env, agt.ap.connect(sb.analysis_export). Port to export.
  • Down: inside the scoreboard, analysis_export.connect(fifo.analysis_export). Export to the child’s export.
  • debug_connected_to() prints the whole path, and the “resolved implementation list” names where write() really lands. Notice the FIFO’s analysis_export is really a uvm_analysis_imp underneath: the chain ends at an imp, as it must.
  • Why bother with the agent’s own port? The env only talks to the agent. Swap the monitor inside, and nothing in the env changes.
Example 6: monitor ap connects up to the agent ap, across to the scoreboard analysis_export, and down to the analysis FIFO imp inside the scoreboard
Figure 8. Example 6: up, across and down to the scoreboard FIFO.

Debug checklist: what is connected to what?

When a scoreboard sees nothing, check the wiring before the logic. All four checks below ran on VCS.

class debug_test extends uvm_test;
  `uvm_component_utils(debug_test)
  mon m; keep_sb a, b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    m = mon::type_id::create("m", this);
    a = keep_sb::type_id::create("a", this);
    b = keep_sb::type_id::create("b", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    m.ap.connect(a.imp);
    m.ap.connect(b.imp);
  endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    $display("m.ap.size() = %0d", m.ap.size());
    m.ap.debug_connected_to();
    uvm_top.print_topology();
  endfunction
endclass

1. Count the connections. size() on any port:

m.ap.size() = 2

2. Print where a port leads. debug_connected_to():

UVM_INFO @ 0: uvm_test_top.m.ap [debug_connected_to] This port's fanout network:

  uvm_test_top.m.ap (uvm_analysis_port)
    |
    |_uvm_test_top.a.imp (uvm_analysis_imp)
    |
    |_uvm_test_top.b.imp (uvm_analysis_imp)

  Resolved implementation list:
  0: uvm_test_top.a.imp (uvm_analysis_imp)
  1: uvm_test_top.b.imp (uvm_analysis_imp)

3. Print the whole testbench. uvm_top.print_topology() lists every component and every port by name:

--------------------------------------------
Name          Type               Size  Value
--------------------------------------------
uvm_test_top  debug_test         -     @336
  a           keep_sb            -     @358
    imp       uvm_analysis_imp   -     @376
  b           keep_sb            -     @367
    imp       uvm_analysis_imp   -     @386
  m           mon                -     @349
    ap        uvm_analysis_port  -     @396
--------------------------------------------

4. Know the error. Here a put port and an analysis port are both left unconnected (unconnected_test in the download):

UVM_ERROR @ 0: uvm_test_top.l.put_port [Connection Error] connection count of 0 does not meet required minimum of 1
UVM_FATAL @ 0: reporter [BUILDERR] stopping due to build errors
  • Only the put port complained. The unconnected analysis port (uvm_test_top.m.ap) is legal: a PA announcement in an empty office.
  • The test stops at time 0 with BUILDERR, before the run phase starts. So this one you can’t miss. A missing analysis connection is the one that slips through silently, which is why checks 1 and 2 are worth adding in end_of_elaboration_phase().
CheckCall it inTells you
port.size()end_of_elaboration_phase() or laterHow many connections
port.debug_connected_to()end_of_elaboration_phase() or laterThe full path and where write() lands
uvm_top.print_topology()end_of_elaboration_phase()Every component and port name
“connection count of 0” errorRead the instance name in itWhich port has nothing on the other end

Common mistakes (and the fixes for each)

These come from real Verification Academy threads, retold in my own words with links. Each fix ran on VCS 2025.06 with UVM 1.2, on 12 Nov 2026, using the two demo files.

1. The scoreboard only ever sees the latest transaction

  • What happened: in 2019 an engineer’s scoreboard pulled items from an analysis FIFO, and every get() returned the newest transaction (Verification Academy, 2019). In 2018 another engineer saw duplicates and overwritten packets in a switch scoreboard (Verification Academy, 2018). A third thread had three monitors with the same problem (Verification Academy).
  • Why: the writer reused one object. Analysis ports and FIFOs pass handles, so every stored entry pointed at the same object. Example 4 with +REUSE=1 reproduces it: 3 3 3. In both the 2018 and 2019 threads, the fix was found on the writing side.

Fix A: a new object for every write (in the monitor). Move the create() inside the loop. type_id::create() goes through the UVM factory, which is why it’s preferred over new() (create() vs new()).

for (int i = 1; i <= 3; i++) begin
  pkt p = pkt::type_id::create("p");   // new object each time
  p.id = i;
  ap.write(p);
end

Output: example 4 with +REUSE=0 reads 1 2 3.

Fix B: clone before you write (in the monitor). Useful when the monitor builds one object over many cycles and you don’t want to restructure it.

pkt c;
$cast(c, p.clone());   // a copy, with its own memory
ap.write(c);

Output (clone_test +MODE=2):

keep_sb stored [0] id=1
keep_sb stored [1] id=2
keep_sb stored [2] id=3

Fix C: clone on arrival (in the subscriber). For when you don’t own the monitor. The monitor here still reuses one object, and the scoreboard protects itself:

function void write(pkt t);
  pkt c;
  $cast(c, t.clone());
  q.push_back(c);      // store the copy, never the handle you were given
endfunction

Output (clone_test +MODE=3, monitor still reusing): id=1, id=2, id=3, the same as fix B.

  • Which to pick: A or B fixes it for every subscriber at once. C only protects the subscriber that does it, so use it as a second line of defence.
  • clone() copies only what do_copy() copies. A transaction with nested objects needs a deep do_copy(); see shallow copy vs deep copy.

2. Expecting an export to do something on its own

  • What happened: in the 2019 thread above, the scoreboard had an analysis export and tried to get() from it. In a 2013 thread, items needed routing to different FIFOs, and a later poster found write() never ran when the port went straight to a FIFO’s export (Verification Academy, 2013).
  • Why: an export has no storage and no code. It only forwards. chr_sue pointed out in 2019 that you need an analysis FIFO to keep items. Example 6’s printout shows it: the chain only works because it ends at a uvm_analysis_imp.

Fix A: end the chain at your own imp. Put a uvm_analysis_imp in the scoreboard and write the write() function yourself, like example 3. Best when each item can be handled the moment it arrives.

Fix B: put an analysis FIFO behind the export. The scoreboard keeps its analysis_export as a front door and connects it to a FIFO inside, like example 6:

analysis_export.connect(fifo.analysis_export);   // in the scoreboard's connect_phase
...
forever begin fifo.get(t); /* check t */ end      // in its run_phase

Best when checking takes time or must wait for other data.

  • Dave Rich’s 2013 answer adds a third pattern: one uvm_analysis_imp whose write() pushes each item into the right FIFO by its content. Same rule, the chain ends in your code.

3. Polling with try_peek() and getting the same item twice

  • What happened: in the 2018 duplicates thread, debug with try_peek() showed the same packet more than once.
  • Why: try_peek() doesn’t remove anything. Called again before a get(), it returns the same item. chr_sue suggested blocking get() or peek() instead.

Here it is on VCS (fifo_api_test):

f0.try_peek -> id=1, used still 3
f0.try_peek -> id=1, used still 3
f0.get      -> id=1, used now 2
f0.get      -> id=2, used now 1

**Fix A: blocking get() in a forever loop.** It waits for an item and removes it, so each one is seen once. Example 4’s fifo_sb does exactly this.

**Fix B: if you must poll, use try_get().** It removes the item too, and returns 0 when the FIFO is empty, so check that return value.

  • Keep peek() and try_peek() for “is there something for me?” decisions, never for consuming items.

4. Assuming a uvm_tlm_fifo has room for everything

  • What happened: in 2015 an engineer thought uvm_tlm_fifo could only hold one item (Verification Academy, 2015).
  • Why: the size is a constructor argument, and its default is 1. Dave Rich confirmed this is in the source and the reference manual.
class fifo_api_test extends uvm_test;
  `uvm_component_utils(fifo_api_test)
  uvm_tlm_fifo #(pkt) f1, f0;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    f1 = new("f1", this);          // no size given: default is 1
    f0 = new("f0", this, 0);       // 0 = no limit
  endfunction
  task run_phase(uvm_phase phase);
    pkt p, t; bit ok;
    phase.raise_objection(this);
    for (int i = 1; i <= 3; i++) begin
      p = pkt::type_id::create("p"); p.id = i;
      ok = f1.try_put(p);
      $display("f1.try_put(%0d) = %0d   used=%0d size=%0d full=%0d", i, ok, f1.used(), f1.size(), f1.is_full());
      void'(f0.try_put(p));
    end
    $display("f0 (size 0) holds %0d items", f0.used());
    repeat (2) begin
      ok = f0.try_peek(t);
      $display("f0.try_peek -> id=%0d, used still %0d", t.id, f0.used());
    end
    f0.get(t);  $display("f0.get      -> id=%0d, used now %0d", t.id, f0.used());
    f0.get(t);  $display("f0.get      -> id=%0d, used now %0d", t.id, f0.used());
    phase.drop_objection(this);
  endtask
endclass

Output (Synopsys VCS 2025.06, UVM 1.2, 12 Nov 2026):

f1.try_put(1) = 1   used=1 size=1 full=1
f1.try_put(2) = 0   used=1 size=1 full=1
f1.try_put(3) = 0   used=1 size=1 full=1
f0 (size 0) holds 3 items

Fix A: pass a size. new("fifo", this, 2) in example 2 holds two.

Fix B: pass 0 for no limit. f0 above took all three. Use it when the writer must never wait.

Fix C: check before you rely on it. try_put() returns 0 when the FIFO is full, and used(), size() and is_full() tell you where you stand. With a blocking put() a full FIFO doesn’t fail; the caller just waits, which looks like a hang.

5. Using a plain uvm_tlm_fifo where an analysis FIFO belongs

  • What happened: in 2016 someone asked how uvm_tlm_fifo and uvm_tlm_analysis_fifo differ (Verification Academy, 2016).
  • Why: chr_sue, quoting the reference manual: the analysis FIFO is an unbounded uvm_tlm_fifo with an analysis_export. The plain one has no analysis_export, so a monitor’s port has nothing to connect to, and it can fill up.

**Fix A: use uvm_tlm_analysis_fifo** whenever the writer is an analysis port, like examples 4 and 6.

Fix B: write your own imp and queue when you need to filter or route items as they arrive. keep_sb in the download does this:

class keep_sb extends uvm_component;
  `uvm_component_utils(keep_sb)
  uvm_analysis_imp #(pkt, keep_sb) imp;
  bit clone_in_sb;
  pkt q[$];
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); imp = new("imp", this); endfunction
  function void write(pkt t);
    if (clone_in_sb) begin pkt c; $cast(c, t.clone()); q.push_back(c); end
    else q.push_back(t);
  endfunction
  function void check_phase(uvm_phase phase);
    foreach (q[i]) $display("keep_sb stored [%0d] id=%0d", i, q[i].id);
  endfunction
endclass

6. Asking why the driver blocks but the monitor broadcasts

  • What happened: an engineer asked why driver-to-sequencer uses a blocking TLM port but monitor-to-scoreboard uses an analysis port (Verification Academy).
  • Why: chr_sue’s answer: the driver decides when it wants the next item, so waiting is fine. The monitor can’t stop the design, so it must never wait, and it may have several listeners.

Fix A: active side, blocking ports. Driver and sequencer: the driver pulls when ready (the get-style link UVM wires for you; see UVM sequencer and driver communication).

Fix B: passive side, analysis ports. Monitor and everything it feeds. If a listener is slow, give it an analysis FIFO (example 4) rather than slowing the monitor down.

  • The flowchart above is this rule as a picture.

7. “connection count of 0 does not meet required minimum of 1”

  • What happened: in 2019 an engineer’s extended sequencer had super.connect_phase(phase) commented out and got this error (Verification Academy, 2019).
  • Why: chr_sue explained that the parent’s connect_phase() makes the sequencer’s own TLM connections. Skip it and a port is left with nothing. Check 4 in the debug checklist shows the exact message and the BUILDERR that follows.

**Fix A: always call super** when you override a phase: super.connect_phase(phase); and super.build_phase(phase); first.

Fix B: read the instance name in the error. uvm_test_top.l.put_port points straight at the port with nothing on the other end. Then follow it with debug_connected_to() from the checklist.

  • cgales also advised against extending uvm_sequencer unless you need to.

Every idea on this page, in one picture

IdeaPicture it as
ComponentA person in the office
TransactionA paper passed around
PortThe phone in the caller’s hand
ImpThe person who picks up and does the work
ExportA department front desk that forwards the call
Blocking put()Handing a paper over the counter and waiting for “done”
uvm_tlm_fifoAn in-tray with a fixed number of slots
Analysis portA PA announcement: say it once, everyone hears it
No subscribersAn announcement in an empty office: no harm done
uvm_tlm_analysis_fifoA voicemail box that records every announcement
uvm_analysis_imp_declTwo phone lines into one desk, each with its own ringtone
Port up, export downCalls go out through your own department’s front desk
Reused transaction objectAnnouncing “look at the whiteboard”, then rewriting the board
clone() before write()Handing out a photocopy instead

FAQ

What is TLM in UVM?

Transaction-level modelling: components pass whole transaction objects through ports and exports instead of driving signals.

What is the difference between a port, an export and an imp?

A port is on the caller’s side. An imp is on the side that holds the real method. An export forwards a connection from a parent to a child.

What is the difference between an analysis port and a TLM FIFO?

An analysis port broadcasts with write(), a function that never waits, to 0 or more subscribers. A uvm_tlm_fifo buffers items between one producer and one consumer, and put() waits when it is full.

When should I use uvm_tlm_analysis_fifo?

When a subscriber, usually a scoreboard, wants to read items later with get() instead of handling each one inside write(). It is unbounded, so the monitor never waits.

What is the default size of uvm_tlm_fifo?

It is 1. Pass a size to the constructor; 0 means no limit.

How do I connect two analysis ports to one scoreboard?

Use the uvm_analysis_imp_decl(_suffix) macro once per input. Each makes an imp type that calls write_suffix(). Example 5 shows it.

Why does my scoreboard see the same transaction many times?

The monitor is reusing one object. Create a new one, or clone() it, before every write().

Does an unconnected analysis port cause an error?

No. Only plain ports need a connection. That’s why a missing analysis connection is easy to miss; check with size() or debug_connected_to().

References

  • Accellera UVM 1.2 class reference: TLM1 interfaces, ports, exports and imps; uvm_analysis_port; uvm_tlm_fifo and uvm_tlm_analysis_fifo; uvm_subscriber; uvm_analysis_imp_decl.
  • IEEE 1800.2-2020, UVM standard: clause 12 (TLM1 and analysis).
  • Verification Academy forum threads linked in the mistakes section (accessed 12 Nov 2026).
  • All examples run on Synopsys VCS 2025.06 with UVM 1.2 on 12 Nov 2026: 12 runs across two files, uvm_tlm_demo.sv and uvm_tlm_more.sv.

What to read next