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. Function Phases vs Task Phases

Part 2 · Phases & Lifecycle · Intermediate

Function Phases vs Task Phases

Function phases are zero-time walks; task phases fork a thread per component and kill them when the phase ends.

On this page

    Look at any driver in the DMA bench: forever begin get_next_item; drive; item_done; end. There is no exit, no stop flag, no break. A junior engineer once asked in a code review: "who stops this loop?" The answers around the table were "the objection", "$finish", and "it just stops". None of them is quite right, and the right one explains a whole class of bugs.

    The idea: a roll call versus a work shift

    A function phase is like a roll call. The phaser walks the tree once and calls each component's method; each call must return immediately, because a SystemVerilog function cannot wait. When the walk is done, the phase is done. No simulated time passes.

    A task phase is a work shift. The phaser starts a separate thread (a SystemVerilog process) for every component's phase task, all at the same moment, and then waits. The shift ends when the last worker who said "I'm busy" has signed out: that sign-in is an objection. When the shift ends, the supervisor sweeps the floor. Every thread the phase started, and every thread those threads forked, is killed, wherever it happens to be.

    So the driver's loop is stopped by the phaser killing its thread when run_phase ends. The objection decides when that happens; the kill is how.

    The picture: one run_phase from start to kill

    Sequence diagram: the phaser forks the test, driver and monitor run_phase threads, the test raises and drops an objection, then the phaser kills the remaining threads and starts extract.
    run_phase in the DMA bench. The phaser forks one thread per component, waits for the objection count to return to zero, then kills every thread it started before moving to extract.

    Build it: what each kind of phase may contain

    The APB monitor is the purest task-phase component in the bench: it loops forever, sampling through the clocking block, and owns no part of deciding when the test ends.

    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
    // snippet: from the course DMA bench
    class apb_monitor extends uvm_monitor;
      `uvm_component_utils(apb_monitor)
      virtual apb_if vif;
      uvm_analysis_port #(apb_item) ap;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
          `uvm_fatal("NOVIF", "apb_if not set for this monitor")
      endfunction
    
      // A transfer completes on the edge where PSEL, PENABLE and PREADY are all 1.
      task run_phase(uvm_phase phase);
        forever begin
          @(vif.mon_cb);
          if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
            apb_item it = apb_item::type_id::create("it");   // a fresh object per transfer
            it.addr   = vif.mon_cb.paddr;
            it.write  = vif.mon_cb.pwrite;
            it.data   = vif.mon_cb.pwdata;
            it.rdata  = vif.mon_cb.prdata;
            it.slverr = vif.mon_cb.pslverr;
            `uvm_info("APB_MON", it.convert2string(), UVM_HIGH)
            ap.write(it);
          end
        end
      endtask
    endclass

    Its build_phase is a function: it reads config_db and returns. Its run_phase is a task that never returns. Neither raises an objection. Monitors never do, because a monitor that objected would keep the test alive for as long as the bus exists.

    Construct

    In a function phase

    In a task phase

    #delay, @(event), wait(...)

    Compile error: a function cannot block.

    Legal; this is what task phases are for.

    fork ... join / join_any

    Compile error: the join blocks.

    Legal.

    fork ... join_none

    Legal. The new thread outlives the function, so it belongs to whatever process called it.

    Legal. The thread is killed with the phase.

    phase.raise_objection(this)

    UVM_ERROR UVM/PH/NULL_OBJECTION: a function phase has no objection.

    Legal; the test or a sequence owns this.

    Starting a sequence

    Impossible: start() is a task.

    Legal.

    The objection row is worth reading twice. A function phase has no objection object at all, because there is nothing to wait for. Raising one there is not a compile error, so it survives code review; it is caught only when the simulation runs.

    Who is killed, and when

    The phaser runs each task phase under one parent process and kills that process, with all its descendants, when the phase moves to cleanup. In practice that means:

    • Every component's run_phase thread, finished or not.

    • Every thread they forked with join_none, including monitors' helper threads and sequences started from the test.

    • For a runtime phase such as main_phase, only the threads of that phase. run_phase threads keep going.

    The last point is why drivers live in run_phase: a driver written in main_phase is killed when main ends, possibly between get_next_item and item_done. Runtime Sub-Phases tells that story.

    Watch it run: the raise that stops the test

    Here is a copy of the smoke test (call it dma_nullobj_test) whose build_phase ends with phase.raise_objection(this). It never drives a single bus cycle:

    Diagram
    UVM_INFO @ 0: reporter [RNTST] Running test dma_nullobj_test...
    UVM_ERROR uvm_phase.svh(1672) @ 0: reporter [UVM/PH/NULL_OBJECTION] 'uvm_test_top' attempted to raise an objection on 'build', however 'build' is not a task-based phase node!
    UVM_FATAL @ 0: reporter [BUILDERR] stopping due to build errors
    ...
    UVM_ERROR :    1
    UVM_FATAL :    1

    The raise itself does nothing; it only reports an error. The fatal comes from a second rule: when end_of_elaboration starts, uvm_root checks whether any UVM_ERROR has been reported so far, and if so it stops the run with BUILDERR. Any error during build or connect therefore ends the test at time 0. Elaboration, Start and the Pre-Time-Zero Contract uses that rule on purpose.

    What breaks: the summary after the forever loop

    A stream monitor was given a counter of observed beats and a closing line: forever begin ... end followed by ¬uvm_info("MON", $sformatf("%0d beats", n), UVM_LOW). The line never appeared. The engineer added +UVM_VERBOSITY=UVM_FULL, then suspected the report server, then the build.

    The code was fine; it was unreachable. Nothing ever leaves the forever loop, and when run_phase ended the phaser killed the thread inside it. The same thing happens to any code placed after a wait that never completes, or after a sequence start() that is still running when the phase ends. The fix is to put summaries where they belong: count in run_phase, report in report_phase, which runs after every run thread is gone. That is exactly how dma_scoreboard prints its match count.

    Common pitfall

    Killing is not a clean exit. A thread killed in the middle of a bus transfer leaves the bus wherever it was. That is harmless at the very end of a test, and it is why mid-test stops (a jump, the end of a runtime phase) need a reset-aware driver.

    Interview

    "Your driver's run_phase is a forever loop. How does it ever stop?"

    A weak answer: "When the objections drop, the simulation ends with $finish." That skips a step, and it implies code after the loop could run.

    A strong answer: "The phaser forks every component's run_phase as a process under one parent process. When the last objection drops and the drain time passes, run_phase goes through ready-to-end and then cleanup, and in cleanup the phaser kills that parent process and all its children, the driver loop included. Extract, check and report then run in zero time. Nothing after the loop runs, so summaries go in report_phase."

    Follow-ups: What happens to a sequence still running when run_phase ends? (killed with the test's thread) and Is raising an objection in build_phase a compile error? (no: a UVM/PH/NULL_OBJECTION error at run time, which then stops the test with BUILDERR).

    Check yourself A scoreboard spawns a checker thread with fork ... join_none from its build_phase. Is that legal, and what happens to the thread?

    Legal: join_none does not block. But the thread starts at time 0 before run_phase and is not under run_phase's parent process, so the phaser does not kill it at the end of run, and it is not tied to any objection. Start long-lived threads from run_phase so they live and die with the phase.

    Check yourself The test starts a sequence with fork seq.start(sqr); join_none, raises nothing, and returns from run_phase. What happens to the sequence?

    Nothing holds run_phase open, so it ends right away (at time 0 here) and the phaser kills the test's thread and every thread it forked, the sequence included. The test ends with no traffic. Raise an objection before forking and drop it when the work is done.

    Check yourself Why is a monitor's run_phase allowed to loop forever, while a test's run_phase must return?

    Neither must return. What matters is objections. The monitor holds none, so its loop never keeps the phase alive and is simply killed at the end. The test holds one, so it must drop it; returning is the usual way to reach the drop.

    Next: the phaser can only call a method it recognises. Signatures, Super Calls, and Debug shows how one wrong letter makes a phase method invisible.

    Related topics

    • Signatures, Super Calls, and Debug

    • The Run Phase: Parallel Execution and Duration

    • Objections Deep Dive

    Previous

    22 / 280

    Next: Signatures, Super Calls, and Debug

    Up next

    Up nextSignatures, Super Calls, and Debug

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why Phases Exist
    • The Phase Schedule: Common and Runtime Phases
    • Inside the Phaser: Nodes, Imps and Schedules