UVM
Component Relationships
The four ways components relate — ownership, communication, configuration, and coordination — each with its mechanism and phase, and the virtual-sequencer peer-reference pattern.
UVM Base Classes · Module 4 · Page 4.7
The Engineering Problem
You now know the base classes — uvm_object, uvm_component, and the data lineage. The last question of this module is how components relate to each other. Because a testbench isn't a pile of independent components; it's a web of relationships, and a given pair of components can be related in several different ways at once. The env owns its agent (parent–child), the monitor sends transactions to the scoreboard (communication), the test configures the agent (configuration), and a virtual sequencer coordinates the agents' sequencers (coordination by reference).
The engineering point is that these are different kinds of relationship, each with its own correct mechanism and its own correct phase — and mixing them up is a rich source of bugs. Ownership is established by create(name, parent) in build_phase; communication by connect() in connect_phase; configuration by the config database; coordination by handing one component a handle to another, after both exist. Establish a relationship the wrong way (a hard-coded reach across the hierarchy) or in the wrong phase (a peer handle grabbed before the target is built) and you get broken reuse or null-handle crashes. This capstone of Module 4 maps the relationship types so you establish each correctly.
In how many ways can two UVM components relate, what is the correct mechanism and phase for each, and why does establishing a relationship the wrong way (or in the wrong phase) break the environment?
Motivation — why naming the relationship types matters
Treating "component relationships" as one thing hides the distinctions that make a testbench correct:
- Each relationship has a correct mechanism — using the wrong one fails. You cannot establish data flow with
create, or ownership withconnect. Knowing that ownership =create, communication =connect, configuration = config_db, and coordination = a handle assignment is what lets you build each relationship the way the framework expects. - Each has a correct phase — using the wrong one is a null bug. Build happens before connect (Module 2.6). Ownership is established in
build_phase(you create children there); communication and coordination inconnect_phase(after children exist). Grab a peer's handle inbuild_phase, before that peer is built, and the handle is null — a crash waiting to happen. - Coordination is the relationship newcomers miss. Parent–child, TLM, and config are familiar; the peer reference — one component holding a handle to a non-child component to coordinate it (the virtual sequencer pattern) — is less obvious and is how cross-agent stimulus is orchestrated. Naming it makes the pattern visible.
- Establishing relationships properly is what keeps reuse intact. A relationship built by a hard-coded absolute path (reaching across the hierarchy) couples components to one structure and breaks under nesting (Module 3.5). Using the proper mechanism — config and connect, scoped relative to
this— keeps components position-independent.
The motivation, in one line: two components can be related in four distinct ways, and each way has a specific correct mechanism and phase — so the skill is recognising which relationship you're establishing and building it the right way, in the right phase.
Mental Model
Hold the four relationships as four kinds of line you can draw between components:
Between any two components you can draw four kinds of line, and each is drawn with a different pen, at a different time. An ownership line (solid) goes from a parent down to a child — drawn at construction time with
create(name, parent); it says "I built and contain you." A communication line (arrow) goes from a producer to a consumer — drawn at connect time withconnect(); it says "I send you transactions." A configuration line (dotted, top-down) goes from a configurer to a configured component — drawn via the config database; it says "I set your parameters from above." And a coordination line (peer) goes sideways between two components that don't own each other — drawn at connect time by handing one a handle to the other; it says "I hold a reference to you so I can orchestrate you" (the virtual sequencer holding agent sequencers). The pen (mechanism) and the timing (phase) are fixed per line type: ownership at build, communication and coordination at connect, configuration top-down before the child reads it.
So for any relationship you need, ask: which of the four is this? — and that tells you the mechanism (create / connect / config_db / handle assignment) and the phase (build / connect). Drawing the line with the wrong pen or at the wrong time is the bug.
Visual Explanation — the four relationship types
The four relationships differ along two axes: their mechanism (how you establish them) and their phase (when). Laying them out makes the correct choice obvious for any relationship you need.
The four rows are a decision table. Ownership is the structural backbone — a parent constructs and contains its children, established by passing the parent to create during build_phase. Communication is data flow — TLM ports connected in connect_phase (Module 3.4). Configuration is top-down parameterisation — the config database, set by an ancestor and read by the component in its build_phase (Module 3.5). Coordination is the peer relationship — one component holding a handle to another it doesn't own, so it can orchestrate it, established by assigning that handle in connect_phase once both components have been built. Each row pairs a relationship with exactly one mechanism and one phase, and matching them correctly is the whole discipline of wiring an environment.
RTL / Simulation Perspective — the four relationships in code
A single env establishes all four relationships, and the code shows each with its mechanism in its phase — ownership and config in build_phase, communication and coordination in connect_phase.
class my_env extends uvm_env;
`uvm_component_utils(my_env)
agent a, b; my_vseqr vseqr; scoreboard sb;
function void build_phase(uvm_phase phase);
// OWNERSHIP: parent → child, via create(name, parent)
a = agent ::type_id::create("a", this);
b = agent ::type_id::create("b", this);
vseqr = my_vseqr ::type_id::create("vseqr", this);
sb = scoreboard::type_id::create("sb", this);
// CONFIGURATION: configurer → configured, via the config database (top-down)
uvm_config_db#(int)::set(this, "a", "mode", FAST);
endfunction
function void connect_phase(uvm_phase phase);
// COMMUNICATION: producer → consumer, via connect() of TLM ports
a.mon.ap.connect(sb.analysis_export);
b.mon.ap.connect(sb.analysis_export);
// COORDINATION: peer → peer, via a handle assignment — AFTER both are built
vseqr.sqr_a = a.sqr; // the virtual sequencer holds the agents' sequencers
vseqr.sqr_b = b.sqr;
endfunction
endclassEach relationship appears with its correct mechanism and phase. Ownership (create("a", this)) and configuration (config_db::set) are in build_phase — ownership because that's when children are constructed, configuration because the child reads it in its build_phase (which runs after this one). Communication (a.mon.ap.connect(sb...)) is in connect_phase, wiring the monitor's analysis port to the scoreboard. Coordination (vseqr.sqr_a = a.sqr) is also in connect_phase — and it must be, because a.sqr (agent a's sequencer) only exists after agent a's own build_phase has run, which is after this env's build_phase. Assigning that handle in build_phase would grab a null. The phase isn't incidental: it's dictated by when each related component comes into existence.
Verification Perspective — coordination, the relationship by reference
Three of the four relationships are familiar (ownership, communication, configuration); the fourth — coordination — is the one that enables cross-component orchestration, and it works by reference. The classic case is the virtual sequencer holding handles to several agents' sequencers, so a virtual sequence can drive all of them in a coordinated order.
Coordination is distinct from the other three because it crosses between subtrees without ownership. The virtual sequencer does not contain the agents' sequencers (they're owned by their agents), and it does not connect to them via TLM — it simply holds handles to them, assigned in connect_phase. A virtual sequence then runs on the virtual sequencer and uses those handles to start sub-sequences on each agent's sequencer, in whatever coordinated order the scenario needs ("write on agent A, then read on agent B"). This is the relationship that orchestrates multi-interface stimulus, and it's why an SoC environment can coordinate its block agents. The handle is the relationship; the phase (connect_phase, after build) is when the targets exist to point at; and the discipline is to assign these handles through proper navigation/config rather than hard-coded absolute paths, so the coordination survives reuse.
Runtime / Execution Flow — relationships are established before they're used
All four relationships are established during the zero-time construction phases and used during the run — and the ordering (build before connect, both before run) is exactly what makes the relationships valid when used.
The ordering is what makes the relationships sound. Because build_phase runs (top-down) before connect_phase, every component in the tree exists by the time connections and coordination handles are wired — so a TLM connect finds its target, and a peer handle points at a real sequencer. This is precisely why communication and coordination belong in connect_phase and not build_phase: in build_phase, the components you'd connect or reference may not have been built yet (a child's build_phase runs after its parent's). And it's why all four relationships are used only in run_phase — by then the structure is complete and wired. The construction phases establish the web of relationships; the run phase exercises it; and the build-before-connect order is the guarantee that no relationship is wired to something that doesn't exist yet.
Waveform Perspective — coordination orchestrating two agents
The coordination relationship is visible at run time as cross-agent ordering: a virtual sequence uses its handles to drive agent A, then agent B, in a coordinated sequence — two interfaces orchestrated by one virtual sequence holding both sequencers.
A virtual sequence coordinates two agents — drive A, then B, in order
10 cyclesThe staggered activity — agent A (cycles 1–2) then agent B (cycles 4–5) — is the coordination relationship producing a coordinated order across two independent interfaces. Neither agent owns the other, and neither is connected to the other by TLM; the only thing linking them is the virtual sequencer holding a handle to each sequencer, and a virtual sequence using those handles to drive them in sequence. This is the relationship by reference at work: it lets stimulus span multiple interfaces in a controlled order, which is exactly what SoC-level verification needs (coordinate the protocol between blocks). The waveform shows why coordination is its own relationship type — it produces cross-component behaviour that ownership, communication, and configuration cannot, and it does so purely through held handles assigned once the targets exist.
DebugLab — the virtual sequencer handle that was null
A virtual sequence crashed on a null sequencer — the handle was assigned too early
A virtual sequence tried to start a sub-sequence on agent A's sequencer through the virtual sequencer, and the simulation crashed with a null object access — the virtual sequencer's sqr_a handle was null. Yet agent A clearly existed in the tree (it appeared in print_topology()), its sequencer was built, and the virtual sequencer was built too. Everything was present, but the reference between them was empty.
The coordination handle was assigned in the wrong phase — in build_phase, before agent A's sequencer existed. Because build_phase runs top-down, the env's build_phase (which assigned the handle) ran before agent A's build_phase (which creates the sequencer):
env.build_phase: a = agent::create("a", this); // agent created, but its build_phase NOT run yet
vseqr.sqr_a = a.sqr; // a.sqr is still NULL here ← BUG
a.build_phase: sqr = sequencer::create("sqr",this); // sequencer created NOW (after env.build_phase)
result: vseqr.sqr_a captured the null → virtual sequence crashes using it
fix: assign vseqr.sqr_a = a.sqr in CONNECT_PHASE (after all build_phases complete)The agent handle a was valid (it was created), but a.sqr — the agent's child sequencer — is created inside agent a's own build_phase, which runs after the env's build_phase returns. So at the moment the env grabbed a.sqr, it was still null. Coordination handles must be assigned in connect_phase, after every component's build_phase has completed and all sequencers exist.
The tell is a null handle to a component that demonstrably exists — a relationship established before its target was built. Diagnose phase-ordering relationship bugs:
- Check the phase of the handle assignment. Peer/coordination handles (and TLM connections) must be set in
connect_phase, notbuild_phase— because a child's sequencer/ports don't exist until itsbuild_phaseruns, which is after its parent's. A null peer handle is almost always an assignment inbuild_phase. - Confirm the target is a grandchild, not a direct field.
a.sqris built insidea's build, one level deeper; the env's build can't see it yet. Anything reached through a child (child.grandchild) isn't available until connect. - Distinguish null reference from missing component.
print_topology()shows the component exists (so it's not an orphan); the null is in the reference, pointing to a relationship wired too early.
Establish each relationship in its correct phase, dictated by when its targets exist:
- Coordination handles and TLM connections go in
connect_phase. By then everybuild_phasehas run and all components (including grandchildren like agent sequencers) exist. Never assign a peer handle inbuild_phase. - Ownership and config go in
build_phase. Create children and set their configuration there; the config is read by the child in its own (later)build_phase. - Reach peers through proper navigation, not absolute paths. Assign coordination handles via the component structure (
a.sqr) in connect, not via hard-coded hierarchical strings — so the relationship survives nesting and reuse (Module 3.5).
The one-sentence lesson: a coordination handle points at a component that may be built later than its referrer — assign peer handles (and TLM connections) in connect_phase, after every build_phase has run, or the handle captures a null.
Common Mistakes
- Assigning peer handles or TLM connections in
build_phase. A child's sub-components (e.g., an agent's sequencer) don't exist until the child's ownbuild_phaseruns, which is after its parent's. Establish communication and coordination inconnect_phase, after build. - Using the wrong mechanism for a relationship. Ownership is
create, communication isconnect, configuration is config_db, coordination is a handle assignment. Trying to establish data flow withcreate, or coordination withconnect, mismatches the relationship to its mechanism. - Hard-coding cross-hierarchy references. Reaching a peer by an absolute hierarchical path couples components to one structure and breaks reuse (Module 3.5). Establish coordination through the component structure or config, scoped relative to
this. - Confusing ownership with the other relationships. Containment (who builds whom) is not data flow (who sends to whom) or coordination (who references whom). The monitor lives in the agent (ownership) but sends to the scoreboard (communication) — different relationships over the same components.
- Forgetting coordination exists. Newcomers wire ownership, TLM, and config but miss the peer-reference relationship, then struggle to orchestrate across agents. The virtual sequencer holding sequencer handles is how cross-agent stimulus is coordinated.
- Reading config in the wrong phase. Configuration is set top-down and must be read in the configured component's
build_phase; reading it later (or before it's set) misses it.
Senior Design Review Notes
Interview Insights
In four distinct ways, each with its own mechanism and phase. First, ownership — the parent–child containment relationship, established by create(name, parent) in build_phase; the env owns the agent, the agent owns its sequencer/driver/monitor. Second, communication — the producer–consumer data relationship, established by connect()-ing TLM ports in connect_phase; the monitor sends transactions to the scoreboard. Third, configuration — the configurer–configured relationship, established through the config database, set top-down by an ancestor and read by the component in its own build_phase; the test configures the agent's mode and interface. Fourth, coordination — the peer-to-peer reference relationship, established by handing one component a handle to another it doesn't own, assigned in connect_phase after both exist; a virtual sequencer holds handles to several agents' sequencers so a virtual sequence can orchestrate them. The same two components can be related in several of these ways at once, and the skill is recognising which relationship you're establishing so you use the right mechanism in the right phase — ownership and config in build, communication and coordination in connect.
Exercises
- Name the four. List the four component relationships, and for each give its mechanism and the phase it's established in.
- Fix the phase. A virtual sequencer assigns
vseqr.sqr_a = a.sqrinbuild_phaseand the handle is null at run time. State the root cause in phase-ordering terms and the corrected phase, and explain whya.sqris null in build. - Match relationship to mechanism. For each, name the relationship and mechanism: (a) the env builds an agent; (b) the monitor feeds the scoreboard; (c) the test sets the agent's mode; (d) a virtual sequence drives two agents' sequencers.
- Coordinate two agents. Describe how you'd set up a virtual sequence to write on one agent then read on another: which handles the virtual sequencer holds, where they're assigned, and why that phase.
Summary
- Two UVM components can relate in four distinct ways, each with its own mechanism and phase: ownership (parent–child,
createinbuild_phase), communication (producer→consumer,connectTLM ports inconnect_phase), configuration (configurer→configured, config database, set top-down, read inbuild_phase), and coordination (peer→peer, a handle assignment inconnect_phase). - The phase is dictated by when targets exist: ownership and config in
build_phase; communication and coordination inconnect_phase, after everybuild_phasehas run so all components (including grandchildren like agent sequencers) exist. - Coordination is the relationship newcomers miss: one component holding a handle to a peer it doesn't own — the virtual sequencer holding agent sequencers — which is how cross-agent stimulus is orchestrated by a virtual sequence.
- The classic bug is a peer handle (or TLM connection) established in
build_phase— the target sub-component isn't built yet, so the handle captures a null. And hard-coding cross-hierarchy paths breaks reuse; wire through the structure/config instead. - The durable rule of thumb: recognise which of the four relationships you're establishing, use its mechanism (
create/connect/config_db/handle), and establish it in its phase (ownership+config in build, communication+coordination in connect) — relationships are wired during zero-time construction and used at run, and the build-before-connect order guarantees every target exists before it's referenced.
Next — UVM Phasing Overview: you've repeatedly relied on the phases — build before connect, run after both — to establish and use relationships. Module 5 makes phasing itself the subject: the complete phase schedule, what each phase is for, and how the framework orchestrates the whole tree of components through it.