UVM TLM Concepts:
In short: UVM TLM lets components talk by passing whole transactions through ports instead of wiggling signals. One component owns a port (the caller), another owns an imp that holds the real method, and connect() joins them in the environment. A uvm_analysis_port is the one-to-many version used to broadcast, for example from a monitor to a scoreboard and coverage at once. A uvm_tlm_fifo buffers items so a producer and consumer can run at their own pace.
Inside a UVM testbench, components need to hand transactions to each other: a monitor gives observed items to a scoreboard, a sequence hands items toward a driver, a producer feeds a consumer. Doing that with raw signals or shared handles gets tangled fast. TLM, transaction-level modelling, gives a small, standard set of ports so any two components connect the same way. This guide explains ports, exports, and imps, the put and get direction, the analysis port for broadcasting, the TLM FIFO for buffering, and how hierarchical connections work, each with runnable code.
A simple way to picture it
Think of a mailbox between two neighbours. One neighbour drops a letter in the slot without knowing or caring who picks it up or when. The other neighbour opens the box and takes the letter. The slot is a fixed shape, so any letter fits, and neither neighbour has to touch the other. A TLM connection is that mailbox: the producer calls a method to hand over a transaction, the consumer provides the real method that receives it, and the two never reach into each other. Swap either neighbour and nothing else changes, because the slot stays the same.
Port, export, and imp
Three names do most of the work, and the difference is only about who holds the real method. A port is the calling end: it declares the methods it wants to call but has no implementation. An imp (short for implementation) holds the actual method body. An export forwards a port or imp up through the hierarchy so a parent can expose a child connection. In the simplest one-to-one link you connect a port directly to an imp.
| Term | Who owns it | Holds the method? | Role |
|---|---|---|---|
| port | The caller (producer) | No | Calls put/get/write on whatever it connects to |
| imp | The receiver (consumer) | Yes | Provides the real method body |
| export | A parent component | No, it forwards | Passes a child port or imp up the hierarchy |
A worked put example: producer to consumer
Here is the smallest complete link. The producer owns a uvm_blocking_put_port and calls put(). The consumer owns a uvm_blocking_put_imp and defines the real put() method. The environment connects the port to the imp, and after that the producer can hand items across without knowing anything about the consumer.
// the transaction that travels across the link
class packet extends uvm_sequence_item;
rand bit [7:0] addr;
`uvm_object_utils(packet)
function new(string name="packet"); super.new(name); endfunction
endclass
// PRODUCER: owns the port and calls put()
class producer extends uvm_component;
`uvm_component_utils(producer)
uvm_blocking_put_port #(packet) put_port;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_port = new("put_port", this);
endfunction
task run_phase(uvm_phase phase);
packet tr = packet::type_id::create("tr");
assert(tr.randomize());
put_port.put(tr); // hand the item across the link
endtask
endclass// CONSUMER: owns the imp and provides the real put()
class consumer extends uvm_component;
`uvm_component_utils(consumer)
uvm_blocking_put_imp #(packet, consumer) put_imp;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
put_imp = new("put_imp", this);
endfunction
// the imp requires a method named put()
task put(packet tr);
`uvm_info("CONS", $sformatf("got addr=%0h", tr.addr), UVM_LOW)
endtask
endclass// ENVIRONMENT: connect the port to the imp
class my_env extends uvm_env;
`uvm_component_utils(my_env)
producer prod;
consumer cons;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_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); // port -> imp
endfunction
endclassThe rule to remember: the caller side is always the argument-free end that calls the method, and the imp side always defines a method of the matching name (put, get, or write). Connect the port to the imp, never the other way around.
Blocking versus non-blocking
Every direction comes in two timing styles. A blocking method, such as put on a uvm_blocking_put_port, waits until the other side is ready, so it can consume simulation time. A non-blocking method, such as try_put, returns straight away with a status bit and never waits. Use blocking when it is fine to pause until the transfer happens, and non-blocking when the caller must keep going regardless.
| Style | Example method | Waits? | Use when |
|---|---|---|---|
| Blocking | put, get, peek | Yes, until ready | It is fine to pause for the transfer |
| Non-blocking | try_put, can_put | No, returns a status | The caller must not stall |
| Combined | get_peek and put together | Depends on method | You need both styles on one connection |
The families of TLM port
Beyond put, TLM names a small family by direction. Get pulls an item from the other side, peek looks without removing it, and the bidirectional transport does a request and response in one call. The naming is regular, so once you read one you can read them all.
| Port family | Direction | Typical use |
|---|---|---|
| uvm_*_put_* | Caller pushes an item out | Producer sends to a consumer |
| uvm_*_get_* | Caller pulls an item in | Consumer requests the next item |
| uvm_*_peek_* | Caller looks without removing | Inspect the next item before taking it |
| uvm_*_transport_* | Request then response | A memory model that returns read data |
| uvm_analysis_* | Broadcast to many | Monitor feeds scoreboard and coverage |
The wildcards read as follows: the first slot is the timing (blocking, nonblocking, or combined) and the last slot is the kind (port, export, or imp). So uvm_blocking_get_port is a blocking get on the calling side.
The analysis port: one to many
A plain put or get link is one-to-one. The uvm_analysis_port is different: it is one-to-many. A component calls write() once, and every subscriber connected to that port receives a copy. This is exactly how a monitor feeds both a scoreboard and a coverage collector from a single broadcast, and it is the most common TLM connection in a real testbench.
// MONITOR broadcasts every observed transaction
class bus_monitor extends uvm_monitor;
`uvm_component_utils(bus_monitor)
uvm_analysis_port #(packet) ap;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
packet tr = packet::type_id::create("tr");
// ... sample pins into tr ...
ap.write(tr); // one call reaches every subscriber
endtask
endclass
// SCOREBOARD receives via an analysis imp
class scoreboard extends uvm_scoreboard;
`uvm_component_utils(scoreboard)
uvm_analysis_imp #(packet, scoreboard) analysis_export;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
analysis_export = new("analysis_export", this);
endfunction
function void write(packet tr); // analysis imp needs write()
`uvm_info("SB", $sformatf("checking addr=%0h", tr.addr), UVM_HIGH)
endfunction
endclassTo add coverage you connect a second subscriber to the same ap. The monitor does not change at all, which is the strength of the broadcast: new listeners are added at the connection, not in the source. See this pattern inside a full testbench in the UVM testbench architecture guide.
The TLM FIFO: let producer and consumer run apart
Sometimes a producer and consumer should not move in lockstep. A uvm_tlm_fifo sits between them as a buffer: the producer puts items into the FIFO and the consumer gets them when ready, so neither has to wait on the other beyond the buffer depth. The FIFO exposes a put_export and a get_export, and you connect each side to the matching export.
class my_env extends uvm_env;
`uvm_component_utils(my_env)
producer prod;
consumer cons;
uvm_tlm_fifo #(packet) fifo;
function new(string n, uvm_component p); super.new(n,p); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
prod = producer::type_id::create("prod", this);
cons = consumer::type_id::create("cons", this);
fifo = new("fifo", this); // a component, built with new
endfunction
function void connect_phase(uvm_phase phase);
prod.put_port.connect(fifo.put_export); // producer -> fifo
cons.get_port.connect(fifo.get_export); // fifo -> consumer
endfunction
endclassNow the producer never blocks until the FIFO is full, and the consumer pulls at its own rate. This decoupling is useful when the two run at different speeds, for example a fast generator feeding a slower model.
Hierarchical connections
Ports do not always connect to a sibling. Often a child component owns a port and the parent needs to expose it, or a value has to travel up and down the tree. The rule keeps it simple: to pass a child port up to the parent, the parent owns an export and connects it to the child port; the parent export then connects outward like any other port. In short, connect from the calling side toward the implementing side, following the hierarchy.
| Connection | What you write | Direction of connect() |
|---|---|---|
| Port to imp (siblings) | a.port.connect(b.imp) | Caller to implementer |
| Child port to parent export | parent.exp.connect(child.port) | Up the hierarchy |
| Parent export to child imp | parent.exp.connect(child.imp) | Down the hierarchy |
| Analysis port to subscriber | mon.ap.connect(sub.analysis_export) | One to many |
Common mistakes to avoid
- Connecting the imp to the port instead of the port to the imp. Always call
connect()on the calling side with the implementing side as the argument. - Forgetting to build the port, imp, or FIFO in
build_phasewithnew(), which leaves a null handle at connect time. - Giving an analysis imp no
write()method, or a put imp noput()method. The imp requires a method of the matching name. - Using a one-to-one get link when you need a broadcast. Reach for
uvm_analysis_portwhen more than one component must receive the item. - Expecting a non-blocking
try_putto wait. It returns a status immediately; check the return value. - Leaving a producer and consumer in lockstep when a
uvm_tlm_fifowould let them run at their own pace.
Keep learning
TLM is how components pass transactions, so see it working end to end in the UVM testbench architecture guide, and see the driver side of a transaction handoff in sequencer and driver communication. The analysis port pairs with checking, covered in UVM reporting and macros. More topics live on the UVM category page, and TLM comes up often in interview questions.
Frequently asked questions
What is TLM in UVM?
TLM, transaction-level modelling, is a set of standard ports that let components pass whole transactions to each other instead of sharing signals or handles. It keeps components loosely joined, so any two connect the same way and either side can be swapped without touching the other.
What is the difference between a port, an export, and an imp?
A port is the calling end and holds no method. An imp holds the real method body on the receiving side. An export forwards a port or imp up through the hierarchy so a parent can expose a child connection. In a one-to-one link you connect a port to an imp.
What is a uvm_analysis_port used for?
It is the one-to-many connection. A component calls write() once and every connected subscriber gets a copy, which is how a monitor feeds a scoreboard and a coverage collector at the same time without changing the monitor.
When should I use a uvm_tlm_fifo?
Use it when a producer and consumer should run at their own pace rather than in lockstep. The FIFO buffers items, so the producer puts and the consumer gets independently up to the buffer depth. It is handy when the two sides run at different speeds.
What is the difference between blocking and non-blocking ports?
A blocking method such as put waits until the other side is ready and can consume simulation time. A non-blocking method such as try_put returns immediately with a status bit and never waits. Choose blocking when pausing is fine and non-blocking when the caller must keep going.
Which way do I call connect()?
Always call connect() on the calling side with the implementing side as the argument, for example producer.put_port.connect(consumer.put_imp). For an analysis link it is monitor.ap.connect(subscriber.analysis_export).
