Skip to content

UVM

TLM Connections

The rules and mechanics of wiring TLM topologies — the connect() direction rule (initiator toward provider), the common-parent rule, making connections in connect_phase after build, and how the topology is resolved and checked at elaboration.

TLM Communication · Module 16 · Page 16.6

The Engineering Problem

The previous chapters built the TLM constructs — ports, exports, imps (Modules 16.2–16.4), and FIFOs (16.5). But having constructs is not wiring them up. A real testbench has many components, nested in a hierarchy, that must be connected into a working topology: producers to consumers, across hierarchy boundaries, in the right direction, by the right component, at the right time. Get any of these wrong and the connection fails: wire backwards and nothing transfers; connect from a component that doesn't own both endpoints and you hit null handles; connect too early (before components exist) and you hit null handles; leave a required connection unmade and elaboration errors. The problem this chapter solves is the grammar of TLM connections — the rules for how, where, by whom, and when TLM connections are made, so the topology is valid and resolved.

TLM connections follow a small set of rules. The direction rule: initiator.connect(target) — the initiator (a port, or a forwarding export) connects toward the target (an export or imp); you always connect from the requesting side toward the providing/implementing side. The common-parent rule: a connection is made by the component that owns both endpoints — a parent wires its own children's interfaces; to connect across distant parts of the hierarchy, you promote each interface to its component's boundary (Module 16.4) and connect at the common ancestor. The timing rule: connections are made in connect_phaseafter build_phase has constructed all components (and their ports/exports/imps), so the handles exist to connect. And the resolution rule: UVM resolves and checks the topology at elaboration (end_of_elaboration) — flagging unconnected required ports and chains with no imp. connect() builds static topology; the transfers (put/get/write) happen at run time. This chapter is the grammar of TLM connections: the direction, common-parent, timing, and resolution rules.

What are the rules for wiring TLM connections — the direction (initiator.connect(target)), the common parent that owns both endpoints, the connect_phase timing after build, and the elaboration-time resolution — that make a connection topology valid?

Motivation — why connections need rules

TLM connections look simple — a.connect(b) — but the rules exist because each can go wrong in a specific way. The reasons:

  • Direction matters: a connection has a requester and a provider. A connection isn't symmetric — one side initiates (calls the method) and the other provides (implements it). connect() must go initiator → target; reversed, the topology is invalid (the provider can't "call into" the requester).
  • Ownership matters: you can only connect what you can reach. A connection is made by a component that holds handles to both endpoints — its own children (or interfaces promoted to their boundaries). A component can't connect two things it doesn't own — it has no handle to them.
  • Hierarchy matters: distant components connect at a common ancestor. Two components in different subtrees connect at their common ancestor, which owns (transitively) bothafter each interface is promoted to its subtree's boundary. You don't reach across the hierarchy directly.
  • Timing matters: connect after build. Components (and their ports) are created in build_phase. Connecting before they exist (in the constructor, or early in build) hits null handles. Connections belong in connect_phase, after all builds.
  • Validity matters: the topology is checked. A valid topology has every required port connected and every chain terminating in an imp. UVM checks this at elaboration and errors on violations — so mistakes are caught, not silently shipped.

The motivation, in one line: a TLM connection has a direction (requester → provider), an owner (the component holding both handles), a time (after build, in connect_phase), and a validity (checked at elaboration) — and each rule exists because violating it produces a specific failure (reversed = invalid, wrong owner = null, too early = null, unmade = elaboration error), so the rules are what make a connection topology valid and resolvable.

Mental Model

Hold TLM connections as running cables in a building — by whoever owns both ends, from source to destination, inspected before power-on:

Wiring TLM connections is like running network cables in a building: each cable runs from a source jack to a destination jack, in that direction; it's run by whoever owns both ends — a floor manager wires their own floor, and to connect two floors you bring each floor's cable to a riser and connect at the level that owns both; cabling happens before power-on, after the rooms are built; and an inspector checks every cable before the power goes on. Picture cabling a building's network. Each cable (a connection) runs from a source jack (the initiator — a port) to a destination jack (the target — an export or imp) — directional: source → destination. The cabling is run by whoever owns both ends — a floor manager (a parent component) wires the jacks on their own floor (their children's interfaces); to connect two different floors, each floor brings its cable up to a shared riser (each subtree promotes its interface to its boundary), and the building manager (the common ancestor) connects them at the level that owns both. The cabling happens after the rooms are built (after build_phase — you can't cable a jack that doesn't exist yet) and before power-on (connections in connect_phase, before the run). And an inspector (UVM's elaboration check) verifies every cable before the power goes onevery required jack cabled, every run reaching a real device (an imp) — flagging a dangling cable or a missing connection. The cabling is the static infrastructure (the topology); the network traffic (the transfersput, get, write) flows later, when the power's on (at run time).

So TLM connections are cabling a building: each connection a directional cable (initiator → target), run by whoever owns both ends (the common parent, with interfaces promoted to boundaries to reach across floors), after the rooms are built (connect_phase, post-build), inspected before power-on (elaboration check). The cabling is static topology; the traffic is run-time transfers. Run each cable from source to destination, by whoever owns both ends, after building, and let the inspector verify before power-on.

Visual Explanation — the direction rule and what connects to what

The defining picture is the direction: initiator.connect(target) — a port (or forwarding export) connects toward an export or imp, never backwards.

The connection direction rule: ports and exports connect toward exports and imps, terminating in an impport.connect(export)port.connect(imp)export.connect(imp)Port (initiator)calls the methodExport (forwarder)forwards toward impPort (initiator)calls the methodImp (target, terminal)implements the method12
Figure 1 — the connection direction rule: initiator.connect(target), always toward the implementer. A port (the initiator) connects to an export or an imp (the target). A forwarding export connects to another export or an imp. An imp is terminal — nothing connects from it. So connections flow from the requesting side toward the providing/implementing side: port → export → imp. The direction is not symmetric; you read a.connect(b) as 'a (initiator) connects to b (target)', and reversing it makes the topology invalid.

The figure shows the direction rule — the grammar of what connects to what. A port (the initiator — it calls the method) connects toward an export or an imp: port.connect(export) and port.connect(imp) are valid. A forwarding export connects toward another export or an imp: export.connect(imp) is valid. The imp is terminal — it implements the method, and nothing connects from it (it's the destination, never a source). The crucial reading is the consistent direction: connections flow from the requesting side (the brand-colored ports) toward the providing/implementing side (the success-colored imp), through any forwarding exportsport → export → imp. The direction is not symmetric: a.connect(b) reads as "a (initiator) connects to b (target)" — the initiator is the one calling connect, the target is the argument. Reversing it (imp.connect(port), or connecting an export backwards) makes the topology invalid — the provider can't "call into" the requester; the direction of connect must match the direction of the method call it sets up. This one ruleinitiator toward target, terminating in an impgoverns all TLM connection (whether the put/get model of Module 16.2 or the analysis model of 16.3–16.4): the port/initiator always connects toward the imp/implementer. The diagram is the connection grammar: read connect left-to-right as requester-to-provider, flow toward the imp, and never reverse it — get the direction right, and the topology is valid in its most fundamental respect.

RTL / Simulation Perspective — the common parent connects, in connect_phase

In code, the common parent makes connections in connect_phase, wiring its own children (or promoted boundaries). The code shows the ownership and timing rules.

the common parent connects its children's interfaces, in connect_phase (after build)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// === A PARENT connects ITS OWN children — it holds handles to both ===
class my_env extends uvm_env;
  producer_comp prod;   consumer_comp cons;        // children, built in build_phase
  function void build_phase(uvm_phase phase);
    prod = producer_comp::type_id::create("prod", this);   // create children FIRST
    cons = consumer_comp::type_id::create("cons", this);
    // ✗ NEVER connect here — prod/cons ports may not be fully constructed; connect in connect_phase
  endfunction
  function void connect_phase(uvm_phase phase);            // AFTER build: handles exist
    prod.port.connect(cons.imp);                          // initiator.connect(target), owns both
  endfunction
endclass
 
// === CROSS-HIERARCHY: each subtree PROMOTES its interface to its boundary; connect at the ANCESTOR ===
class top_env extends uvm_env;
  sub_env_a a;   sub_env_b b;     // each sub-env exposes a boundary interface (Module 16.4)
  function void connect_phase(uvm_phase phase);
    a.out_port.connect(b.in_export);   // connect the BOUNDARIES — not a.prod.port → b.cons.imp
    // ✗ NEVER: a.prod.port.connect(b.inner.cons.imp);  ← reaches into both subtrees' internals
  endfunction
endclass
// connect() builds the static topology; the actual put/get/write transfers happen at RUN TIME.

The code shows the ownership and timing rules. A parent (my_env) connects its own children's interfaces — it holds handles to both prod and cons, so it can wire them. Timing: the children are created in build_phase, and the connection is made in connect_phaseafter build, when the handles exist (the comment marks the trap: connecting in build_phase, where the ports may not be constructed, hits null handles). Direction: prod.port.connect(cons.imp)initiator toward target. For cross-hierarchy (top_env), each sub-env promotes its interface to its boundary (Module 16.4 — a.out_port, b.in_export), and the common ancestor connects the boundaries (a.out_port.connect(b.in_export)) — the comment marks the encapsulation violation: never reach a.prod.port.connect(b.inner.cons.imp) (into both subtrees' internals). The shape to carry: connections are made by the component that owns both endpoints (a parent its children; the common ancestor for cross-subtree, via promoted boundaries), in connect_phase (after build), in the initiator → target direction. The closing comment re-grounds the static/dynamic split: connect() builds the topology (who talks to whom — static), and the transfers (put/get/write) happen at run time (dynamic). The connect_phase is where the cabling happens; the run is where the traffic flows.

Verification Perspective — connection is static topology, resolved at elaboration

A TLM connection is static structure — built in connect_phase, resolved and checked at elaboration, before any transfer. Understanding the lifecycle is understanding when connection errors surface.

The connection lifecycle: build components, connect topology, resolve and check at elaboration, then run transfersbuild_phase: create components + ports → connect_phase: wire the topology → end_of_elaboration: resolve + check → run_phase: transfers happenbuild_phase: create components + ports → connect_phase: wire the topology → end_of_elaboration: resolve + check → run_phase: transfers happen1build_phase: create components + interfacescomponents and their ports, exports, and imps are constructed —handles now exist.2connect_phase: wire the topologycommon parents call connect() — initiator toward target — buildingthe static topology.3end_of_elaboration: resolve + checkUVM resolves the bindings and checks: required ports connected,chains terminate in an imp.4run_phase: transfers happenonly now do the actual put/get/write transfers flow through theestablished topology.
Figure 2 — the connection lifecycle: build, connect, resolve/check, then run. In build_phase, components and their ports/exports/imps are created. In connect_phase, the common parents wire the topology with connect(). At end_of_elaboration, UVM resolves the bindings and checks the topology — every required port connected, every chain terminating in an imp — erroring on violations. Only then, at run time, do the actual transfers (put/get/write) happen. Connection errors surface at elaboration, before the run; the topology is static, the transfers dynamic.

The figure shows the connection lifecyclewhen each thing happens, and when errors surface. build_phase (step 1): components and their ports, exports, and imps are constructed — the handles now exist. connect_phase (step 2): the common parents call connect()initiator toward targetbuilding the static topology. end_of_elaboration (step 3): UVM resolves the bindings and checks the topology — every required port connected, every chain terminating in an imperroring on violations. run_phase (step 4): only now do the actual transfers (put/get/write) flow through the established topology. The verification insight is that the connection is entirely static and resolved before the run: the topology is built (connect_phase) and checked (elaboration) before a single transfer happens. This is why connection errors — unconnected required port, chain with no imp (Module 16.4), wrong direction — surface at elaboration, not at run time: UVM resolves the bindings and validates them before the run, so a malformed topology is caught early (a build/elaboration error), not discovered as a mysterious run-time failure. The separation of static topology (connect + resolve) from dynamic transfer (run) is fundamental: connect() never moves a transaction — it establishes who can talk to whom; the transfers use that topology later. The figure is the temporal map of connections: create, connect, resolve, run — with connection correctness established (and checked) entirely in the first three (static) steps, before the fourth (dynamic). Get the topology right and resolved at elaboration, and the run-time transfers just flow through it.

Runtime / Execution Flow — wiring a connection step by step

Making a connection is a fixed procedure: identify the endpoints, find their common owner, promote interfaces to boundaries if needed, and connect in connect_phase. The flow shows the steps.

Wiring a connection: identify endpoints, find common owner, promote to boundaries, connect in connect_phaseidentify initiator + target → find the common owner → promote interfaces to boundaries (if buried) → connect(initiator → target) in connect_phaseidentify initiator + target → find the common owner → promote interfaces to boundaries (if buried) → connect(initiator → target) in connect_phase1Identify the initiator and targetthe initiator (a port) calls the method; the target (an export orimp) provides it.2Find the common ownerthe component that holds handles to both endpoints — a commonparent in the hierarchy.3Promote buried interfaces to boundariesif the endpoints are deep in different subtrees, expose each at itssubtree's boundary (Module 16.4).4connect(initiator → target) in connect_phasein the common owner's connect_phase, wire them; elaboration thenresolves and checks.
Figure 3 — wiring a connection. Identify the two endpoints: the initiator (a port) and the target (an export or imp). Find the component that owns both — a common parent. If the endpoints are buried in different subtrees, promote each to its subtree's boundary (an export or promoted port). In the common parent's connect_phase, call initiator.connect(target). The topology is then resolved and checked at elaboration. Each connection follows this: owner, direction, timing, boundaries.

The flow is the procedure for making a connection correctly. Identify (step 1): the initiator (a port — it calls the method) and the target (an export or imp — it provides it). Find the common owner (step 2): the component that holds handles to both endpoints — a common parent in the hierarchy. Promote (step 3): if the endpoints are deep in different subtrees, expose each at its subtree's boundary (Module 16.4) — so the common owner can reach them. Connect (step 4): in the common owner's connect_phase, call initiator.connect(target); elaboration then resolves and checks. The runtime insight is that every connection follows this same procedureowner, direction, timing, boundaries — so wiring a topology is applying it repeatedly: for each producer-consumer pair, find their common owner, promote their interfaces to that level's boundaries, and connect in connect_phase. The procedure makes the four rules operational: direction (step 1, initiator → target), common parent (step 2, the owner), boundaries (step 3, promotion for cross-hierarchy), timing (step 4, connect_phase). This is why a well-structured testbench's connections are clean: each is made at the right level (the common owner), through boundaries (promoted interfaces), in the right direction, at the right time — and the hierarchy (Module 15) naturally provides the common owners (each level owns its children, connects them, and promotes what external levels need). The flow is the connection discipline: for each connection, owner → boundaries → direction → connect_phaseapplied consistently, it builds a valid, encapsulated topology that resolves cleanly at elaboration.

Waveform Perspective — topology before the run, transfers during it

The static/dynamic split is visible on a timeline: the connection topology is established before the run (at elaboration), and the transfers happen during the run. The waveform shows the two regimes.

The topology is established before the run (elaboration); transfers happen during the run

12 cycles
The topology is established before the run (elaboration); transfers happen during the runelaboration: topology established + checked (connected) — before the runelaboration: topology …run: transfers flow through the established topology (xfer, A1)run: transfers flow th…run: more transfers (A2, A3) — same fixed topology, used repeatedlyrun: more transfers (A…clkelaborateconnectedxfertxn----A1----A2----A3------t0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — connection is static, transfers are dynamic. Before the run, at elaboration, the topology is established and checked: connected marks the connection resolved (a single setup event before time advances). Then during the run, the actual transfers flow through that established topology: xfer pulses each time a put/get/write moves a transaction. So connect() built the path once, before the run; the transfers use it repeatedly, during the run. Connection errors would have surfaced at elaboration, before any transfer — the topology is fixed before the first transaction moves.

The waveform shows the static/dynamic split. Before the run, at elaboration, the topology is established and checked: elaborate pulses and connected goes (and stays) high — the connection is resolved (a single setup event before time advances). Then during the run, the actual transfers flow through that established topology: xfer pulses each time a put/get/write moves a transaction (A1, then A2, A3). The crucial reading is the two regimes: connected is established once, before the run, and stays fixed (the static topology); xfer pulses repeatedly, during the run (the dynamic transfers). The connection connect() built the path once, before the run; the transfers use it repeatedly, during the run. The picture to carry is that connection and transfer are separate in time: the cabling (connected) is done before power-on; the traffic (xfer) flows after. Connection errors — wrong direction, unconnected port, no imp — would have surfaced at elaboration (where connected is established and checked), before any transfer — so by the time xfer first pulses, the topology is known-valid. Reading the timeline this way — topology fixed and checked at elaboration; transfers flowing through it at runseparates the static structure from the dynamic behavior. The connection established before the first transaction moves is the signature of TLM connection: connect() wires a fixed topology at elaboration, and the run-time transfers flow through that fixed, pre-validated topology — structure first, then traffic.

DebugLab — the connection made too early

A null-handle failure because connect() was called in build_phase, before the target was built

Symptom

A testbench failed with a null-handle error — a fatal during build, pointing at a connect() call: the target of the connection (a child component's port/imp) was null when connect() tried to use it. The components seemed defined; the connection looked correct in direction and ownership. But it failed — the target handle didn't exist yet at the moment connect() ran.

Root cause

The connect() call was placed in build_phase (or the constructor) — before the target component had been built, so its port/imp didn't exist yet when connect() tried to reach it:

why connecting too early hit a null handle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ connect() in BUILD_PHASE (or new()) — the target isn't built yet:
  function void build_phase(uvm_phase phase);
    prod = producer::type_id::create("prod", this);
    prod.port.connect(cons.imp);     // ✗ cons is NULL here — not created yet (or created after)
    cons = consumer::type_id::create("cons", this);
  endfunction
  // even reordered, ports may not be fully constructed in build → null handle / fatal
 
✓ connect() in CONNECT_PHASE — runs AFTER all build_phases complete:
  function void build_phase(uvm_phase phase);
    prod = producer::type_id::create("prod", this);   // build creates components...
    cons = consumer::type_id::create("cons", this);
  endfunction
  function void connect_phase(uvm_phase phase);        // ...connect wires them, AFTER they exist
    prod.port.connect(cons.imp);     // both built → handles valid → connection succeeds
  endfunction

This is the connect-too-early bug — making a connection before the components (and their interfaces) exist, which hits a null handle. UVM's phasing separates build_phase (where components are created) from connect_phase (where they're connected) precisely so that, by the time connections are made, all components exist. build_phase runs top-down and creates the hierarchy; connect_phase runs after all build_phases complete, so every component — parent and childand its ports/exports/impsexist. Placing connect() in build_phase violates this: at that point, the target component may not be created yet (if it's created later in the same build, or in a child's build that hasn't run), so its port/imp handle is null — and connect() fatals. Even reordering the creates doesn't reliably fix it, because a child's ports are constructed in the child's build_phase, which runs after the parent's — so a parent connecting in its build_phase can't see a child's fully-constructed ports. The fix is to make connections in connect_phase: build_phase creates the components, and connect_phase — running after all buildswires them, when all handles are valid. The general lesson, and the chapter's thesis: TLM connections are made in connect_phase, after build_phase has constructed all components and their interfacesconnecting earlier (in build_phase or the constructor) hits null handles, because the targets don't exist yet; UVM's build-then-connect phasing exists to order creation before connection, so respect itcreate in build, connect in connect. You can't cable a jack that hasn't been installed; build the rooms first, then run the cables.

Diagnosis

The tell is a null-handle fatal at a connect() call. Diagnose connect-timing errors:

  1. Check which phase the connect() is in. A connect() in build_phase or a constructor runs before targets exist; it belongs in connect_phase.
  2. Confirm the target is built before the connect runs. A null target handle means the component wasn't created when connect() executed.
  3. Remember children build after parents. A parent connecting in its own build_phase can't see a child's ports, which are built in the child's later build_phase.
  4. Move connections to connect_phase. If a connection fails with a null handle but the direction and ownership are right, the timing is the issue.
Prevention

Create in build, connect in connect:

  1. Make all connections in connect_phase. It runs after every build_phase, so all components and their interfaces exist.
  2. Never connect in the constructor or build_phase. The targets may not be constructed yet; connecting hits null handles.
  3. Create components in build_phase. Build constructs the hierarchy; connect wires it — keep the two phases' jobs separate.
  4. Trust the phasing. UVM orders build before connect precisely so connections have valid handles; respect the order rather than working around it.

The one-sentence lesson: make TLM connections in connect_phase, after build_phase has constructed all components and their ports/exports/imps — connecting earlier (in build_phase or the constructor) hits null handles because the targets don't exist yet; UVM's build-then-connect phasing exists to order creation before connection, so create in build and connect in connect.

Common Mistakes

  • Connecting in build_phase or the constructor. Targets may not exist yet; make connections in connect_phase, after all builds complete.
  • Reversing the direction. connect() goes initiator → target (port toward export/imp); reversing it makes the topology invalid.
  • Connecting from a component that doesn't own both endpoints. A connection is made by the common owner; reaching for handles it doesn't hold fails.
  • Reaching across the hierarchy into internals. Connect at the common ancestor through promoted boundaries, not by reaching into distant subtrees' internals.
  • Leaving a required port unconnected. Elaboration errors on an unconnected required port; connect every port that needs it.
  • Expecting connect() to transfer data. connect() builds static topology; transfers (put/get/write) happen at run time through that topology.

Senior Design Review Notes

Interview Insights

There are four rules: direction, common parent, timing, and resolution. Direction: a connection is made initiator toward target — initiator.connect(target) — where the initiator is a port, or a forwarding export, and the target is an export or an imp. You always connect from the requesting side toward the providing or implementing side, terminating in an imp. The direction is not symmetric, and reversing it makes the topology invalid. Common parent: a connection is made by the component that owns both endpoints — a parent wires its own children's interfaces. To connect components in different subtrees, you promote each interface to its subtree's boundary and connect at the common ancestor that owns both; you don't reach across the hierarchy into internals. Timing: connections are made in connect_phase, which runs after build_phase has constructed all components and their ports, exports, and imps, so the handles exist. Connecting earlier, in build_phase or a constructor, hits null handles because the targets don't exist yet. Resolution: UVM resolves the bindings and checks the topology at end_of_elaboration — every required port connected, every chain terminating in an imp — and errors on violations, so malformed topologies are caught before the run. Underlying all of this is that connect() builds static topology, the who-talks-to-whom, while the actual transfers — put, get, write — happen at run time through that topology. The mental model is cabling a building: each cable runs from a source to a destination, run by whoever owns both ends, after the rooms are built, and inspected before power-on. So a valid connection has the right direction, is made by the common owner through boundaries, in connect_phase, and resolves cleanly at elaboration.

Because components and their interfaces are created in build_phase, so they don't all exist until build_phase completes, and connect_phase runs after that — when all the handles are valid. UVM's phasing deliberately separates the two jobs: build_phase constructs the hierarchy, creating components and their ports, exports, and imps, while connect_phase wires them together. build_phase runs top-down, so a parent is built before its children, and each child's ports are constructed in the child's build_phase, which runs after the parent's. This means that during a parent's own build_phase, its children's ports may not be constructed yet. If you call connect() in build_phase, the target component or its port may be null, and the connection fatals with a null-handle error. Even reordering the create calls doesn't reliably fix it, because a child's interfaces simply aren't built until the child's build runs. connect_phase solves this by running after every build_phase across the whole hierarchy has completed, so by the time connections are made, every component and every port, export, and imp exists. The handles are all valid, and connect() succeeds. So the rule is to create components in build_phase and connect them in connect_phase, keeping the two phases' jobs separate. This is one of the clearest reasons UVM has distinct build and connect phases — to order construction before connection. The practical guidance is never to connect in a constructor or in build_phase; if a connection fails with a null handle but the direction and ownership look right, the cause is almost always that the connection was attempted too early. Trust the phasing: build first, then connect.

You promote each component's interface to its subtree's boundary, then connect the boundaries at the common ancestor that owns both subtrees. A connection is made by the component that holds handles to both endpoints. If the two components are buried in different subtrees, no single component near them holds both handles — only their common ancestor does, transitively. But the ancestor shouldn't reach down into each subtree's internals to grab the buried ports, because that breaks encapsulation and couples the connection to internal structure. Instead, each subtree exposes the relevant interface at its own boundary — a producer subtree promotes its port up to its boundary, and a consumer subtree exposes an analysis export or similar at its boundary that forwards inward to its imp. Then the common ancestor connects those boundary interfaces: it connects the producer subtree's boundary port to the consumer subtree's boundary export. The connection is made at the level that owns both subtrees, using only the boundary interfaces, so neither subtree's internals are exposed. This is the same encapsulation discipline as elsewhere — connect at boundaries, not internals — and it's enabled by the export's forwarding role, which promotes a buried imp to a boundary. So the procedure is: identify the initiator and target, find their common owner, promote each buried interface to its subtree's boundary, and connect initiator toward target at the common owner's connect_phase. The hierarchy naturally provides the common owners, since each level owns its children and can promote what external levels need. The result is a connection made at the right level, through clean boundaries, that doesn't break when either subtree's internals change. Reaching across the hierarchy directly into internals would work mechanically but would couple the connection to internal structure and forfeit reuse, which is exactly what promoting to boundaries avoids.

Building the topology is static and happens before the run; the transfers are dynamic and happen during the run, through the established topology. connect() establishes who can talk to whom — it wires the ports, exports, and imps into a connection graph. This is purely structural: it doesn't move any transaction; it just sets up the paths. It happens in connect_phase, and then UVM resolves and checks the topology at end_of_elaboration, validating that every required port is connected and every chain terminates in an imp. All of this occurs before the run starts. Then, at run time, the actual transfers flow through the established topology: a producer calls put or write on its port, and the call travels through the connected exports to the imp, which implements the method. These transfers happen repeatedly during the run, each moving a transaction, all using the same fixed topology that connect() built once. So the topology is built and validated once, before the run, and the transfers use it many times, during the run. This separation matters for debugging: connection errors — wrong direction, unconnected required port, a chain with no imp — surface at elaboration, before any transfer, as build or elaboration errors. They are not mysterious run-time failures; they're caught when the topology is resolved. Conversely, a run-time problem with the data is not a connection problem, because the connection was already validated. The mental model is cabling versus traffic: connect() is the cabling, done before power-on and inspected; the transfers are the network traffic, flowing after power-on through the cables. Understanding this tells you where to look — topology problems at elaboration, data problems at run time — and it clarifies that connect() never transfers anything itself; it only establishes the paths that transfers later use.

UVM resolves the bindings and checks the topology at end_of_elaboration, after connect_phase and before run_phase. The main things it checks are that every port with a required minimum number of connections actually has them, and that every connection chain terminates in an imp that implements the method. A port that requires at least one connection but was left unconnected is flagged, and an analysis or TLM chain that forwards through exports but never reaches an imp — so there's no implementation to receive the calls — is flagged as having no implementation. These checks happen during the resolve-bindings step at end_of_elaboration, which is a phase that runs after all connect_phases complete and before the run begins. The reason this timing matters is that it catches malformed topologies before any transaction moves, turning what would otherwise be a confusing run-time failure — calls going nowhere, a subscriber receiving nothing — into a clear elaboration-time error that points at the specific unconnected port or unterminated chain. So the topology is validated statically, as part of elaboration, not discovered dynamically during the run. For the engineer, this means connection mistakes show up early, with diagnostic messages, rather than as silent data loss later. It also reinforces the separation between static topology and dynamic transfer: the checking is about the structure being well-formed — directions valid, required connections present, chains terminating in implementers — independent of what data will eventually flow. If the topology passes these elaboration checks, you know the paths are well-formed before the first transfer. If it fails, you fix the connection — connect the missing port, terminate the chain in an imp — and re-run. So the answer is that UVM checks required connections and proper termination in imps, at end_of_elaboration, before the run, so topology errors are caught up front.

Exercises

  1. State the rules. List the four TLM connection rules and the specific failure each prevents.
  2. Fix the direction. Given imp.connect(port), explain why it's invalid and write the correct form.
  3. Connect across hierarchy. Describe how to connect a producer in one sub-env to a consumer in another, and at which level.
  4. Diagnose the null handle. A connect() fatals with a null target. Name the likely phase mistake and the fix.

Summary

  • TLM connections follow four rules. Direction: initiator.connect(target) — a port (or forwarding export) connects toward an export or imp, terminating in an imp; reversing it is invalid.
  • Common parent: a connection is made by the component that owns both endpoints — a parent wires its own children; to connect across distant hierarchy, promote each interface to its boundary (Module 16.4) and connect at the common ancestor.
  • Timing: connections are made in connect_phaseafter build_phase constructs all components and their interfaces, so the handles exist; connecting earlier (build_phase or constructor) hits null handles.
  • Resolution: UVM resolves and checks the topology at elaboration (end_of_elaboration) — flagging unconnected required ports and chains with no imp — so malformed topologies are caught before the run; connect() builds static topology, transfers happen at run time.
  • The durable rule of thumb: wire each TLM connection by its rules — initiator.connect(target) (toward the imp), made by the common owner through promoted boundaries, in connect_phase after build, validated at elaboration; the connection is static topology built and checked before the run, and the transfers flow through it during the run — connect too early, backwards, or from the wrong owner, and the topology fails.

Next — Broadcasting Transactions: the module closes by returning to the analysis path at full strength — broadcasting: how one producer fans a transaction out to many subscribers at once through the analysis system, the one-to-many semantics, the patterns for multiple subscribers and multiple streams, and how broadcasting ties together the ports, exports, and connections into the testbench's observation backbone.