Build, connect and observe the phase order
Know which phase runs when, in which order it visits the tree, and which work belongs in each one.
- Lesson
- U1.2
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
An environment creates its agent and scoreboard in build_phase, then connects the monitor's analysis port to the scoreboard on the next line. The simulation stops at time 0 with Null pointer dereferenced. The agent object exists, but its own build_phase has not run yet, so agent.mon is still null.
Each column is one step of the top-down build. The monitor appears only when the agent's own build_phase runs.
A phase is a step every component takes
UVM splits a test into named steps called phases. Each phase is a method of uvm_component that you can override, such as build_phase or run_phase. The library calls that method on every component in the tree before it moves on to the next phase.
Most phases are functions, so they take no simulation time. Only the run-time phases, such as run_phase, are tasks that can wait for clocks. A #10 or @(posedge clk) inside build_phase does not compile at all, because a function cannot contain a timing control.
Each phase method receives a uvm_phase argument, conventionally named phase. It describes the phase being run, and in the run phase it is also the handle you use to raise and drop objections.
Because every component goes through the same steps, you never write the order by hand. You only decide which step each piece of work belongs to.
The order from start to finish
build_phase (top-down)
Each component creates its children and reads its configuration. A parent builds before its children.
kind functiontime 0connect_phase (bottom-up)
Ports are connected to their exports and imps. Children connect before their parent.
kind functiontime 0end_of_elaboration_phase (bottom-up)
The tree is complete. A good place to print it or check the structure.
kind functiontime 0start_of_simulation_phase (bottom-up)
Last zero-time setup before anything consumes time.
kind functiontime 0run_phase (all in parallel)
Every component's run_phase starts in the same time step. The run-time phases reset, configure, main and shutdown, each with pre and post steps, run alongside it.
kind tasktime passesextract_phase, check_phase, report_phase (bottom-up)
Collect results, decide pass or fail, print them.
kind functiontime frozen at the end of runfinal_phase (top-down)
Close files and finish. The last phase before the simulation ends.
kind functiontime frozen
The two short phases after connect are easy to skip, but they have distinct jobs. In end_of_elaboration_phase the tree and its connections are final, so it is the place to print the topology or check that a required component exists. In start_of_simulation_phase you can print the configuration a test is about to use, which makes a failing log easier to read later.
The run phase is the only stretch where simulation time moves. The phases after it all see the same time, the moment the run phase ended. [U1.3](U1-3-uvm-objection-lifetime.html) shows what decides that moment.
eoe is end_of_elaboration and sos is start_of_simulation. In this example the driver's run_phase only logs its name and returns at 0 ns; the test's objection alone keeps the phase open. Function phases take no time, and later phases see the time the run phase ended.
Why build goes down and connect goes up
Build runs top-down because a parent creates its children. The child objects do not exist until the parent's build_phase has run, so the parent must go first. Going first also lets a parent store configuration for a child before the child reads it.
Connect runs bottom-up, after the whole tree exists. By then every port and export has been created, so any connection is safe. Lower levels finish their own wiring first, so an agent can connect its internals before the environment uses them.
type_id::create calls made inside each parent's build_phase. The connect link is the hook's agent.mon.ap.connect(sb.imp), made by env. The order is the measured one from this lesson's example; everything happens at 0 ns.- The test builds first0 ns (illustrative)
UVM calls
build_phaseonuvm_test_top, which creates env.More context
createonly constructs the object. env has no children until its ownbuild_phaseruns.Follow the calls, values and waits
- call create("env"): uvm_test_top → env
- Waiting: env — exists; build_phase not run yet
- env builds: agent and sb exist0 ns (illustrative)
env's
build_phasecreates the agent and the scoreboard. Neither has built anything yet, soagent.monis still null.More context
This is where the opening bench connected. A connect written on the next line of env's
build_phasereadsagent.monand stops at time 0.Follow the calls, values and waits
- call create("agent"): env → agent
- call create("sb"): env → sb
- Waiting: agent — mon is null
- Waiting: sb — exists; build_phase not run yet
- Released: env
- The agent builds, then sb0 ns (illustrative)
The agent's
build_phasecreates the driver and the monitor. The scoreboard builds after the whole agent subtree.More context
The measured line is
build ... uvm_test_top env agent drv mon sb. Each child is built just after its parent, and uvm-core takes siblings in name order.Follow the calls, values and waits
- call create("drv"): agent → drv
- call create("mon"): agent → mon
- Released: agent
- Released: sb
- Connect from the leaves up0 ns (illustrative)
Now every component exists. connect_phase runs drv, mon, agent, sb, then env, so env links the monitor's port to the scoreboard after both are complete.
More context
The measured line is
connect ... drv mon agent sb env uvm_test_top. A connect link only records a route; nothing travels along it until the run phase.
Read the diagram as text
- uvm_test_top: phase_test
- env: Creates agent and sb
- agent: Creates drv and mon
- sb: Scoreboard
- drv: Driver
- mon: Monitor with port ap
- uvm_test_top → env: creates
- env → agent: creates
- env → sb: creates
- agent → drv: creates
- agent → mon: creates
- mon → sb: ap.connect(sb.imp)
The parent goes first in build because it creates its children. In connect, the parent goes last, after every child is complete.
The opening failure mixed the two jobs. The fix is to move the connection into connect_phase, where agent.mon is guaranteed to exist.
Connect inside build_phase
function void build_phase(uvm_phase phase);
agent = incr_agent::type_id::create("agent", this);
sb = incr_scoreboard::type_id::create("sb", this);
agent.mon.ap.connect(sb.imp); // agent.mon is null
endfunction
Stops at time 0: the agent has not built its monitor yet.
Build, then connect in connect_phase
function void build_phase(uvm_phase phase);
agent = incr_agent::type_id::create("agent", this);
sb = incr_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
agent.mon.ap.connect(sb.imp);
endfunction
By connect_phase the whole tree exists, so the port is there to connect.
Configuration flows down with the build
Top-down building has a useful side effect. A parent can store a setting for a child in uvm_config_db during its own build_phase, and the child reads it during its build_phase, which always runs later.
It does not even matter whether the parent stores the setting before or after it creates the child. Creating a child only allocates the object; the child's build_phase runs after the parent's has finished. By then every setting the parent made is in place.
The same reasoning tells you where a setting arrives too late. A value stored in the child's connect_phase or in the run phase is invisible to a get that already ran in build_phase. Store configuration for a component before or during its parent's build.
Run-phase tasks run side by side
When the run phase starts, UVM starts every component's run_phase task in the same time step. The driver loops forever, fetching items. The monitor loops forever, watching pins. The test starts a sequence and waits for it.
Nobody returns from a forever loop, so returning cannot be what ends the phase. Instead, the phase ends when no component objects any more, and UVM then kills any run_phase task still running. The next lesson covers those objections.
Twelve finer run-time phases run beside run_phase. The next section shows how they are scheduled.
The run-time schedule beside run_phase
run_phase for 10 ns, in reset_phase for 20 ns, in main_phase for 30 ns and in shutdown_phase for 10 ns.- Both branches start0 ns (illustrative)
run_phaseandpre_reset_phasestart in the same time step.pre_resethas no objection, so it ends at once andreset_phasestarts.More context
The two starts are independent: nothing in one branch waits for the other until extract. Every component's
run_phaseandreset_phasetasks are forked here.Follow the calls, values and waits
- call start run_phase: start_of_simulation → run_phase
- call start pre_reset: start_of_simulation → pre_reset .. post_reset
- Waiting: run_phase — test objects until 10 ns
- Waiting: pre_reset .. post_reset — reset objection until 20 ns
- run_phase has dropped, but waits10 ns (illustrative)
The test drops its
run_phaseobjection at 10 ns. The run phase does not end yet: it shares its successor, extract, with the schedule, and waits for it.More context
In the probe,
run_phasereported its drop at 10 ns but its end at 60 ns. A task phase with a shared successor stays open with no objection.Follow the calls, values and waits
- Waiting: run_phase — objections 0; waits for the schedule
- Released: run_phase
- Reset ends; main begins20 ns (illustrative)
The reset objection drops at 20 ns.
post_resetand the three configure phases have no objection, so they pass in zero time andmain_phasestarts at 20 ns.More context
Five phases start and end at 20 ns: post_reset, pre_configure, configure, post_configure and pre_main. Each ends in the step it starts because nobody objects in it.
Follow the calls, values and waits
- call start pre_configure: pre_reset .. post_reset → pre_configure .. post_main
- Waiting: pre_configure .. post_main — main objection until 50 ns
- Released: pre_reset .. post_reset
- Main ends; shutdown begins50 ns (illustrative)
The main objection drops at 50 ns.
post_mainandpre_shutdownpass in zero time, andshutdown_phaseholds until 60 ns.More context
The test objected in
main_phasefor 30 ns, from 20 to 50 ns.run_phaseis still waiting, 40 ns after its own drop.Follow the calls, values and waits
- call start pre_shutdown: pre_configure .. post_main → pre_shutdown .. post_shutdown
- Waiting: pre_shutdown .. post_shutdown — shutdown objection until 60 ns
- Released: pre_configure .. post_main
- Both branches end60 ns (illustrative)
The shutdown objection drops at 60 ns. The run phase and
post_shutdownboth end at 60 ns, and extract starts.More context
Extract needs both of its predecessors. The last one to finish, post_shutdown, decides the 60 ns;
run_phaseonly had to wait.Follow the calls, values and waits
- return post_shutdown ended: pre_shutdown .. post_shutdown → extract
- return run_phase ended: run_phase → extract
- Released: pre_shutdown .. post_shutdown
- Released: run_phase
Read the diagram as text
- start_of_simulation: Last zero-time step
- run_phase: Objection 0-10 ns
- pre_reset .. post_reset: reset objection 0-20 ns
- pre_configure .. post_main: configure takes 0 ns; main 20-50 ns
- pre_shutdown .. post_shutdown: shutdown objection 50-60 ns
- extract: Starts when both branches end
- start_of_simulation → run_phase: starts at 0
- start_of_simulation → pre_reset .. post_reset: starts at 0
- pre_reset .. post_reset → pre_configure .. post_main: ends at 20
- pre_configure .. post_main → pre_shutdown .. post_shutdown: ends at 50
- pre_shutdown .. post_shutdown → extract: ends at 60
- run_phase → extract: ends at 60
The run phase ends at 60 ns, although its own objection dropped at 10 ns.
The schedule is pre_reset, reset, post_reset, pre_configure, configure, post_configure, pre_main, main, post_main, pre_shutdown, shutdown and post_shutdown. Each is a task phase with its own objection. A phase with no objection ends in the time step it starts.
That is the trap. An objection raised in run_phase keeps run_phase open; it does not keep main_phase open. A test that raises in run_phase but sends its traffic from main_phase sees main end at 0 ns, with nothing sent yet.
Objection in the wrong phase
task run_phase(uvm_phase phase);
phase.raise_objection(this);
#100;
phase.drop_objection(this);
endtask
task main_phase(uvm_phase phase);
repeat (5) #10 sent++;
endtask
Measured: main ends at 0 ns with sent=0, so shutdown starts before any traffic has been sent.
Object in the phase doing the work
task main_phase(uvm_phase phase);
phase.raise_objection(this);
repeat (5) #10 sent++;
phase.drop_objection(this);
endtask
Main stays open until the fifth item at 50 ns.
Use the finer phases when the order matters to the whole bench: every agent finishes reset before any agent starts traffic. Many benches keep everything in run_phase and order it inside one sequence, which is simpler; these lessons do that. Do not mix the two styles in one component without a reason.
Jumping back to an earlier phase, and domains
phase.jump(uvm_pre_reset_phase::get()) ends the current run-time phase early and restarts the schedule from pre_reset. In a measured run, main_phase jumped at 25 ns: reset_phase ran a second time at 25 ns, main completed at 50 ns, and run_phase started only once. A jump only reschedules callbacks; your reset sequence, scoreboard flush and driver recovery still do the real work. [U10.2](U10-2-uvm-reset-accounting.html) builds that recovery.
All twelve phases belong to the uvm domain. A bench can put a group of components in their own domain so that their schedule can jump without moving the rest. Most benches never need more than the default.
What belongs in each phase
| Phase | Do here | Avoid here |
|---|---|---|
build_phase | Create children with the factory; get configuration | Connecting ports; anything that needs a child's children |
connect_phase | Connect ports to exports and imps; copy handles between components | Creating components |
end_of_elaboration_phase | Print the tree; check the structure is as expected | Changing the tree |
run_phase | Drive and monitor pins; start sequences; wait | Building or connecting |
extract_phase | Collect final values: counters, model state, coverage | Judging them: the next phase does that |
check_phase | Fail on leftover expected items and wrong counts | Waiting: time has stopped |
report_phase | Print results and summaries | Deciding pass or fail for the first time |
final_phase | Close files and release resources | New checks: reports after this point are easy to miss |
A working rule for each phase.
Call super.build_phase(phase) first when you override build_phase. The base version can apply configuration to fields registered with the UVM field macros. Skipping it silently loses those settings.
Try it
Every component class here extends traced, a small helper that appends the component's name to a per-phase list. When a phase has finished everywhere, the test's phase_ended callback prints that list with the current time.
Check the order against the steps above: build goes from the test down, connect and the cleanup phases come up from the leaves, and final goes down again. Move the #30 in the test to #50 and predict which lines change.
build ends at 0 ns: uvm_test_top env agent drv mon sb connect ends at 0 ns: drv mon agent sb env uvm_test_top end_of_elaboration ends at 0 ns: drv mon agent sb env uvm_test_top start_of_simulation ends at 0 ns: drv mon agent sb env uvm_test_top run ends at 30 ns: drv mon agent sb env uvm_test_top extract ends at 30 ns: drv mon agent sb env uvm_test_top check ends at 30 ns: drv mon agent sb env uvm_test_top report ends at 30 ns: drv mon agent sb env uvm_test_top final ends at 30 ns: uvm_test_top env agent drv mon sbCheck yourself
Remember
- Each of the twelve run-time phases has its own objection; an objection in
run_phasedoes not holdmain_phaseopen, so raise it in the phase that does the work. - Build runs top-down because parents create children; connect and the cleanup phases run bottom-up; final runs top-down.
- Only run-time phases are tasks and consume time; a timing control in a function phase does not compile.
- Connect ports in
connect_phase, when every component in the tree exists. - Rely on parent and child order only, never on the order of siblings.