You are integrating a scoreboard into a testbench that has no phases: each class wires itself in its constructor. The scoreboard needs a handle to the APB monitor's output, but the monitor is created three lines later, inside an agent that someone else owns. You reorder the top-level script, and now the stream agent breaks because it needed the scoreboard first. Two days later you have a 300-line script that only one person dares to touch.
UVM's answer is to stop letting each component pick its own moment. Every component gets the same named callbacks, and the library calls them in one shared order. This lesson shows what that order buys you on the DMA bench, and what happens when one line of code is in the wrong callback.
Think of a building site. The framers put up every wall on every floor first. Only then does the electrician run cables between rooms, because a cable needs a wall at both ends. Power goes on after the wiring is inspected, and the building is signed off only when the work has actually stopped, not when a clock says it should have.
A UVM testbench follows the same rule. A phase is one step of that shared schedule, and a phase method (such as build_phase or connect_phase) is the callback a component overrides to do its part of that step. The phaser runs one phase on every component before it starts the next. That gives four guarantees no single author could give alone:
Question every integrator asks | Phase that answers it | Guarantee |
|---|---|---|
Does the thing I want to connect to exist yet? |
| Every component in the tree is built before any component connects. |
Is the wiring complete and legal? |
| All connections are resolved and checked before time 0 ends. |
When may I drive pins and start sequences? |
| Every component's run thread starts together, after setup is done. |
Is the test finished, or just quiet? | objections in | Run ends when every component that is busy says it is done. |
The first row is the one integration bugs come from. A component never needs to know who else is in the tree or in what order they were created. It only needs to know that by the time its connect_phase runs, every build_phase in the whole tree has returned.
The arrows between the boxes are barriers: nothing on the right starts until everything on the left has finished, across the whole tree. Inside a box the order follows the tree, parents first for build and children first for connect. The next lessons explain both directions; for now the barrier is what matters.
Here is the env of the course bench. Its build_phase only creates children and leaves one configuration note for the output agent. Its connect_phase only wires. It never creates in connect, and never wires in build.
// snippet: from the course DMA bench
class dma_env extends uvm_env;
`uvm_component_utils(dma_env)
apb_agent apb;
axis_agent in_agt, out_agt;
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The output side is observed only: the DUT drives it.
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
endfunction
endclassapb.ap is the APB agent's public analysis port. The agent sets it in its own connect_phase (ap = mon.ap), which is why the env can only use it in connect. Traversal Order Rules explains why the agent's connect is guaranteed to run first.
sb.apb_export and its siblings are created in the scoreboard's constructor, so they exist the moment create returns. The env does not depend on that: by connect time, everything exists anyway.
The config_db note for out_agt is set before the agent is created. That is fine but not required. The agent reads it in its own build_phase, which runs after the env's build returns.
The base test prints the component tree in end_of_elaboration_phase, after every build and connect has finished. This is the start of the smoke test's log (ports and sequencer fields trimmed):
UVM_INFO @ 0: reporter [RNTST] Running test dma_smoke_test...
UVM_INFO uvm_root.svh(531) @ 0: reporter [UVMTOP] UVM testbench topology:
----------------------------------------------------------------
Name Type Size Value
----------------------------------------------------------------
<unnamed> uvm_root - @68
uvm_test_top dma_smoke_test - @172
env dma_env - @242
apb apb_agent - @256
drv apb_driver - @458
mon apb_monitor - @318
sqr uvm_sequencer - @335
in_agt axis_agent - @264
...
out_agt axis_agent - @272
mon axis_monitor - @674
sb dma_scoreboard - @280Every line is stamped @ 0. All of build, connect and elaboration happen before simulated time moves.
out_agt has only a monitor: the passive setting the env left in config_db was read during the agent's build.
The whole tree exists at this point, so this is the right moment to audit it. Elaboration, Start and the Pre-Time-Zero Contract covers what else belongs here.
The Env Owns the Infrastructure opened with a colleague moving dma_env's three connect calls into build_phase, and the run dying at time 0 on apb.ap.connect(sb.apb_export). Now you can name the exact mechanism.
The root cause is the barrier. create constructed the agent, but nothing inside it has run yet: its build_phase (which creates the monitor) and its connect_phase (which sets ap = mon.ap) both come later. apb.ap is a handle that nobody has assigned. The fix is to put the line back in connect_phase, where every build in the tree has finished and every child has forwarded its ports.
Notice that the scoreboard side would have worked: its exports are built in its constructor. Code that works for one endpoint and fails for the other is the classic sign of wiring in the wrong phase. The rule "create in build, wire in connect" removes the question entirely.
Code that depends on creation order: some component wired or read a handle during build, so adding a sibling changed what existed at that moment. With all creates in build_phase and all wiring in connect_phase, adding a component cannot change what exists when connect runs, because every build has finished by then.
dma_env::connect_phase, why is sb.apb_export safe to use even though the scoreboard's own connect_phase may not have run yet?The export is created in the scoreboard's constructor, and connect only needs the object to exist. The env's connect does not depend on anything the scoreboard does in its connect. What it does depend on is the agent's connect (ap = mon.ap), and bottom-up order guarantees that one ran first.
apb.ap is assigned in the APB agent's connect_phase, and the monitor behind it is created in the agent's build_phase. Neither has run when the env's build creates the agent, so apb.ap.connect(...) in build dereferences a null handle. Creating a child does not run any of its phases.
Next: the barriers above are only part of the schedule. In The Phase Schedule you will see all twenty-one phases, including twelve that run alongside run_phase.