Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
    • The Phase System
      • Why Phases Exist
      • The Phase Schedule: Common and Runtime Phases
      • Traversal Order Rules
      • Function Phases vs Task Phases
      • Signatures, Super Calls, and Debug
      • Inside the Phaser: Nodes, Imps and Schedules
    • UVM Phases: Build, Connect, Elaborate and Run
    • Objections Deep Dive: Drain Time and Propagation
    • Phase Debugging and End-of-Test Signoff
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹The Phase System
  1. Topics
  2. UVM Verification
  3. UVM Phases & Simulation Lifecycle
  4. The Phase System
  5. Inside the Phaser: Nodes, Imps and Schedules

Part 2 · Phases & Lifecycle · Intermediate

Inside the Phaser: Nodes, Imps and Schedules

Phase nodes versus imp singletons, phase states, the phase callbacks, and how run_test drives the phase graph.

On this page

    The DMA bench's end-of-test scoreboard starts its phase_ready_to_end with if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;. In a review, someone suggests shortening the first test to phase != uvm_run_phase::get(), "since phase already is the run phase". It compiles. The drain test still passes, with one word never checked. To see why, you need to know that there are two different kinds of uvm_phase object.

    The idea: the song and the slot on the festival line-up

    A festival line-up lists slots: "Stage A, 9 pm". Each slot has its own state (waiting, playing, done) and its own crowd. The song played in that slot exists independently of any slot, and the same song can be played in two slots on two stages. UVM's phases are built the same way.

    • An imp is the song: one singleton object per phase type, returned by uvm_run_phase::get(), uvm_main_phase::get() and so on. It knows what to call on a component (run_phase, main_phase) and in which walk direction. It has no state and no objection.

    • A node is the slot: one object per place in a schedule. It has a state, predecessors and successors, an objection count, and a link to its imp. The run phase of the common domain is one node; if you create a second domain, its main is a different node pointing at the same uvm_main_phase imp.

    Every phase argument the phaser passes to your code (to run_phase, phase_started, phase_ready_to_end) is a node. uvm_run_phase::get() returns the imp. They are different objects, so phase == uvm_run_phase::get() is never true. To ask "is this the run phase?", compare the node's imp, phase.get_imp() == uvm_run_phase::get(), or its name, phase.get_name() == "run", or use phase.is(uvm_run_phase::get()), which checks both.

    The picture: the life of one phase node

    State diagram of a UVM phase node: dormant, scheduled, syncing, started, executing, ready to end, ended, cleanup, done, with loops for re-raised objections and jumps.
    The states of a phase node (uvm_phase_state). Function phases pass through EXECUTING in one walk; task phases stay there until the objection count returns to zero.

    You can read a node's state with phase.get_state() and block on one with phase.wait_for_state(UVM_PHASE_ENDED, UVM_GTE). Most testbenches never need to; the value of the picture is knowing which callback runs at which edge.

    How run_test drives the graph

    Before any phase runs, the library builds the common domain: a chain of nodes from build to final. It then adds the uvm domain's schedule of twelve runtime nodes with the run node, which is what makes them start together and end before extract. run_test creates the test, then hands the graph to the phaser. For every node, the phaser does the same steps:

    • Wait until every predecessor node is done (DORMANT, SCHEDULED, SYNCING).

    • Start: call phase_started(phase) on every component (STARTED).

    • Execute: for a function phase, one walk calling the imp's method on each component; for a task phase, fork one thread per component under a single parent process (EXECUTING).

    • Wait for zero: a task phase stays executing until its objection count has been zero for the drain time. Then phase_ready_to_end(phase) is called on every component; if any of them raises, the node goes back to executing. The loop is capped (20 times by default, set_max_ready_to_end_iterations).

    • End: call phase_ended(phase) on every component (ENDED), kill the phase's threads and clear its objection (CLEANUP), mark it DONE, and let its successors go.

    The three callbacks are called for every phase, common and runtime alike, so code inside them almost always starts by checking which phase it was given.

    Build it: hooking the schedule correctly

    Here is the bench scoreboard that owns end of test. It uses the node-to-imp comparison exactly as described, raises its own objection when words are still owed, and drops it from a forked thread when they arrive (a fork ... join_none is legal in a function).

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // snippet: from the course DMA bench
    // A scoreboard that owns end of test. It learns how many words each job owes
    // from the observed LEN and GO register writes, and when every objection drops
    // while words are still owed, it raises its own objection until they arrive.
    // (Counting on the expected queue alone is not enough: the last input word may
    // not have been published yet when the test drops its objection.)
    class dma_scoreboard_eot extends dma_scoreboard;
      `uvm_component_utils(dma_scoreboard_eot)
      int unsigned len_reg, owed;
      event        drained;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      virtual function void write_apb(apb_item t);
        super.write_apb(t);
        if (!t.write || t.slverr) return;
        if (t.addr == LEN)                 len_reg = t.data[15:0];
        if (t.addr == CTRL && t.data[0])   owed += len_reg;   // GO: this job owes len_reg words
      endfunction
    
      virtual function void write_out(axis_item t);
        super.write_out(t);
        if (owed > 0) owed--;
        if (owed == 0) -> drained;
      endfunction
    
      function void phase_ready_to_end(uvm_phase phase);
        if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;
        phase.raise_objection(this, $sformatf("%0d word(s) still owed by the DMA", owed));
        fork begin
          @drained;
          phase.drop_objection(this, "all words out");
        end join_none
      endfunction
    endclass

    To see the two kinds of object for yourself, a copy of the smoke test adds this to its run_phase:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet
    task run_phase(uvm_phase phase);
      `uvm_info("PH", $sformatf("node==get(): %0b  get_imp()==get(): %0b  is(get()): %0b  get_name(): %s  type: %s",
        phase == uvm_run_phase::get(), phase.get_imp() == uvm_run_phase::get(),
        phase.is(uvm_run_phase::get()), phase.get_name(), phase.get_phase_type().name()), UVM_LOW)
      super.run_phase(phase);
    endtask

    Watch it run

    Diagram
    UVM_INFO dma_tb.sv(...) @ 0: uvm_test_top [PH] node==get(): 0  get_imp()==get(): 1  is(get()): 1  get_name(): run  type: UVM_PHASE_NODE
    • node==get(): 0 is the bug from the opening, measured: the argument is never the imp.

    • type: UVM_PHASE_NODE confirms what the phaser hands you. uvm_run_phase::get().get_phase_type() would return UVM_PHASE_IMP.

    • The node's full name is common.run (domain, then phase); a runtime node's is uvm.uvm_sched.main. Use get_name() for short checks and get_full_name() when several domains are in play.

    Objections live on nodes too. A copy of the smoke test that raises on the imp, uvm_run_phase::get().raise_objection(this), gets the same error as a raise in build, and the library adds a hint:

    Diagram
    UVM_ERROR uvm_phase.svh(1672) @ 0: reporter [UVM/PH/NULL_OBJECTION] 'uvm_test_top' attempted to raise an objection on 'run', however 'run' is not a task-based phase node!  (This is a UVM_PHASE_IMP, you have to query the schedule to find the UVM_PHASE_NODE)

    What breaks: the extension that never extended

    A copy of the drain test whose scoreboard uses phase != uvm_run_phase::get() instead of the imp comparison shows how quiet this bug is. Compare the two runs:

    Diagram
    correct (get_imp() compare):
    UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
    UVM_ERROR :    0
    
    node compared with get():
    UVM_INFO dma_tb.sv(411) @ 365000: uvm_test_top.env.sb [SB] 5 words matched, 0 mismatched
    UVM_ERROR :    0

    The early return is taken on every call, so the scoreboard never raises. The test drops its objection as soon as the last input word is accepted, nothing objects at ready-to-end, and run_phase ends 10 ns early, with the last word inside the DMA, never predicted and never compared. Both runs report zero errors. Only the match count, six words programmed and five checked, gives it away. The "simplification" removed the only thing keeping the phase open.

    The same mistake hides in other forms: if (phase == uvm_main_phase::get()) in phase_started to start a main-phase helper, or a case on phase handles. Each compiles and silently never matches. The habit that prevents all of them is to compare imps or names, never a node against a get().

    Note

    phase_ready_to_end itself is one of the end-of-test tools. When to use it rather than a longer objection or a drain time is covered in the Objections Deep Dive topic, including its iteration cap and how to keep the extension bounded.

    Interview

    "What's the difference between the `phase` argument of run_phase and `uvm_run_phase::get()`?"

    A weak answer: "They're the same phase, one is just a static accessor." This is the answer that writes the never-matching comparison.

    A strong answer: "get() returns the imp, a singleton per phase type that knows which method to call and how to traverse. The argument is a node in the schedule graph: it has the state, the objection and the graph edges, and it points at its imp. They're never equal, so I compare phase.get_imp() against get(), or the name, or use phase.is(). Objections only exist on nodes, which is why raising on the imp gives a NULL_OBJECTION error."

    Follow-ups: Why does the design separate them? (one imp can appear in several schedules or domains) and In what order are phase_started, phase_ready_to_end and phase_ended called? (start, then ready-to-end whenever the count reaches zero, possibly several times, then ended).

    Check yourself You add a second domain for an independent interface. How many main nodes exist now, and how many uvm_main_phase imps?

    Two main nodes (one per domain's schedule) and still one imp. That is the point of the split: the same phase behaviour, placed in two schedules with independent states and objections.

    Check yourself A component overrides phase_started to print a banner "when run starts". The banner prints twenty-one times. Why, and what is the fix?

    phase_started is called for every phase node: nine common and twelve runtime. Filter inside it with if (phase.get_imp() == uvm_run_phase::get()) (or phase.get_name() == "run").

    Check yourself phase_ready_to_end raises an objection and immediately drops it in the same call. Does that extend the phase?

    No. The count goes 0, 1, 0 within the same call, so when the phaser looks again it is still zero and the phase ends. To extend, raise in the callback and drop later, from a thread that waits for the real condition, as dma_scoreboard_eot does.

    Next: with the machinery in view, the Build, Connect and Run topic walks each phase of the DMA bench in turn, starting with why build goes top-down.

    Related topics

    • Next topic: Build, Connect and Run

    • phase_ready_to_end

    • Phase Domains and Sync

    Previous

    24 / 280

    Next: UVM Phases: Build, Connect, Elaborate and Run

    Up next

    Up nextUVM Phases: Build, Connect, Elaborate and Run

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why Phases Exist
    • The Phase Schedule: Common and Runtime Phases
    • Traversal Order Rules