Part 2 · Phases & Lifecycle · Intermediate

Scheduler vs User Code: Who Calls Whom

Clear boundaries between library scheduler behavior and user-implemented phase callbacks — what you must never do and what the library guarantees.

The golden rule

The scheduler calls your callbacks . You never call phase methods on other components to 'force' ordering. Coordination flows through phase placement, config_db, TLM, and objections.

diagram
[UVM][PHASE] DO / DON'T

DO:
  implement build/connect/run/check callbacks
  use config_db for hierarchical data distribution
  use objections for run completion
  use factory overrides before create

DON'T:
  env.build_phase calling agent.connect_phase()
  'wait for neighbor build' busy loops
  module initial ordering UVM component phases
  $finish in run_phase to end test

Key takeaways

  • Neighbor phase calls are always a methodology smell.

  • config_db + factory + objections replace integrator scripts.

  • Simulation end is objection-driven, not $finish-driven.

Common pitfalls

  • Exporting 'init()' methods on VIPs that duplicate build_phase.

  • Top module initial blocks that create UVM components after run_test.

  • Using global singletons to bypass config_db phase timing.


How user code hooks into the scheduler

Test entry

systemverilog
module tb_top;
  initial begin
    uvm_config_db#(virtual dut_if)::set(null, "uvm_test_top.*", "vif", dut_if_i);
    run_test("my_test"); // scheduler takes over from here
  end
endmodule

Component callbacks

systemverilog
class my_test extends uvm_test;
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
  endfunction

  task run_phase(uvm_phase phase);
    super.run_phase(phase);
    phase.raise_objection(this, "test");
    env.v_sqr.run_scenario();
    phase.drop_objection(this, "test");
  endfunction
endclass

Library guarantees you rely on

  1. When your build_phase runs, parent's build already ran (top-down).

  2. When your connect_phase runs, all build_phase finished tree-wide.

  3. When your run_phase starts, build-time phases completed globally.

  4. When extract starts, run-time quiescence achieved per objection rules.

diagram
[PHASE] if you need X, use Y

need neighbor to exist:
  -> put create in earlier phase (build), not neighbor call

need neighbor configured:
  -> config_db set before their build, not direct field poke

need stimulus after reset:
  -> runtime sub-phase or run_phase wait, not build delay

need test to end:
  -> drop objections, not $finish

Key takeaways

  • run_test() hands control to the scheduler — respect that boundary.

  • Every workaround table entry maps to a first-class UVM mechanism.

  • If you need neighbor phase calls, the architecture needs redesign.

Common pitfalls

  • Calling run_test() multiple times in one simulation.

  • Mixing UVM phases with OOP constructors for TB bring-up ordering.

  • Using uvm_domain jump as a substitute for correct phase placement.