UVM TLM ports, exports and imps can be chained to send transactions across several levels of the testbench hierarchy. A port carries a call up and out of the component that initiates it, an export carries it down into the component that contains the target, and an imp at the end of the chain hands it to the method that implements it. This article walks through the four chain shapes you will meet in practice, each with a working example.
Chaining matters because real testbenches nest components. The component that generates transactions is often buried inside an agent, and the one that consumes them is buried inside another, so the connection has to climb out of one and into the other without either inner component knowing where the other one lives.
8 min read | Intermediate Level
What You'll Learn
- Apply the direction rules for port-to-port, port-to-export and export-to-imp connections
- Forward a transaction out of a subcomponent through a parent's port
- Forward a transaction into a subcomponent through a parent's export
- Read a topology printout to confirm how a chain is built
The Connection Rules
- Port to port - going up on the initiator side. The child connects its port to the parent's port:
child.port.connect(this.port). - Port to export, or port to imp - going across between siblings, made by their common parent.
- Export to export, or export to imp - going down on the target side. The parent connects its export to the child's export or imp:
this.export.connect(child.imp). - Every chain ends at exactly one imp, in the component that implements the method.
In every case connect() is called on the connector nearer the initiator. The four chains below are all built from these steps.
All examples send this packet class with a blocking put.
class Packet extends uvm_object;
rand bit[7:0] addr;
rand bit[7:0] data;
`uvm_object_utils_begin(Packet)
`uvm_field_int(addr, UVM_ALL_ON)
`uvm_field_int(data, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "Packet");
super.new(name);
endfunction
endclass
1. Port to Port to Export to Imp
subCompA inside componentA sends to subCompB inside componentB. The transaction goes up through componentA's port, across to componentB's export, and down to subCompB's imp.
class subCompA extends uvm_component;
`uvm_component_utils(subCompA)
uvm_blocking_put_port #(Packet) m_put_port;
int m_num_tx = 2;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_put_port = new("m_put_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
repeat (m_num_tx) begin
Packet pkt = Packet::type_id::create("pkt");
assert(pkt.randomize());
`uvm_info("SUBCOMPA", "Packet sent to subCompB", UVM_LOW)
pkt.print(uvm_default_line_printer);
m_put_port.put(pkt);
end
phase.drop_objection(this);
endtask
endclass
class componentA extends uvm_component;
`uvm_component_utils(componentA)
subCompA m_subcomp_A;
uvm_blocking_put_port #(Packet) m_put_port; // carries subCompA's calls out
function new(string name = "componentA", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_subcomp_A = subCompA::type_id::create("m_subcomp_A", this);
m_put_port = new("m_put_port", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
m_subcomp_A.m_put_port.connect(this.m_put_port); // port to port: child to parent
endfunction
endclass
class componentB extends uvm_component;
`uvm_component_utils(componentB)
subCompB m_subcomp_B;
uvm_blocking_put_export #(Packet) m_put_export; // carries calls in to subCompB
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_subcomp_B = subCompB::type_id::create("m_subcomp_B", this);
m_put_export = new("m_put_export", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
m_put_export.connect(m_subcomp_B.m_put_imp); // export to imp: parent to child
endfunction
endclass
class subCompB extends uvm_component;
`uvm_component_utils(subCompB)
uvm_blocking_put_imp #(Packet, subCompB) m_put_imp;
function new(string name = "subCompB", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_put_imp = new("m_put_imp", this);
endfunction
virtual task put(Packet pkt);
`uvm_info("SUBCOMPB", "Packet received from subCompA", UVM_LOW)
pkt.print(uvm_default_line_printer);
endtask
endclass
class my_test extends uvm_test;
`uvm_component_utils(my_test)
componentA compA;
componentB compB;
function new(string name = "my_test", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
compA = componentA::type_id::create("compA", this);
compB = componentB::type_id::create("compB", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
compA.m_put_port.connect(compB.m_put_export); // port to export: across
endfunction
virtual function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
uvm_top.print_topology();
endfunction
endclass
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
------------------------------------------------------
Name Type Size Value
------------------------------------------------------
uvm_test_top my_test - @1842
compA componentA - @1911
m_put_port uvm_blocking_put_port - @2008
m_subcomp_A subCompA - @1974
m_put_port uvm_blocking_put_port - @2047
compB componentB - @1942
m_put_export uvm_blocking_put_export - @2115
m_subcomp_B subCompB - @2081
m_put_imp uvm_blocking_put_imp - @2152
------------------------------------------------------
UVM_INFO testbench.sv(71) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: (Packet@2191) { addr: 'h24 data: 'h31 }
UVM_INFO testbench.sv(151) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: (Packet@2191) { addr: 'h24 data: 'h31 }
UVM_INFO testbench.sv(71) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: (Packet@2209) { addr: 'h9a data: 'hfb }
UVM_INFO testbench.sv(151) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: (Packet@2209) { addr: 'h9a data: 'hfb }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
The topology shows two ports on the initiator side, one export and one imp on the target side. The same packet object, Packet@2191, arrives at subCompB, even though the call passed through four connectors. UVM resolves the chain before the run starts, so at run time the call goes straight to subCompB's put().
2. Port to Port to Imp
Here componentB is itself the target, so the chain ends at an imp in componentB instead of an export. componentA, subCompA and the test are unchanged, except that subCompA's message now says "CompB".
class componentB extends uvm_component;
`uvm_component_utils(componentB)
uvm_blocking_put_imp #(Packet, componentB) m_put_imp; // componentB implements put()
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_put_imp = new("m_put_imp", this);
endfunction
virtual task put(Packet pkt);
`uvm_info("COMPB", "Packet received from subCompA", UVM_LOW)
pkt.print(uvm_default_line_printer);
endtask
endclass
// In the test: compA.m_put_port.connect(compB.m_put_imp);
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
----------------------------------------------------
Name Type Size Value
----------------------------------------------------
uvm_test_top my_test - @1839
compA componentA - @1908
m_put_port uvm_blocking_put_port - @2005
m_subcomp_A subCompA - @1971
m_put_port uvm_blocking_put_port - @2044
compB componentB - @1939
m_put_imp uvm_blocking_put_imp - @2081
----------------------------------------------------
UVM_INFO testbench.sv(70) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to CompB
pkt: (Packet@2119) { addr: 'h24 data: 'h31 }
UVM_INFO testbench.sv(124) @ 0: uvm_test_top.compB [COMPB] Packet received from subCompA
pkt: (Packet@2119) { addr: 'h24 data: 'h31 }
UVM_INFO testbench.sv(70) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to CompB
pkt: (Packet@2133) { addr: 'h9a data: 'hfb }
UVM_INFO testbench.sv(124) @ 0: uvm_test_top.compB [COMPB] Packet received from subCompA
pkt: (Packet@2133) { addr: 'h9a data: 'hfb }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
3. Port to Export to Imp
Now the initiator is componentA itself, with no subcomponent, and the target is still subCompB inside componentB. componentA holds the send loop directly; componentB, subCompB and the test are the same as in example 1.
class componentA extends uvm_component;
`uvm_component_utils(componentA)
uvm_blocking_put_port #(Packet) m_put_port;
int m_num_tx = 2;
function new(string name = "componentA", uvm_component parent = null);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_put_port = new("m_put_port", this);
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this);
repeat (m_num_tx) begin
Packet pkt = Packet::type_id::create("pkt");
assert(pkt.randomize());
`uvm_info("SUBCOMPA", "Packet sent to subCompB", UVM_LOW)
pkt.print(uvm_default_line_printer);
m_put_port.put(pkt);
end
phase.drop_objection(this);
endtask
endclass
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
------------------------------------------------------
Name Type Size Value
------------------------------------------------------
uvm_test_top my_test - @1839
compA componentA - @1908
m_put_port uvm_blocking_put_port - @1974
compB componentB - @1939
m_put_export uvm_blocking_put_export - @2044
m_subcomp_B subCompB - @2010
m_put_imp uvm_blocking_put_imp - @2081
------------------------------------------------------
UVM_INFO testbench.sv(73) @ 0: uvm_test_top.compA [SUBCOMPA] Packet sent to subCompB
pkt: (Packet@2120) { addr: 'ha1 data: 'h64 }
UVM_INFO testbench.sv(127) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: (Packet@2120) { addr: 'ha1 data: 'h64 }
UVM_INFO testbench.sv(73) @ 0: uvm_test_top.compA [SUBCOMPA] Packet sent to subCompB
pkt: (Packet@2134) { addr: 'hc1 data: 'hb9 }
UVM_INFO testbench.sv(127) @ 0: uvm_test_top.compB.m_subcomp_B [SUBCOMPB] Packet received from subCompA
pkt: (Packet@2134) { addr: 'hc1 data: 'hb9 }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
The message ID is still SUBCOMPA, but the log line's source is uvm_test_top.compA, confirming that componentA is now the initiator.
4. Port to Port to Export to Export to Imp
Finally, the target sits two levels down: subCompB2 inside subCompB1 inside componentB. Each level on the target side adds one export that simply forwards to the level below.
class subCompB1 extends uvm_component;
`uvm_component_utils(subCompB1)
subCompB2 m_subcomp_B2;
uvm_blocking_put_export #(Packet) m_put_export; // forwards to subCompB2
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_subcomp_B2 = subCompB2::type_id::create("m_subcomp_B2", this);
m_put_export = new("m_put_export", this);
endfunction
virtual function void connect_phase(uvm_phase phase);
m_put_export.connect(m_subcomp_B2.m_put_imp); // export to imp
endfunction
endclass
// componentB now creates m_subcomp_B1 and connects:
// m_put_export.connect(m_subcomp_B1.m_put_export); // export to export
// subCompB2 is the old subCompB, reporting with the ID SUBCOMPB2
UVM_INFO @ 0: reporter [RNTST] Running test my_test...
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_root.svh(579) @ 0: reporter [UVMTOP] UVM testbench topology:
--------------------------------------------------------
Name Type Size Value
--------------------------------------------------------
uvm_test_top my_test - @1845
compA componentA - @1914
m_put_port uvm_blocking_put_port - @2011
m_subcomp_A subCompA - @1977
m_put_port uvm_blocking_put_port - @2050
compB componentB - @1945
m_put_export uvm_blocking_put_export - @2118
m_subcomp_B1 subCompB1 - @2084
m_put_export uvm_blocking_put_export - @2186
m_subcomp_B2 subCompB2 - @2152
m_put_imp uvm_blocking_put_imp - @2223
--------------------------------------------------------
UVM_INFO testbench.sv(75) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: (Packet@2262) { addr: 'h24 data: 'h31 }
UVM_INFO testbench.sv(179) @ 0: uvm_test_top.compB.m_subcomp_B1.m_subcomp_B2 [SUBCOMPB2] Packet received from subCompA
pkt: (Packet@2262) { addr: 'h24 data: 'h31 }
UVM_INFO testbench.sv(75) @ 0: uvm_test_top.compA.m_subcomp_A [SUBCOMPA] Packet sent to subCompB
pkt: (Packet@2284) { addr: 'h9a data: 'hfb }
UVM_INFO testbench.sv(179) @ 0: uvm_test_top.compB.m_subcomp_B1.m_subcomp_B2 [SUBCOMPB2] Packet received from subCompA
pkt: (Packet@2284) { addr: 'h9a data: 'hfb }
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_objection.svh(1271) @ 0: reporter [TEST_DONE] 'run' phase is ready to proceed to the 'extract' phase
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
--- UVM Report Summary ---
Quick Check
Q: In example 4, which component's connect_phase makes the export-to-export connection, and which makes the export-to-imp connection?
Show Answer
componentB connects its own export to subCompB1's export, and subCompB1 connects its export to subCompB2's imp. Each component only ever connects its own connectors to those of its direct children; the test makes the single across connection at the top.
Why Not Connect the Inner Components Directly?
The test could, in principle, reach inside and write compA.m_subcomp_A.m_put_port.connect(compB.m_subcomp_B.m_put_imp). It works, but it breaks encapsulation: the test now depends on how componentA and componentB are built inside. If either is reorganized, or replaced by a factory override with a different internal structure, the connection breaks.
With a port on componentA's boundary and an export on componentB's, each component presents a fixed interface to the outside and keeps its internals private. That is exactly how reusable agents and environments are delivered: an agent exposes its monitor's analysis port as its own port, and an environment exposes its agents' connectors in the same way.
Common Mistakes
Mistake #1: Reversing the port-to-port connection
Wrong: this.m_put_port.connect(m_subcomp_A.m_put_port);
Right: m_subcomp_A.m_put_port.connect(this.m_put_port);
Why? On the initiator side the call travels from child to parent, so the child's port is the one that calls connect().
Mistake #2: Using an export where the chain should end
If componentB implements put() itself, it needs an imp, as in example 2. An export with nothing connected below it leaves the chain without an implementation, and UVM reports an error when it resolves the connections before the run.
Mistake #3: Skipping a level
A component may only connect to its own connectors and those of its direct children. Reaching through a child to connect a grandchild's imp directly bypasses the child's export, ties the parent to the child's internals, and makes the child impossible to reuse on its own.
Mistake #4: Forgetting to create an intermediate connector
Every port and export in the chain is an object that must be created with new() in its component's build_phase. A missing new() on a middle-level port or export causes a null-handle error during connect_phase, often reported far from the line that is actually missing.
Summary
In this article, you learned:
- Ports carry a call up and out of the initiator's hierarchy, and exports carry it down into the target's hierarchy
- Every chain ends at one imp, in the component that implements the method
connect()is always called on the connector nearer the initiator: child port to parent port, parent export to child export or imp- Each component connects only its own connectors and its direct children's, and the common parent makes the across connection
- However long the chain, UVM resolves it before the run, so calls reach the implementation in a single hop