TLM Sockets & Multiple Imps
Two loose ends remain in the TLM picture built up across this section: a more compact alternative to declaring a port and export separately, and what to do when one component genuinely needs more than one imp of the same transaction type — something none of the patterns so far actually support.
The problem: two imps, same type, one component
Every analysis imp so far has had a unique host component — fifo_scoreboard implements write(fifo_txn) once, and that's unambiguous. Suppose the testbench grows to compare two parallel FIFO instances for redundancy, with two independent monitors, and one scoreboard needs to receive fifo_txn from both:
// This does NOT work — a component can only have one write(fifo_txn) method,
// so a second uvm_analysis_imp #(fifo_txn, ...) has nothing distinct to call
class fifo_dual_scoreboard extends uvm_scoreboard;
uvm_analysis_imp #(fifo_txn, fifo_dual_scoreboard) ap_imp_a; // ambiguous
uvm_analysis_imp #(fifo_txn, fifo_dual_scoreboard) ap_imp_b; // with which write()?
endclass
Both imps would need to call the same write(fifo_txn) method — UVM has no way to tell them apart, since the imp-to-method binding is by type, not by which imp instance triggered it.
The _decl macro: declaring a distinctly-named imp
// Generates a new class, uvm_analysis_imp_a, with its own write_a() callback
`uvm_analysis_imp_decl(_a)
// Generates uvm_analysis_imp_b, calling write_b()
`uvm_analysis_imp_decl(_b)
class fifo_dual_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_dual_scoreboard)
uvm_analysis_imp_a #(fifo_txn, fifo_dual_scoreboard) ap_imp_a;
uvm_analysis_imp_b #(fifo_txn, fifo_dual_scoreboard) ap_imp_b;
function new(string name, uvm_component parent);
super.new(name, parent);
ap_imp_a = new("ap_imp_a", this);
ap_imp_b = new("ap_imp_b", this);
endfunction
function void write_a(fifo_txn txn); // called only via ap_imp_a
// ... handle traffic from monitor A ...
endfunction
function void write_b(fifo_txn txn); // called only via ap_imp_b
// ... handle traffic from monitor B ...
endfunction
endclass
Three details make this work correctly, and all three have to agree:
`uvm_analysis_imp_decl(_a)is invoked outside the class, before it's used — it generates a whole new class (uvm_analysis_imp_a) at that point, which the component below then instantiates like any other imp type.- The suffix (
_a,_b— any consistent suffix works, not just letters) has to match in three places: the generated class name (uvm_analysis_imp_a), the declared imp instance's type, and the callback method name (write_a). Mismatching any one of the three is a compile error, not a silent bug. - Each monitor connects to its own specific imp —
monitor_a.ap.connect(scoreboard.ap_imp_a),monitor_b.ap.connect(scoreboard.ap_imp_b)— sameconnect_phasepattern as every other TLM wiring in this section, just with two distinct destinations instead of one.
This is a genuinely uncommon-enough need that most UVM primers skip it entirely, but it's worth recognizing on sight — a component receiving from two same-typed sources without it is either silently wrong or has resorted to an awkward workaround (wrapping one side's data in a different type just to disambiguate).
TLM sockets: port and export, fused
Everything in this section has kept ports and exports as separate declarations. TLM sockets — uvm_tlm_b_initiator_socket and uvm_tlm_b_target_socket — collapse that distinction into one object per side, at the cost of adopting a slightly different API shape (b_transport() instead of separate put/get calls):
class fifo_alt_driver extends uvm_component;
uvm_tlm_b_initiator_socket #(fifo_txn) sock;
// ...
endclass
class fifo_alt_target extends uvm_component;
uvm_tlm_b_target_socket #(fifo_txn) sock;
task b_transport(fifo_txn txn, uvm_tlm_time delay);
// ... handle txn, optionally annotate timing via delay ...
endtask
endclass
Initiator sockets only ever connect to target sockets — never to another initiator, and never through an intermediate export — which is what "fused" actually buys: one fewer kind of connection mismatch to get wrong, in exchange for a narrower, more specialized API than the put/get/analysis family covers. Sockets show up far less often than the rest of this section in typical block-level UVM testbenches like the FIFO example running through it; recognizing the two class names and the b_transport() shape is usually enough to read code that uses them, without needing to reach for sockets by default.
b_transport()'s second argument, uvm_tlm_time delay, is worth naming even at this recognize-on-sight level of depth: it's a dedicated timing-annotation type, not a plain SystemVerilog time literal, specifically because it can carry a delay value across two components running at different simulation timescales/precisions — a real concern once a socket connects VIPs that weren't necessarily built assuming the same time unit. It also lets an initiator push the realization of a delay downstream rather than actually blocking in simulation for it, which is one of the reasons TLM2 sockets can model timing without paying for it in raw simulation wall-clock time the way an equivalent #delay would.
Non-blocking transport sockets: nb_transport_fw()/nb_transport_bw()
b_transport() is a single call that only returns once the entire transaction is complete — fine for a simple request/response, but it gives the initiator no visibility into intermediate timing points along the way. UVM also defines non-blocking transport sockets, using a pair of methods instead of one:
nb_transport_fw()— sent from initiator to target, carrying the transaction forward.nb_transport_bw()— sent from target back to initiator, carrying a response (or an intermediate status) backward.
Each call is tagged with a phase (e.g. BEGIN_REQ, END_REQ, BEGIN_RESP, END_RESP) marking where in the transaction's lifetime that particular call falls, letting both sides observe and react to intermediate timing points that a single blocking b_transport() call collapses into "started" and "finished," with nothing in between. This is more machinery than most block-level testbenches need — the same "recognize it, don't necessarily reach for it" relationship this page already has with sockets generally.
What's next
Every mechanism in this section — analysis ports, blocking and non-blocking put/get, the TLM FIFO, multiple imps, and sockets — governs how already-built components talk to each other. The next section goes back to how stimulus itself gets generated, picking up where The Sequencer & Sequence left off with the `uvm_do macro family and coordinating sequences across multiple agents at once.