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.

The build phase, one component at a time
01234buildingtestenvagentenv.agentnullobjectenv.agent.monnullobjectenv connects here: mon is 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

  1. build_phase (top-down)

    Each component creates its children and reads its configuration. A parent builds before its children.

    kindfunction
    time0
  2. connect_phase (bottom-up)

    Ports are connected to their exports and imps. Children connect before their parent.

    kindfunction
    time0
  3. end_of_elaboration_phase (bottom-up)

    The tree is complete. A good place to print it or check the structure.

    kindfunction
    time0
  4. start_of_simulation_phase (bottom-up)

    Last zero-time setup before anything consumes time.

    kindfunction
    time0
  5. run_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.

    kindtask
    timepasses
  6. extract_phase, check_phase, report_phase (bottom-up)

    Collect results, decide pass or fail, print them.

    kindfunction
    timefrozen at the end of run
  7. final_phase (top-down)

    Close files and finish. The last phase before the simulation ends.

    kindfunction
    timefrozen

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.

Phase timeline of this lesson's example
01234567891011phasebuildconnecteoesosrunextractcheckreportfinaltime (ns)0102030objectionstest run_phase-raise#30drop-drv run_phase-returns-all run_phase tasks startlast drop: run ends at 30

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.

Build goes down the tree, connect comes back upThe creates arrows are 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.
connect() link
Build goes down the tree, connect comes back upThe creates arrows are `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. component uvm_test_top phase_test component env Creates agent and sb component agent Creates drv and mon component sb Scoreboard component drv Driver component mon Monitor with port ap createscreatescreatescreatescreatesap.connect(sb.imp)
  1. The test builds first0 ns (illustrative)

    UVM calls build_phase on uvm_test_top, which creates env.

    More context

    create only constructs the object. env has no children until its own build_phase runs.

    Follow the calls, values and waits
    1. call create("env"): uvm_test_top → env
    2. Waiting: env — exists; build_phase not run yet
  2. env builds: agent and sb exist0 ns (illustrative)

    env's build_phase creates the agent and the scoreboard. Neither has built anything yet, so agent.mon is still null.

    More context

    This is where the opening bench connected. A connect written on the next line of env's build_phase reads agent.mon and stops at time 0.

    Follow the calls, values and waits
    1. call create("agent"): env → agent
    2. call create("sb"): env → sb
    3. Waiting: agent — mon is null
    4. Waiting: sb — exists; build_phase not run yet
    5. Released: env
  3. The agent builds, then sb0 ns (illustrative)

    The agent's build_phase creates 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
    1. call create("drv"): agent → drv
    2. call create("mon"): agent → mon
    3. Released: agent
    4. Released: sb
  4. 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

Two branches between start of simulation and extractArrows show which phase starts after which. Times come from a measured run in which the test objects in run_phase for 10 ns, in reset_phase for 20 ns, in main_phase for 30 ns and in shutdown_phase for 10 ns.
Two branches between start of simulation and extractArrows show which phase starts after which. Times come from a measured run in which the test objects in `run_phase` for 10 ns, in `reset_phase` for 20 ns, in `main_phase` for 30 ns and in `shutdown_phase` for 10 ns. component start_of_simulation Last zero-time step component run_phase Objection 0-10 ns component pre_reset ..post_reset reset objection 0-20ns component pre_configure ..post_main configure takes 0 ns;main 20-50 ns component pre_shutdown ..post_shutdown shutdown objection50-60 ns component extract Starts when bothbranches end starts at 0starts at 0ends at 20ends at 50ends at 60ends at 60
  1. Both branches start0 ns (illustrative)

    run_phase and pre_reset_phase start in the same time step. pre_reset has no objection, so it ends at once and reset_phase starts.

    More context

    The two starts are independent: nothing in one branch waits for the other until extract. Every component's run_phase and reset_phase tasks are forked here.

    Follow the calls, values and waits
    1. call start run_phase: start_of_simulation → run_phase
    2. call start pre_reset: start_of_simulation → pre_reset .. post_reset
    3. Waiting: run_phase — test objects until 10 ns
    4. Waiting: pre_reset .. post_reset — reset objection until 20 ns
  2. run_phase has dropped, but waits10 ns (illustrative)

    The test drops its run_phase objection 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_phase reported 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
    1. Waiting: run_phase — objections 0; waits for the schedule
    2. Released: run_phase
  3. Reset ends; main begins20 ns (illustrative)

    The reset objection drops at 20 ns. post_reset and the three configure phases have no objection, so they pass in zero time and main_phase starts 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
    1. call start pre_configure: pre_reset .. post_reset → pre_configure .. post_main
    2. Waiting: pre_configure .. post_main — main objection until 50 ns
    3. Released: pre_reset .. post_reset
  4. Main ends; shutdown begins50 ns (illustrative)

    The main objection drops at 50 ns. post_main and pre_shutdown pass in zero time, and shutdown_phase holds until 60 ns.

    More context

    The test objected in main_phase for 30 ns, from 20 to 50 ns. run_phase is still waiting, 40 ns after its own drop.

    Follow the calls, values and waits
    1. call start pre_shutdown: pre_configure .. post_main → pre_shutdown .. post_shutdown
    2. Waiting: pre_shutdown .. post_shutdown — shutdown objection until 60 ns
    3. Released: pre_configure .. post_main
  5. Both branches end60 ns (illustrative)

    The shutdown objection drops at 60 ns. The run phase and post_shutdown both 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_phase only had to wait.

    Follow the calls, values and waits
    1. return post_shutdown ended: pre_shutdown .. post_shutdown → extract
    2. return run_phase ended: run_phase → extract
    3. Released: pre_shutdown .. post_shutdown
    4. 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

PhaseDo hereAvoid here
build_phaseCreate children with the factory; get configurationConnecting ports; anything that needs a child's children
connect_phaseConnect ports to exports and imps; copy handles between componentsCreating components
end_of_elaboration_phasePrint the tree; check the structure is as expectedChanging the tree
run_phaseDrive and monitor pins; start sequences; waitBuilding or connecting
extract_phaseCollect final values: counters, model state, coverageJudging them: the next phase does that
check_phaseFail on leftover expected items and wrong countsWaiting: time has stopped
report_phasePrint results and summariesDeciding pass or fail for the first time
final_phaseClose files and release resourcesNew 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 sb

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`include "uvm_macros.svh"
import uvm_pkg::*;
string log_s;
class agent_c extends uvm_agent;
  `uvm_component_utils(agent_c)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);   log_s = {log_s, " B:agent"}; endfunction
  function void connect_phase(uvm_phase phase); log_s = {log_s, " C:agent"}; endfunction
endclass
class env_c extends uvm_env;
  `uvm_component_utils(env_c)
  agent_c agent;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    log_s = {log_s, " B:env"};
    agent = agent_c::type_id::create("agent", this);
  endfunction
  function void connect_phase(uvm_phase phase); log_s = {log_s, " C:env"}; endfunction
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  env_c env;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    log_s = {log_s, "B:test"};
    env = env_c::type_id::create("env", this);
  endfunction
  function void connect_phase(uvm_phase phase); log_s = {log_s, " C:test"}; endfunction
  function void start_of_simulation_phase(uvm_phase phase);
    $display("%s", log_s);
  endfunction
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugEach option is the body of an environment class whose agent builds a monitor mon with an analysis port ap. Which one fails?
ConceptWhere can a driver wait for a clock edge with @(posedge vif.clk)?
Predict the outputThe test objects in run_phase for 100 ns and sends five items from main_phase, one every 10 ns, without an objection there. What line does report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class main_test extends uvm_test;
  `uvm_component_utils(main_test)
  int  sent, sent_at_main_end;
  time main_end;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  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
  function void phase_ended(uvm_phase phase);
    if (phase.get_name() == "main") begin
      main_end = $time;
      sent_at_main_end = sent;
    end
  endfunction
  function void report_phase(uvm_phase phase);
    $display("main ended at %0t ns with sent=%0d", main_end, sent_at_main_end);
  endfunction
endclass
module tb;
  initial run_test("main_test");
endmodule
ConceptAn environment creates scoreboard, then agent, in its build_phase. What can you rely on about the order of their connect_phase calls?
Read the waveformThis timeline is from the example. What simulation time does check_phase see?
Phases and time
01234567phasebuildconnectruncheckfinalobjectionsrun starts at 0last drop at 30

The test raises an objection at 0 and drops it at 30.

Remember

  • Each of the twelve run-time phases has its own objection; an objection in run_phase does not hold main_phase open, 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.