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
Output
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);
Output
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
Output
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
Output
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
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5