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()orwrite()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.
The building blocks
| Piece | Who owns it | What it does | Office picture |
|---|---|---|---|
uvm_*_port | The caller | Calls put(), get() or peek() | The phone you dial with |
uvm_*_imp | The callee | Holds the real method | The person who picks up |
uvm_*_export | A parent component | Forwards the call inward | The department front desk |
uvm_analysis_port | Usually a monitor | Broadcasts with write() | A PA announcement |
uvm_tlm_fifo | Sits between two components | Buffers items, fixed size | An in-tray with N slots |
uvm_tlm_analysis_fifo | Usually a scoreboard | Records every write(), read later with get() | A voicemail box |
The methods
| Method | Kind | Waits? |
|---|---|---|
put(t) | task | Yes, until the other side accepts it |
get(t) | task | Yes, until there is an item; removes it |
peek(t) | task | Yes, until there is an item; leaves it there |
try_put, try_get, try_peek | function | Never. Returns 0 if it can’t |
can_put, can_get, can_peek | function | Never. Just asks |
write(t) | function | Never. 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. Allconnect()calls belong inconnect_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 callconsumer::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’sput()is the consumer’sput()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 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)andput(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_exportfor the producer,get_exportfor 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.

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_port | uvm_tlm_fifo | uvm_tlm_analysis_fifo | |
|---|---|---|---|
| Call | write(), a function | put() and get(), tasks | write() in, get() out |
| Does the writer wait? | Never | Yes, when full | Never |
| Size | No storage | Default 1; set in new(); 0 = no limit | No limit |
| Listeners | 0, 1 or many | One putter, one getter (usually) | Any number of writers |
| Typical place | Monitor output | Between two active components | Inside a scoreboard or checker |
| Office picture | PA announcement | In-tray | Voicemail box |

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 yourconnect()lines ran.

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=1the 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.

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 classuvm_analysis_imp_expthat callswrite_exp(). Same for_act. - The expected item waits in
exp_quntil 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=0inreport_phaseconfirms nothing was left unchecked. Theuvm_erroralso 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.

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 wherewrite()really lands. Notice the FIFO’sanalysis_exportis really auvm_analysis_impunderneath: 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.

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 inend_of_elaboration_phase().
| Check | Call it in | Tells you |
|---|---|---|
port.size() | end_of_elaboration_phase() or later | How many connections |
port.debug_connected_to() | end_of_elaboration_phase() or later | The full path and where write() lands |
uvm_top.print_topology() | end_of_elaboration_phase() | Every component and port name |
| “connection count of 0” error | Read the instance name in it | Which 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=1reproduces 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 whatdo_copy()copies. A transaction with nested objects needs a deepdo_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 foundwrite()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_impwhosewrite()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 aget(), it returns the same item. chr_sue suggested blockingget()orpeek()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()andtry_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_fifocould 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_fifoanduvm_tlm_analysis_fifodiffer (Verification Academy, 2016). - Why: chr_sue, quoting the reference manual: the analysis FIFO is an unbounded
uvm_tlm_fifowith ananalysis_export. The plain one has noanalysis_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 theBUILDERRthat 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_sequencerunless you need to.
Every idea on this page, in one picture
| Idea | Picture it as |
|---|---|
| Component | A person in the office |
| Transaction | A paper passed around |
| Port | The phone in the caller’s hand |
| Imp | The person who picks up and does the work |
| Export | A department front desk that forwards the call |
Blocking put() | Handing a paper over the counter and waiting for “done” |
uvm_tlm_fifo | An in-tray with a fixed number of slots |
| Analysis port | A PA announcement: say it once, everyone hears it |
| No subscribers | An announcement in an empty office: no harm done |
uvm_tlm_analysis_fifo | A voicemail box that records every announcement |
uvm_analysis_imp_decl | Two phone lines into one desk, each with its own ringtone |
| Port up, export down | Calls go out through your own department’s front desk |
| Reused transaction object | Announcing “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_fifoanduvm_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.svanduvm_tlm_more.sv.
What to read next
- UVM scoreboard mismatch: 6 causes: what to do when the analysis-port data and the expected data disagree
- UVM sequencer and driver communication: the blocking TLM link every agent uses
- Shallow copy vs deep copy: why
clone()fixes the reused-object bug - UVM testbench architecture: where monitors, agents and scoreboards sit
- Mailbox in SystemVerilog: the plain SystemVerilog cousin of
uvm_tlm_fifo - UVM interview questions: TLM comes up in almost every one
- Stuck on a failing test? See the practical verification debug guide: five checks, in order, to find the root cause.
Part of the learning path UVM methodology
