UVM TLM: Ports, Exports, Imps, and Analysis Ports
In a UVM testbench, components rarely pass data by poking each other’s pins or reaching into each other’s members. Instead they exchange whole transactions — objects such as a bus read or a packet — through well-defined channels. That style is called transaction-level modeling, and UVM TLM is the set of ports, exports, and FIFOs that make it work. This decoupling lets you swap a driver or add a checker without touching its neighbours.
This page is example-driven. The UVM snippets below are shown as syntax references: to elaborate and run them you need a UVM-capable simulator such as industry-standard simulation tools, VCS, or industry-standard simulators (or a free EDA Playground session); the UVM class library cannot be linked by lightweight open-source tools. The plain-SystemVerilog example near the end has no UVM dependency and runs on any simulator, so you can watch the same idea execute.
Ports, exports, and imps
Every TLM connection has two ends. One end needs a method to be available; the other end provides it. UVM names the three building blocks like this:
| Object | Role | Think of it as |
|---|---|---|
uvm_*_port | Requires an interface — the component calls a method it does not implement. | A plug that needs a socket. |
uvm_*_export | Forwards a required interface through the hierarchy to where it is implemented. | An extension cord. |
uvm_*_imp | Provides the implementation — binds the method to a real task or function. | The socket itself. |
A producer declares a port because it calls put(); a consumer declares an imp because it implements put(). The #(my_txn) parameter is the transaction type on the link.
class producer extends uvm_component;
// A PORT requires an interface: the component calls put(), someone else implements it.
uvm_blocking_put_port #(my_txn) put_port;
`uvm_component_utils(producer)
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
endclass
class consumer extends uvm_component;
// An IMP provides the implementation, binding put() to a local method.
uvm_blocking_put_imp #(my_txn, consumer) put_export;
`uvm_component_utils(consumer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
put_export = new("put_export", this);
endfunction
task put(my_txn t); // the imp forwards every put() call here
`uvm_info("CONS", $sformatf("got %s", t.convert2string()), UVM_LOW)
endtask
endclassThe two ends are joined in the parent’s connect_phase. The rule is always port.connect(export-or-imp) — the requiring side connects to the providing side, never the reverse.
// In the parent component's connect_phase:
prod.put_port.connect(cons.put_export);put, get, and peek
TLM defines a small, fixed vocabulary of transport methods, so any two components speak the same language:
put(t)— hand a transaction out to the other side.get(t)— take a transaction in, removing it from the channel.peek(t)— read the next transaction without consuming it, so a laterget()still returns the same item.
peek is useful when a component must inspect the head of a stream to decide what to do, while leaving the item in place.
my_txn t;
get_port.peek(t); // read the head item WITHOUT removing it
get_port.get(t); // remove and return the head itemBlocking vs nonblocking
Each transport method comes in two flavours, and the distinction is strict:
- Blocking variants (
put,get,peek) are tasks. They may consume simulation time and suspend the caller until the operation can complete — a blockingputwaits while the channel is full. - Nonblocking variants (
try_put,try_get,try_peek, pluscan_put/can_get) are functions. They return in zero time with a boolean telling you whether the operation succeeded.
Choose blocking when the caller should stall until the channel is ready; choose nonblocking when it must stay responsive and handle a “not now” answer itself.
// Blocking: a TASK. May consume time; suspends until it completes.
task send(my_txn t);
put_port.put(t); // waits here while the channel is full
endtask
// Nonblocking: a FUNCTION. Returns immediately with success or failure.
function void try_send(my_txn t);
if (!put_port.try_put(t)) // returns 0 if the channel cannot accept now
`uvm_info("PROD", "channel full - retry later", UVM_LOW)
endfunctionuvm_tlm_fifo: a channel between components
Ports and imps describe the interface, but something must store items in transit. uvm_tlm_fifo is that buffer: a put_export on one side, a get_export (and peek) on the other, so producer and consumer run independently while the FIFO absorbs the rate mismatch.
class env extends uvm_env;
producer prod;
consumer cons;
uvm_tlm_fifo #(my_txn) fifo;
`uvm_component_utils(env)
function void build_phase(uvm_phase phase);
prod = producer::type_id::create("prod", this);
cons = consumer::type_id::create("cons", this);
fifo = new("fifo", this); // default depth 1; new("fifo", this, 8) => depth 8
endfunction
function void connect_phase(uvm_phase phase);
prod.put_port.connect(fifo.put_export); // producer puts INTO the fifo
cons.get_port.connect(fifo.get_export); // consumer gets FROM the fifo
endfunction
endclassThe FIFO’s depth controls back-pressure. With the default depth of one, a blocking put on a full FIFO suspends the producer until the consumer performs a get; a deeper FIFO lets the producer run ahead. This is how UVM TLM decouples components with no explicit synchronisation code of your own.
analysis_port and analysis_imp
Everything above is point-to-point. Monitoring is different: a monitor observes the bus and many components may care (a scoreboard, a coverage collector, a logger). For that one-to-many broadcast, TLM provides the analysis pair.
An uvm_analysis_port offers a single method, write(t), and it is always nonblocking: calling write invokes write() on every connected subscriber and returns. Each subscriber provides an uvm_analysis_imp and implements its own write(). If nobody is connected, write is a harmless no-op.
class monitor extends uvm_component;
uvm_analysis_port #(my_txn) ap; // broadcasts; does not know who listens
`uvm_component_utils(monitor)
function void build_phase(uvm_phase phase);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
my_txn t;
// ... sample a transaction t off the interface ...
ap.write(t); // nonblocking fan-out to every subscriber
endtask
endclass
class scoreboard extends uvm_component;
uvm_analysis_imp #(my_txn, scoreboard) ap_imp; // subscribes via an imp
`uvm_component_utils(scoreboard)
function void build_phase(uvm_phase phase);
ap_imp = new("ap_imp", this);
endfunction
function void write(my_txn t); // called once per broadcast item
`uvm_info("SB", $sformatf("checking %s", t.convert2string()), UVM_LOW)
endfunction
endclassConnect one analysis_port to as many imps as you like; the fan-out delivers every transaction to every subscriber, in order.
// One analysis_port, many imps -- every subscriber sees every item.
mon.ap.connect(sb.ap_imp);
mon.ap.connect(cov.ap_imp);When a single component must receive several analysis streams, use the uvm_analysis_imp_decl(_suffix) macro to generate distinct imp classes, since a class can implement only one plain write().
See the idea run without UVM
The library above does not run on free tools, but the mechanism does. The example below is plain SystemVerilog — no UVM — modelling a depth-2 FIFO plus an analysis-style broadcast: three puts into a two-slot FIFO (the third is refused, like a full channel), then each drained item fanned out to two subscribers.
put ok : addr=0 data=0 (fifo used=1)
put ok : addr=1 data=16 (fifo used=2)
put FULL : addr=2 data=32 dropped (fifo used=2)
get : addr=0 data=0 (fifo used=1)
[coverage] write(addr=0 data=0)
[checker] write(addr=0 data=0)
get : addr=1 data=16 (fifo used=0)
[coverage] write(addr=1 data=16)
[checker] write(addr=1 data=16)
done: coverage saw 2, checker saw 2
module tb;
// Plain-SystemVerilog model of TLM put/get plus an analysis broadcast.
// No UVM library, so it runs on any SystemVerilog simulator.
class txn;
int addr;
int data;
function new(int a, int d);
addr = a;
data = d;
endfunction
function string to_s();
return $sformatf("addr=%0d data=%0d", addr, data);
endfunction
endclass
// Bounded FIFO: try_put fails when full, try_get returns the oldest item.
class tlm_fifo;
txn q[$];
int depth;
function new(int d);
depth = d;
endfunction
function bit try_put(txn t);
if (q.size() >= depth) return 0;
q.push_back(t);
return 1;
endfunction
function bit try_get(ref txn t);
if (q.size() == 0) return 0;
t = q.pop_front();
return 1;
endfunction
function int used();
return q.size();
endfunction
endclass
// Each subscriber implements write() -- the analysis_imp side.
class subscriber;
string name;
int seen = 0;
function new(string n);
name = n;
endfunction
function void write(txn t);
seen++;
$display(" [%s] write(%s)", name, t.to_s());
endfunction
endclass
// analysis_port: one write() fans out to every connected subscriber.
class analysis_port;
subscriber subs[$];
function void connect(subscriber s);
subs.push_back(s);
endfunction
function void write(txn t);
foreach (subs[i]) subs[i].write(t);
endfunction
endclass
tlm_fifo f;
analysis_port ap;
subscriber cov, chk;
txn t, c;
initial begin
f = new(2);
ap = new();
cov = new("coverage");
chk = new("checker");
ap.connect(cov);
ap.connect(chk);
// Push three items into a FIFO that holds only two.
for (int i = 0; i < 3; i++) begin
t = new(i, i*16);
if (f.try_put(t))
$display("put ok : %s (fifo used=%0d)", t.to_s(), f.used());
else
$display("put FULL : %s dropped (fifo used=%0d)", t.to_s(), f.used());
end
// Drain the FIFO; broadcast each item on the analysis_port.
while (f.try_get(c)) begin
$display("get : %s (fifo used=%0d)", c.to_s(), f.used());
ap.write(c);
end
$display("done: coverage saw %0d, checker saw %0d", cov.seen, chk.seen);
$finish;
end
endmodule
The real uvm_tlm_fifo adds blocking semantics — a full put waits rather than fails — but the shape is identical: a bounded buffer for put/get, and a one-call fan-out for analysis.
Quick check
What is the difference between a TLM port and an imp?
A port requires a method — the component calls it but does not implement it. An imp provides the implementation, binding the method to a local task or function. You always connect port.connect(imp).
How do blocking and nonblocking transport methods differ?
Blocking methods (put/get/peek) are tasks: they can consume time and suspend the caller until the operation completes. Nonblocking methods (try_put/try_get/try_peek) are functions: they return immediately with a boolean indicating success or failure.
When would you use peek instead of get?
Use peek to read the next transaction without removing it from the channel, so a later get still returns the same item. It lets a component inspect the head of a stream while leaving it in place.
Why is analysis_port used for a monitor instead of a put_port?
A monitor broadcasts to many subscribers at once. analysis_port.write() is a nonblocking one-to-many fan-out that calls write() on every connected analysis_imp, so adding a scoreboard or coverage collector never changes the monitor.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses