Skip to content

UVM

connect_phase

The bottom-up phase after build that wires the structure — TLM ports (port.connect(export)) and coordination handles, why it runs after build, and the connect direction.

UVM Phasing · Module 5 · Page 5.3

The Engineering Problem

build_phase constructed the tree; connect_phase wires it. This is the phase where the data-flow graph (Module 3.4) and the coordination relationships (Module 4.7) are established — TLM ports connected, peer handles assigned. And it has two defining properties that, together, make it work and that bugs cluster around: it runs after build_phase (so every component and every port already exists), and it runs bottom-up (children wire their own internals before parents wire across them).

The phase is short to describe but precise in its rules. You may only establish connections here — not in build_phase, where the things you'd connect may not exist yet. The connect() call has a direction — you call it on the initiator's port with the provider's export as the argument (port.connect(export)), and reversing it doesn't wire the data path. And which connections belong to which component follows the structure: a component connects the ports of the children it owns and can see. Get the phase wrong (wiring in build), the direction wrong (export.connect(port)), or the responsibility wrong (nobody makes a cross-child connection), and transactions silently don't flow. This chapter makes connect_phase's timing, direction, and responsibilities precise.

What happens in connect_phase, why does it run after build_phase and bottom-up, what is the correct connect() direction, and how do you avoid the wiring bugs that leave transactions silently unable to flow?

Motivation — why a separate wiring phase, after build and bottom-up

connect_phase being its own phase, after build, traversed bottom-up, is exactly what a wiring step needs:

  • It runs after build, so everything to wire exists. A connection needs both endpoints — the monitor's analysis port and the scoreboard's export — to already exist. Because build_phase (the whole top-down cascade) completes before any connect_phase runs, every component and every port is present when you wire. This is why connections belong here and not in build: in build, a child's ports may not have been created yet.
  • Bottom-up matches who-connects-what. Each component wires the ports of the children it owns. A child connects its own internals first (the agent wires its driver to its sequencer), then its parent wires across it (the env wires the agent's monitor to the scoreboard). Bottom-up traversal means the lower-level connections are made before the higher-level ones that may depend on them.
  • It separates structure from wiring. Build constructs; connect wires. Keeping them in separate phases means you never try to connect something half-built — the tree is whole before any wire is drawn. This is the construct-then-connect discipline (Module 2.6) realised as two phases.
  • The direction and responsibility rules prevent silent failures. A connection wired backwards, or a cross-child connection nobody made, doesn't crash — transactions just don't flow. Knowing the direction (port.connect(export)) and whose job each connection is keeps the data-flow graph complete.

The motivation, in one line: wiring needs every endpoint to exist (so it's after build) and to proceed from the inside out (so it's bottom-up), and connect_phase is the dedicated, zero-time phase where the data-flow graph and coordination relationships are established under those exact conditions.

Mental Model

Hold connect_phase as wiring a building that's already been constructed, from the inside out:

connect_phase is the electrician's pass through a building whose rooms are all already framed. Construction (build_phase) finished first — every room exists, every outlet and junction box is in place — so now the electrician can run wires between them with confidence that both ends are there. They work from the inside out: each room's internal wiring is done first (the agent wires its own driver to its own sequencer), then the connections between rooms are run by whoever owns the larger space (the env runs the wire from the agent's monitor to the scoreboard). The wires have a direction — they run from a source outlet to a destination (port.connect(export)) — and a wire run backwards, or a connection between rooms that nobody was responsible for, leaves a dead circuit: present in the blueprint, but carrying nothing.

So in a connect_phase, you draw wires between things that already exist: connect the ports of the children you own (initiator's port to provider's export), and assign any coordination handles to peers. The building is framed (build is done); you're just running the wires, from the inside out, in the right direction.

Visual Explanation — connect_phase between build and run

connect_phase sits between construction and execution: build_phase makes the components exist, connect_phase wires them, and run_phase uses the wiring. Its two jobs are the data-flow connections and the coordination handles.

connect_phase wires TLM ports and assigns coordination handles, between build_phase construction and run_phase executionbuild (construct) → connect (wire) → run (use)build (construct) → connect (wire) → run (use)1build_phase — constructthe tree and all ports come into existence (top-down). Zero time.2connect_phase — TLM portswire the data-flow graph: port.connect(export) —monitor→scoreboard, driver↔sequencer.3connect_phase — coordinationassign peer handles — a virtual sequencer's handles to agentsequencers (Module 4.7).4run_phase — usetransactions flow over the connections; coordinated peers areorchestrated. Time advances.
Figure 1 — connect_phase between build and run. build_phase constructs the tree (all components and ports exist). connect_phase then wires it: TLM ports are connected (port.connect(export)) to establish the data-flow graph, and coordination handles are assigned (peer references). run_phase then uses the wiring — transactions flow over the connections. connect runs after build (so endpoints exist) and is zero-time, like build.

The position is the point. connect_phase is squarely after build and before run, and it's zero-time (a function phase, Module 5.1). Its two jobs are the two non-ownership relationships from Module 4.7: communication (TLM connect(), establishing the data-flow graph that Module 3.4 described) and coordination (assigning peer handles like a virtual sequencer's sequencer references). Both must happen here rather than in build, because in build the ports and sub-components you'd wire may not have been created yet — only after the entire build cascade completes is every endpoint guaranteed to exist. And both must happen before run, because the run phase uses these connections to move transactions. Build makes it exist, connect wires it, run uses it — and connect is the wiring step in the middle.

RTL / Simulation Perspective — connect_phase in code

Each component's connect_phase wires the ports of the children it owns. A child (the agent) connects its own internals; the parent (the env) connects across its children and assigns coordination handles. Both run after build, so all ports exist.

connect_phase — wire TLM ports (port.connect(export)) and assign handles
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class my_agent extends uvm_agent;            // a CHILD: connects its OWN internal ports
  `uvm_component_utils(my_agent)
  function void connect_phase(uvm_phase phase);
    if (get_is_active() == UVM_ACTIVE)
      drv.seq_item_port.connect(sqr.seq_item_export);   // driver(port) → sequencer(export)
  endfunction
endclass
 
class my_env extends uvm_env;                // the PARENT: connects ACROSS its children
  `uvm_component_utils(my_env)
  function void connect_phase(uvm_phase phase);
    // COMMUNICATION (data-flow graph): monitor's port → scoreboard's export
    agent.mon.ap.connect(sb.analysis_export);
    // COORDINATION (peer handle): the virtual sequencer holds the agent's sequencer
    vseqr.sqr = agent.sqr;
  endfunction
endclass

The code shows the direction, the responsibility split, and the timing. Direction: every connect() is called on the initiator's port with the provider's export as the argument — drv.seq_item_port.connect(sqr.seq_item_export) (driver pulls, so its port connects to the sequencer's export) and agent.mon.ap.connect(sb.analysis_export) (monitor produces, so its analysis port connects to the scoreboard's export). Responsibility: the agent connects its own children (driver↔sequencer); the env connects across its children (agent's monitor to the scoreboard) — each component wires what it owns and can see. Timing: all of this works because it's in connect_phase, after every component's build_phase has run, so drv, sqr, agent.mon, sb, and agent.sqr all exist. The coordination handle (vseqr.sqr = agent.sqr) is here for the same reason — agent.sqr exists only after the agent's build.

Verification Perspective — bottom-up, and why direction matters

Two mechanics define connect_phase: it traverses bottom-up (children before parents), and connect() has a fixed direction (initiator port → provider export). Both follow from how connections work.

Bottom-up: agent connect runs before env connect. Direction: monitor analysis port connects to scoreboard analysis exportchild connects first (bottom-up)child connectsfirst…port.connect(export)agent.connect_phase#1: drv.port → sqr.export (internal)env.connect_phase#2: agent.mon.ap → sb.export (across)monitor analysis PORTinitiator (calls connect)scoreboard analysis EXPORTprovider (the argument)12
Figure 2 — bottom-up connect and the connect() direction. Bottom-up: the agent's connect_phase (wiring its internal driver↔sequencer) runs before the env's connect_phase (wiring the agent's monitor to the scoreboard) — children wire their internals before parents wire across them. Direction: connect() is always called on the initiator's port with the provider's export as the argument (port.connect(export)); the monitor's analysis port connects to the scoreboard's analysis export, not the reverse.

Both mechanics prevent silent wiring failures. Bottom-up means a component's own internal connections are made before its parent's cross-child connections — the agent wires its driver to its sequencer before the env wires the agent's monitor to the scoreboard — which matches the natural dependency (a parent wires across children that have already wired themselves). Direction is fixed because TLM connections are directional: the initiator (the side that calls a method — the driver pulling, the monitor broadcasting) holds the port and calls connect(); the provider (the side that implements — the sequencer, the scoreboard's write) holds the export/imp and is the argument. So you always write port.connect(export), never export.connect(port) — reversing it doesn't establish the data path (the DebugLab). Bottom-up for ordering, port-to-export for direction: get both right and the data-flow graph is wired completely and correctly, all in this one zero-time phase.

Runtime / Execution Flow — wired at connect, used at run

The connections you make in connect_phase are used throughout the run phase: every transaction that flows from a monitor to a scoreboard, every item pulled by a driver from a sequencer, travels over a connection established once, at connect, and exercised many times at run.

connect_phase establishes the monitor to scoreboard connection; run_phase exercises it for every transactionThe connection: established at connect, exercised at runThe connection: established at connect, exercised at run1connect_phase — establishmonitor.ap.connect(sb.export): the connection is made, once, inzero time.2run_phase — first transactionthe monitor observes a transaction and broadcasts it over theconnection to the scoreboard.3run_phase — every transactionthe same connection carries every subsequent transaction, for thewhole run.4If the connection is missing/reversedno transactions reach the scoreboard — a silent miss, traced backto connect_phase.
Figure 3 — wired once at connect, used many times at run. In connect_phase (zero time), the monitor's analysis port is connected to the scoreboard's export — the connection is established. In run_phase, every transaction the monitor observes is broadcast over that same connection to the scoreboard, repeatedly, for the whole run. The connection is made once at connect and exercised continuously at run; a missing or reversed connection at connect means no transactions reach the scoreboard at run.

This establish-once, use-many relationship is why connect-phase bugs are silent and persistent. A connection is a one-time act in connect_phase, but it governs every transfer over it for the entire run. If you make it correctly, transactions flow for the whole simulation; if you make it backwards, forget it, or make it in the wrong phase, no transactions flow over it — and nothing crashes, because an unconnected analysis port simply broadcasts to nobody (Module 3.4). So a scoreboard receiving nothing, all run long, is almost always a single mistake in connect_phase: a reversed direction, a missing cross-child connection, or wiring attempted in build_phase where the port didn't exist yet. The fix is always at the connect site, made once, that then governs the whole run. Wire it right once, and it works for the entire simulation.

Waveform Perspective — connections made at connect, exercised at run

On a timeline, connect_phase is a zero-time sliver between build and run, and the connections it makes carry transactions throughout the run that follows — the wiring established in an instant, then exercised continuously.

connect_phase wires the graph at time zero; run_phase exercises it

10 cycles
connect_phase wires the graph at time zero; run_phase exercises itconnect_phase (zero time): monitor→scoreboard wired, after build, before runconnect_phase (zero ti…run_phase: a transaction flows over that connection (analysis broadcast)run_phase: a transacti…every TLM transfer at run uses a connection made once, at connect_phaseevery TLM transfer at …clkconnectingvaliddata0000A0A100B0B1000000analysist0t1t2t3t4t5t6t7t8t9
Figure 4 — connect_phase (the connecting region) wires the monitor→scoreboard connection at time zero, after build, with no pin or transaction activity. Then run_phase begins: transactions are driven (valid/data), and each observed transaction is broadcast over the connection made at connect (the analysis pulses). The connection is established once, in zero time, and carries every transaction for the whole run — which is why a missing connection at connect means a scoreboard silent for the entire run.

The connecting sliver at time zero, followed by analysis pulses during the run, is the establish-once/use-many pattern on a timeline. The connection is wired during the zero-time connect region — no transactions move yet, because connect is a function phase that only establishes structure. Then, throughout the run, each observed transaction is broadcast over that connection (the analysis pulses), which is the connection being exercised. The contrast between the single connect sliver and the repeated run-phase usage makes the lesson concrete: you act once, at connect, and that act governs the whole run. A flat analysis track during run — no broadcasts ever reaching the scoreboard — would point straight back to a missing or reversed connection in that thin connect sliver at the start.

DebugLab — the connection wired backwards

A scoreboard that received nothing — the analysis connection was reversed

Symptom

A scoreboard received zero transactions for the entire run, even though the monitor was clearly observing traffic (its own debug prints showed transactions) and the connection was present in connect_phase. The build was clean, the topology correct, the monitor working — yet nothing arrived at the scoreboard. The connection existed but carried nothing.

Root cause

The connect() was called in the wrong direction — on the scoreboard's export with the monitor's port as the argument, instead of the other way around:

why the reversed connect carried nothing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
correct:   agent.mon.ap.connect(sb.analysis_export);    // initiator PORT . connect( provider EXPORT )
written:   sb.analysis_export.connect(agent.mon.ap);    // reversed: export . connect( port )  ✗
rule:      connect() goes from the INITIATOR's port to the PROVIDER's export
           - monitor's analysis_port is the initiator (it calls write/broadcast)
           - scoreboard's analysis_export is the provider (it receives)
result:    the reversed call does not establish monitor→scoreboard delivery → scoreboard gets nothing
fix:       agent.mon.ap.connect(sb.analysis_export);    // port.connect(export)

TLM connections are directional: the side that initiates (here the monitor, which broadcasts) holds the port and must be the one to call connect(), with the receiving side's export as the argument. Calling connect the other way round — export-to-port — doesn't wire the producer-to-consumer delivery, so the monitor's broadcasts have no subscriber and the scoreboard stays empty. The connection looked present (a connect call was there) but pointed the wrong way.

Diagnosis

The tell is a present-but-non-functional connection — a scoreboard receiving nothing while the monitor clearly produces. Diagnose direction bugs at the connect site:

  1. Check the connect direction. It must be initiator_port.connect(provider_export). For a monitor→scoreboard analysis connection, that's mon.ap.connect(sb.analysis_export) — the monitor's port calls connect. Reversed (sb.export.connect(mon.ap)) is the bug.
  2. Identify initiator vs provider. The initiator calls the method (the monitor broadcasts via write); the provider implements it (the scoreboard's write/export receives). The initiator holds the port and calls connect; the provider's export is the argument.
  3. Confirm the monitor actually broadcasts. If the monitor produces (its prints show transactions) but the scoreboard gets nothing, the connection — not the monitor — is the suspect; check its direction and existence.
Prevention

Wire every connection in the right direction, in connect_phase:

  1. Always port.connect(export) — initiator to provider. The producer/initiator (monitor's analysis port, driver's seq_item port) calls connect() with the consumer/provider's export as the argument. Never reverse it.
  2. Make all connections in connect_phase. After build, when every port exists; never in build_phase. Each component connects the children it owns; the parent makes cross-child connections.
  3. Verify data flows, not just that a connect exists. A present connection can be backwards; confirm the scoreboard actually receives (count transactions) rather than trusting that a connect line is enough.

The one-sentence lesson: connect() is directional — call it on the initiator's port with the provider's export as the argument (port.connect(export)); reversing it leaves a present-but-dead connection, so a scoreboard that receives nothing while the monitor produces is almost always a backwards connect.

Common Mistakes

  • Reversing the connect direction. connect() goes from the initiator's port to the provider's export (port.connect(export)); calling it backwards (export.connect(port)) leaves a present-but-dead connection. The producer/initiator calls connect.
  • Wiring in build_phase instead of connect_phase. In build, the ports and sub-components you'd connect may not exist yet (a child's ports are created in its build, which runs later). All wiring belongs in connect_phase, after build.
  • Forgetting a cross-child connection. Each component connects what it owns; cross-child connections (the monitor in one subtree to the scoreboard in another) are the parent's responsibility. If nobody makes them, the data-flow graph has a gap and transactions don't reach the consumer.
  • Assuming connect is top-down. connect_phase is bottom-up — children wire their internals before parents wire across them. Expecting a parent's connections to be in place when a child connects is backwards.
  • Doing time-consuming work in connect. connect_phase is a zero-time function; waiting or driving belongs in run_phase. Connect only wires.
  • Trusting a connect line without checking data flow. A connection that's present but reversed, or to the wrong port, carries nothing. Verify the consumer actually receives, not just that a connect() call exists.

Senior Design Review Notes

Interview Insights

connect_phase is the zero-time phase, after build_phase, where you wire the already-constructed component tree. It establishes two of the relationship types: communication (the data-flow graph) and coordination. For communication, you connect TLM ports — for example, monitor.analysis_port.connect(scoreboard.analysis_export) and driver.seq_item_port.connect(sequencer.seq_item_export) — establishing the producer-to-consumer paths over which transactions will flow during the run. For coordination, you assign peer handles, such as setting a virtual sequencer's handles to the agents' sequencers. It runs after build for a critical reason: every component and every port must already exist to be wired, and only after the entire top-down build cascade completes is that guaranteed (a child's ports are created in its own build, which runs after its parent's). It also runs bottom-up — children connect their own internal ports before parents connect across them. And connect() is directional: you call it on the initiator's port with the provider's export as the argument. connect_phase is a function phase, so it only establishes connections and assigns handles — nothing time-consuming, which belongs in the run phase.

Exercises

  1. Write the connections. For an agent with a driver, sequencer, and monitor, and an env with a scoreboard, write the connect_phase of each: the agent's internal connection and the env's cross-child connection, with correct direction.
  2. Fix the direction. A scoreboard gets nothing; the code has sb.analysis_export.connect(mon.ap). State the rule it violates and the corrected line.
  3. Phase placement. Explain why the connection agent.mon.ap.connect(sb.analysis_export) must be in connect_phase and not build_phase, citing when the ports exist.
  4. Whose job? For a monitor in agent A and a scoreboard in the env, state which component's connect_phase makes the connection between them, and why it's that component's responsibility.

Summary

  • connect_phase is the zero-time, bottom-up phase after build_phase where you wire the constructed tree — establishing the data-flow graph (TLM connect()) and coordination relationships (peer handles).
  • It runs after build so every component and port exists to wire (a child's ports are created in its build, which runs after its parent's), and bottom-up so each component connects its own internals before its parent connects across it.
  • connect() is directional: call it on the initiator's port with the provider's export as the argument (port.connect(export)) — the producer/initiator (monitor's analysis port, driver's seq_item port) calls connect. Reversing it leaves a present-but-dead connection.
  • Responsibility follows structure: each component connects the children it owns; cross-child connections are the parent's job. A connection is established once at connect and exercised throughout the run, so a wiring bug (reversed, missing, or in the wrong phase) silently starves the consumer for the entire run.
  • The durable rule of thumb: wire in connect_phase after build, call connect() from the initiator's port to the provider's export, let each component connect what it owns and the parent connect across children — and verify data actually flows, because a connection made once governs the whole run.

Next — end_of_elaboration_phase: the tree is built and wired; the next phase is the last chance to adjust before simulation starts. end_of_elaboration_phase runs after connect, for final checks and tweaks once the whole structure exists and is connected — the bridge between elaboration and the run.