UVM TLM (Transaction Level Modeling) is the set of ports, exports and implementation ports that components use to pass transactions to each other. Instead of wiring signals, a component calls a method such as put(), get() or write() on a port, and the call is routed to whichever component implements that method. Neither side needs to know the other's type, so components can be swapped and reused freely.
8 min read | Intermediate Level
What You'll Learn
- Explain what ports, exports and imps do and how they connect
- Distinguish the put, get, peek, transport and analysis interfaces
- Choose between blocking and nonblocking TLM methods
- Apply the connection rules across levels of hierarchy
Transactions Instead of Signals
At the pin level, one bus write might involve an address, data, strobes and handshake signals over several clock cycles. At the transaction level, the same write is a single object with an address and a data field. Components above the driver and below the monitor work only with these objects, which makes them faster to simulate and much easier to read and debug.
class simple_packet extends uvm_object;
`uvm_object_utils(simple_packet)
rand bit [7:0] addr;
rand bit [7:0] data;
bit rwb; // 1 = read, 0 = write
constraint c_addr { addr > 8'h2a; }
constraint c_data { data inside {[8'h14:8'he9]}; }
function new(string name = "simple_packet");
super.new(name);
endfunction
endclass
A simple_packet is the kind of object that travels through TLM connections. TLM does not care what is inside it; the port and the receiving method only need to agree on the type.
Ports, Exports and Imps
Every TLM connection is made from three kinds of connector, each shown with its usual symbol.
- Port - placed in the component that initiates the call. The component calls a method on its port, for example
m_put_port.put(pkt). - Imp (implementation port) - placed in the component that implements the method. The component defines the actual
put()task, and the imp routes calls to it. - Export - placed in a parent component to forward calls down to an imp in one of its children, so the parent can expose a child's implementation without implementing anything itself.
The port always sits on the side that makes the call, whichever way the data moves. In a put() the data goes from initiator to target. In a get() the initiator still owns the port, but the data comes back from the target.
TLM Interface Types
Connectors are named after the interface they carry, as uvm_<interface>_port, uvm_<interface>_export and uvm_<interface>_imp.
| Interface | Methods | Purpose |
|---|---|---|
| put | put(), try_put(), can_put() | Send a transaction to the target |
| get | get(), try_get(), can_get() | Take a transaction from the target, consuming it |
| peek | peek(), try_peek(), can_peek() | Look at the next transaction without consuming it |
| get_peek | get and peek methods combined | Both in one connector |
| transport | transport(), nb_transport() | Send a request and receive a response in one call |
| analysis | write() | Broadcast to any number of subscribers |
Each of put, get, peek, get_peek and transport comes in three flavors: blocking_ (only the task), nonblocking_ (only the try_/can_ functions) and the plain name, which offers both. For example, uvm_blocking_put_port, uvm_nonblocking_put_port and uvm_put_port.
Blocking vs Nonblocking
A blocking method is a task and may take simulation time. put() returns only once the target has accepted the transaction, and get() returns only once a transaction is available. A nonblocking method is a function that returns immediately with a status bit: try_put() returns 0 if the target cannot accept the transaction right now, and can_put() just asks without sending anything.
Note what a successful put() promises: the transaction has been delivered to the target, not necessarily processed. A target is free to store it and act on it later.
Quick Check
Q: A monitor must report every transaction it sees to a scoreboard and a coverage collector, without ever waiting for either. Which interface fits?
Show Answer
The analysis interface. write() is a function, so it never blocks, and one analysis port can be connected to any number of subscribers. Put and get connections link exactly one initiator to one target.
Connection Rules
Connections are made in connect_phase by calling connect() on the connector closer to the initiator, passing the one closer to the target.
// Initiator and target are siblings: port to imp
compA.m_put_port.connect(compB.m_put_imp);
// Child to parent on the initiator side: port to port
m_subcomp_A.m_put_port.connect(this.m_put_port);
// Parent to child on the target side: export to export, or export to imp
this.m_put_export.connect(m_subcomp_B.m_put_imp);
- A port can connect to a port (going up the hierarchy), an export or an imp.
- An export can connect only to another export or to an imp (going down the hierarchy).
- An imp cannot be connected to anything. It is bound to its implementing component when it is constructed.
- Both sides must use the same transaction type, and the provider must offer the interface the requester needs. A
uvm_blocking_put_portcan connect to auvm_put_imp, because the imp offers blocking put among others, but not the other way round.
However many levels a connection crosses, UVM resolves the chain just before end_of_elaboration_phase. At run time every call goes straight to the implementation in a single hop. Worked examples of each chain are in UVM TLM Port to Export to Imp.
TLM in a Real Testbench
Most testbenches use three TLM patterns. A driver pulls items from its sequencer through a specialized pull port, seq_item_port. A monitor broadcasts captured transactions through an analysis port to scoreboards and coverage collectors. And a uvm_tlm_fifo buffers transactions between two components that run at different rates. Each of these is covered in its own article in this chapter.
UVM also includes TLM-2.0 sockets, based on the SystemC TLM-2.0 standard, for modeling memory-mapped buses with a generic payload. They are mainly used when connecting to SystemC models. See UVM TLM Sockets.
Common Mistakes
Mistake #1: Connecting in the wrong direction
Wrong: compB.m_put_imp.connect(compA.m_put_port);
Right: compA.m_put_port.connect(compB.m_put_imp);
Why? connect() is always called on the side nearer the initiator. An imp cannot be connected at all, so the reversed call is an error.
Mistake #2: Leaving a port unconnected
A put or get port expects exactly one connection. If connect() is never called, UVM reports an error when it resolves the connections at the end of elaboration, before the run starts. An analysis port is the exception: it may have zero subscribers.
Mistake #3: Mixing transaction types
A uvm_blocking_put_port #(packet) cannot connect to a uvm_blocking_put_imp #(other_packet, ...), even if the classes look similar. The type parameters must match exactly, or the connection fails to compile.
Summary
In this article, you learned:
- TLM passes transaction objects between components through method calls instead of signals
- A port initiates a call, an imp implements it, and an export forwards it to a child
- put, get, peek and transport come in blocking, nonblocking and combined flavors, and analysis broadcasts with
write() - Ports connect up the hierarchy or across, exports connect down, and imps are the end of every chain
- Connections are resolved before
end_of_elaboration_phase, so calls take a single hop at run time