UVM TLM sockets are the TLM-2.0 way of connecting an initiator to a target. A socket bundles the forward and backward paths of a connection into one object, so a single connect() links both directions. Sockets carry transport calls such as b_transport() and are normally used with the generic payload, a standard transaction for memory-mapped buses.
UVM includes TLM-2.0 mainly for interoperability with SystemC models, which use the same interfaces. If your testbench talks to a SystemC virtual platform or reference model, sockets are how the two worlds connect. For purely SystemVerilog testbenches, the TLM-1 ports covered earlier in this chapter are more common.
7 min read | Intermediate Level
What You'll Learn
- Explain what a socket is and how it differs from a TLM-1 port
- Use blocking initiator and target sockets with
b_transport() - Describe the generic payload and the timing annotation argument
- Understand the forward and backward paths of nonblocking transport
Sockets vs Ports
A TLM-1 connection is one-directional: a port calls a method in an imp, and any reply has to come back through a second connection. A socket is derived from the same uvm_port_base class, but it represents both directions at once. An initiator socket can only connect to a target socket, and a target socket only to an initiator socket, apart from passthrough sockets, which forward a connection through an intermediate level of hierarchy just as exports do.
| Socket | Used by | Must implement |
|---|---|---|
uvm_tlm_b_initiator_socket #(T) | Blocking initiator | Nothing |
uvm_tlm_b_target_socket #(IMP, T) | Blocking target | b_transport() |
uvm_tlm_nb_initiator_socket #(IMP, T, P) | Nonblocking initiator | nb_transport_bw() |
uvm_tlm_nb_target_socket #(IMP, T, P) | Nonblocking target | nb_transport_fw() |
T is the transaction type and defaults to uvm_tlm_generic_payload. IMP is the class that implements the required method, and P is the phase type used by nonblocking transport.
Syntax: Blocking Transport
uvm_tlm_b_initiator_socket #(T) init_socket; // in the initiator
uvm_tlm_b_target_socket #(IMP, T) tgt_socket; // in the target
virtual task b_transport(T t, uvm_tlm_time delay); // the target implements this
One call to b_transport() carries a complete transaction. When it returns, the transaction is finished, and any response, such as read data or a status, has been written into the same object by the target.
The Timing Annotation
The delay argument is a uvm_tlm_time object. It lets the target say "this transaction would have taken this long" without actually waiting. A fast, loosely timed model can then run far ahead of real simulation time, which is the main reason TLM-2.0 models simulate quickly. The target may also choose to consume real time inside b_transport(), since it is a task.
Example: Initiator and Target
The initiator sends five randomized packets through a blocking initiator socket.
class initiator extends uvm_component;
`uvm_component_utils(initiator)
uvm_tlm_b_initiator_socket #(simple_packet) initSocket;
uvm_tlm_time delay;
function new(string name = "initiator", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
initSocket = new("initSocket", this);
delay = new();
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this); // keep the phase alive while sending
repeat (5) begin
simple_packet pkt = simple_packet::type_id::create("pkt");
assert(pkt.randomize());
`uvm_info("INIT", "Packet sent to target", UVM_LOW)
pkt.print(uvm_default_line_printer);
initSocket.b_transport(pkt, delay); // returns when the target is done
end
phase.drop_objection(this);
endtask
endclass
The target declares a blocking target socket and implements b_transport().
class target extends uvm_component;
`uvm_component_utils(target)
uvm_tlm_b_target_socket #(target, simple_packet) targetSocket; // IMP first, then T
function new(string name = "target", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
targetSocket = new("targetSocket", this);
endfunction
task b_transport(simple_packet pkt, uvm_tlm_time delay);
`uvm_info("TGT", "Packet received from Initiator", UVM_MEDIUM)
pkt.print(uvm_default_line_printer);
endtask
endclass
The environment connects the two sockets, always calling connect() on the initiator side.
class my_env extends uvm_env;
`uvm_component_utils(my_env)
initiator init;
target tgt;
function new(string name = "my_env", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
init = initiator::type_id::create("init", this);
tgt = target::type_id::create("tgt", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
init.initSocket.connect(tgt.targetSocket);
endfunction
endclass
Quick Check
Q: In uvm_tlm_b_target_socket #(target, simple_packet), what does the first parameter mean?
Show Answer
It is the class that implements b_transport(), here the target component itself. The socket forwards every incoming call to that object's method, in the same way an imp's second parameter does in TLM-1.
The Generic Payload
The example uses a custom packet to keep it short, but sockets are normally used with uvm_tlm_generic_payload, a uvm_sequence_item designed for memory-mapped buses. It has fields for the command, address, data, data length, byte enables and response status, with accessor methods for each.
uvm_tlm_generic_payload gp = uvm_tlm_generic_payload::type_id::create("gp");
byte unsigned wdata[] = '{8'hAB, 8'hCD};
gp.set_command(UVM_TLM_WRITE_COMMAND);
gp.set_address(64'h1000);
gp.set_data(wdata);
gp.set_data_length(2);
initSocket.b_transport(gp, delay);
if (!gp.is_response_ok())
`uvm_error("INIT", gp.get_response_string())
The target reads the command and address, performs the access, and sets the response status, for example UVM_TLM_OK_RESPONSE or UVM_TLM_ADDRESS_ERROR_RESPONSE. Because every TLM-2.0 model agrees on this one transaction type, models from different teams and languages can be connected without adapters.
Nonblocking Transport
Nonblocking sockets split a transaction into phases: BEGIN_REQ, END_REQ, BEGIN_RESP and END_RESP. The initiator calls nb_transport_fw() on the forward path, and the target answers later through nb_transport_bw() on the backward path, which the initiator must implement. Every call is a function that returns immediately with UVM_TLM_ACCEPTED, UVM_TLM_UPDATED or UVM_TLM_COMPLETED. This allows several transactions to be in flight at once, which matters for approximately timed models of pipelined buses, but it is considerably more complex than b_transport().
Sockets Across Hierarchy
When the initiator or target sits inside another component, the parent uses a passthrough socket to forward the connection, in the same way TLM-1 uses ports and exports. On the initiator side, the child's initiator socket connects to the parent's uvm_tlm_b_passthrough_initiator_socket. On the target side, the parent's uvm_tlm_b_passthrough_target_socket connects down to the child's target socket. Nonblocking versions of both exist as well.
// Inside an agent that contains the real initiator
uvm_tlm_b_passthrough_initiator_socket #(simple_packet) agent_socket;
virtual function void connect_phase(uvm_phase phase);
m_init.initSocket.connect(agent_socket); // child socket up to the parent
endfunction
When to Use Sockets
Choose sockets when a model has to plug into a TLM-2.0 world: a SystemC virtual platform, a vendor's TLM-2.0 bus model, or a reference model written against the generic payload. Using the standard interfaces and transaction means no adapter code is needed at the boundary.
Inside a SystemVerilog-only testbench, TLM-1 ports, analysis ports and FIFOs remain the usual choice. They are simpler, every UVM component already uses them, and the sequencer-driver connection is built on them. Many testbenches therefore use sockets only at the edge where they meet a SystemC model, and TLM-1 everywhere else. The rest of this chapter's articles, starting with UVM TLM, cover the TLM-1 side.
Common Mistakes
Mistake #1: Swapping the target socket parameters
Wrong: uvm_tlm_b_target_socket #(simple_packet, target)
Right: uvm_tlm_b_target_socket #(target, simple_packet)
Why? The implementing class comes first and the transaction type second, the opposite order from TLM-1 imps.
Mistake #2: Forgetting the objection in the initiator
Without an objection, run_phase can end before the initiator has sent anything. The original version of this example had no objection; the initiator above raises one around its loop.
Mistake #3: Not implementing the backward path
A nonblocking initiator socket requires its component to implement nb_transport_bw(), even if the target never calls it. Leaving it out fails to compile.
Summary
In this article, you learned:
- A socket bundles the forward and backward paths of a TLM-2.0 connection, and initiator sockets connect to target sockets
- Blocking sockets carry a whole transaction in one
b_transport()call implemented by the target - The
uvm_tlm_timedelay lets a model annotate timing without consuming simulation time uvm_tlm_generic_payloadis the standard transaction for memory-mapped buses- Nonblocking transport uses forward and backward function calls with phases, so transactions can overlap