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.

SocketUsed byMust implement
uvm_tlm_b_initiator_socket #(T)Blocking initiatorNothing
uvm_tlm_b_target_socket #(IMP, T)Blocking targetb_transport()
uvm_tlm_nb_initiator_socket #(IMP, T, P)Nonblocking initiatornb_transport_bw()
uvm_tlm_nb_target_socket #(IMP, T, P)Nonblocking targetnb_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_time delay lets a model annotate timing without consuming simulation time
  • uvm_tlm_generic_payload is the standard transaction for memory-mapped buses
  • Nonblocking transport uses forward and backward function calls with phases, so transactions can overlap
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5