The virtual-platform team ships a TLM-2.0 model of the DMA's register block, and your job is to drive it from the UVM bench so the same register sequences run against the model and the RTL. Their model exposes a target socket. You add an initiator socket, connect it, and the first attempt fails with a Connection Error saying the provider does not provide the complete interface. You used a blocking socket; their target is non-blocking. What does a socket actually bundle, and which pairs fit together?
In TLM 1 a request-response link with callbacks needs a port one way and an export or imp the other way, and two connect() calls. A TLM 2.0 socket bundles them. An initiator socket is the end that starts transfers; a target socket is the end that services them. For blocking transport there is only a forward path, so the initiator socket is essentially a port and the target socket an imp. For non-blocking transport there is also a backward path, target to initiator, so each socket contains both: the initiator socket is a forward port plus a backward imp, the target socket a forward imp plus a backward port. One connect() binds both.
Compare a two-way radio handset with separate microphone and speaker cables. The handset is one plug; you cannot plug the microphone into the wrong socket because there is no separate microphone plug.
Class | Owner | Calls | Implements |
|---|---|---|---|
| Initiator |
| nothing |
| Target | nothing |
|
| Initiator |
|
|
| Target |
|
|
| Interconnect or wrapper | forward the same calls | nothing |
T defaults to uvm_tlm_generic_payload and P to the phase enum uvm_tlm_phase_e, so uvm_tlm_b_initiator_socket #() is the common declaration. The IMP parameter plays the same role as in a TLM 1 imp: the class whose method the socket calls. If you omit the constructor's third argument, the socket casts its parent to IMP.
These are from a small TLM 2.0 test added to the bench: an initiator component that issues transfers and a memory model that services them.
// snippet: from tlm2_gp_test in tlm_tb.sv
class gp_init extends uvm_component;
`uvm_component_utils(gp_init)
uvm_tlm_b_initiator_socket #() isock; // T = uvm_tlm_generic_payload
function new(string name, uvm_component parent);
super.new(name, parent);
isock = new("isock", this);
endfunction
// xfer() builds a payload and calls isock.b_transport(gp, delay)
endclass
class gp_mem extends uvm_component;
`uvm_component_utils(gp_mem)
uvm_tlm_b_target_socket #(gp_mem) tsock; // IMP = gp_mem
byte unsigned mem[bit [63:0]];
function new(string name, uvm_component parent);
super.new(name, parent);
tsock = new("tsock", this);
endfunction
task b_transport(uvm_tlm_generic_payload gp, uvm_tlm_time delay);
// decode, check, execute, set the response status
endtask
endclass
// test connect_phase: the initiator side calls connect()
ini.isock.connect(mem.tsock);What the target does inside b_transport, and why the initiator waits after the call, are the next lesson. The payload's fields are their own lesson.
As in TLM 1, connect() is called on the initiator's end: initiator socket to passthrough or target socket, passthrough initiator up to a parent's passthrough initiator, passthrough target down to a child's target socket.
Calling connect() on a target socket is an error: You cannot call connect() on a target termination socket.
An initiator socket binds to exactly one target path (minimum and maximum 1), so an unconnected initiator socket is an error at end of elaboration, like any non-analysis port.
Blocking and non-blocking sockets do not mix. A blocking initiator into a non-blocking target fails at connect() with does not provide the complete interface, followed by type mismatch in connect.
To run the bench's existing register sequences against the model, you do not need new sequences. You need a driver that, instead of wiggling APB pins, turns each apb_item into a generic payload and calls b_transport. The sequencer, the sequences and the item_done() handshake stay exactly as they are; only the last hop changes.
// snippet
class apb2tlm_driver extends uvm_driver #(apb_item);
`uvm_component_utils(apb2tlm_driver)
uvm_tlm_b_initiator_socket #() isock;
function new(string name, uvm_component parent);
super.new(name, parent);
isock = new("isock", this);
endfunction
task run_phase(uvm_phase phase);
uvm_tlm_time delay = new("delay");
forever begin
uvm_tlm_generic_payload gp = uvm_tlm_generic_payload::type_id::create("gp");
byte unsigned data[] = new[4];
seq_item_port.get_next_item(req);
{data[3], data[2], data[1], data[0]} = req.data; // little-endian bytes
gp.set_command(req.write ? UVM_TLM_WRITE_COMMAND : UVM_TLM_READ_COMMAND);
gp.set_address(req.addr);
gp.set_data(data);
gp.set_data_length(4);
gp.set_byte_enable_length(0);
gp.set_streaming_width(4);
gp.set_response_status(UVM_TLM_INCOMPLETE_RESPONSE);
delay.reset();
isock.b_transport(gp, delay);
#(delay.get_realtime(1ns));
gp.get_data(data);
req.rdata = {data[3], data[2], data[1], data[0]};
req.slverr = !gp.is_response_ok();
seq_item_port.item_done(); // read data rides back in req
end
endtask
endclassThis is the same pattern the register layer uses in reverse: uvm_reg_tlm_adapter converts register operations into generic payloads. Swapping apb_driver for apb2tlm_driver with a factory override points every existing test at the model.
Under the sockets, UVM also provides the plain TLM 2.0 ports, exports and imps (uvm_tlm_b_transport_port, uvm_tlm_nb_transport_fw_imp, and so on). Sockets are the recommended interface; the separate pieces exist mainly for building custom sockets.
With the sockets bound, the initiator wrote four bytes at address 0x10. The target added 10 ns of delay and the initiator waited it out before logging:
UVM_INFO tlm_tb.sv(502) @ 10000: uvm_test_top.ini [GP] UVM_TLM_WRITE_COMMAND [0x0000000000000010] = 11 22 33 44 (status=OK) -> OKThe message is the payload's own convert2string(): command, 64-bit address, data bytes and response status. One call, one complete transfer.
Back to the opening. Both sockets used the generic payload, so the types matched, and the code compiled. But the model's target socket was uvm_tlm_nb_target_socket, which implements nb_transport_fw and expects a backward path; the bench's initiator socket was blocking and could only call b_transport. connect() compares the interface masks, finds the target cannot answer a b_transport, and refuses the link at time 0. The fix was not to change the model: the team put a small adapter component between them, with a blocking target socket facing the bench and a non-blocking initiator socket facing the model, that converts each b_transport into the phase sequence of the non-blocking lesson. That adapter is also the place to bridge timing styles.
IMP, the class that implements the transport method. The target socket must call b_transport or nb_transport_fw on that class, exactly like a TLM 1 imp calls write() on its component. A blocking initiator socket implements nothing, so it needs no IMP.
A passthrough target socket, uvm_tlm_b_passthrough_target_socket. The wrapper connects it down to the model's target socket in its connect_phase, and the env connects the initiator socket to the wrapper's passthrough socket, mirroring a TLM 1 export forwarding to a child imp.
apb2tlm_driver, but nothing connects its socket. What happens, and how should the env avoid it?The run stops at time 0: an initiator socket needs exactly one connection, so resolve_bindings reports connection count of 0 does not meet required minimum of 1 and the root raises BUILDERR. The env should build the TLM path only when its configuration selects the model: read a knob such as cfg.use_tlm_model in build_phase and, only when it is set, create the model and apply the apb_driver to apb2tlm_driver override; connect the socket in connect_phase under the same condition. That is the same rule as for any optional TLM 1 port: build it only where you will connect it.
Next: the socket carries a call, but who accounts for the time it takes? Blocking transport shows the delay argument and the two styles of using it.