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
    • UVM Phases: Build, Connect, Elaborate and Run
    • Objections Deep Dive: Drain Time and Propagation
    • Phase Debugging and End-of-Test Signoff
      • End-of-Test Chain: Extract, Check, Report and Final
      • Phase Domains and Sync
      • Reset Phase and Phase Jump
      • Ordering Bugs and Silent Overrides
      • Objection Triage and Hang Forensics
      • Custom Phases
  • 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

‹Phase Debugging and End-of-Test Signoff
  1. Topics
  2. UVM Verification
  3. UVM Phases & Simulation Lifecycle
  4. Phase Debugging and End-of-Test Signoff
  5. Custom Phases

Part 2 · Phases & Lifecycle · Intermediate

Custom Phases

Defining a uvm_task_phase subclass, inserting it into a schedule or a domain, and why reset-aware run_phase components usually make it unnecessary.

On this page

    A design review for a DDR controller bench. Link training must happen after the registers are configured and before any traffic, and it takes a variable time. Someone proposes a new phase, training_phase, between configure and main. It sounds clean: every component that cares implements it, and the schedule guarantees the order. Someone else asks the question that decides it: what does every other VIP in the SoC do with a phase it has never heard of?

    This lesson shows how a custom phase is built and inserted, because interviewers ask and because you will meet them in vendor VIP. It also shows why most teams decide against writing one.

    The idea: adding a stop to a bus route

    A UVM schedule is like a bus route: a fixed list of stops, in order, that every passenger (component) rides. A custom phase adds a stop. Passengers who know about it get off and do something there; everyone else sits through it. The route map is shared, so adding a stop affects every passenger on that route, not only the ones who asked for it.

    Mechanically, every phase is a singleton implementation object (the imp) that knows how to call one method on a component, plus a node in a schedule graph that says where it runs. uvm_main_phase::get() returns the imp; a schedule holds nodes that point at imps. A custom phase is a new imp class plus a call that inserts it into a schedule.

    The picture

    The runtime schedule: reset phases, configure phases, then a custom training phase, then main and shutdown phases. A separate node says run_phase runs in parallel and is untouched.
    A custom task phase inserted after post_configure and before pre_main. Every component in the domain passes through it; only those that implement it do anything there.

    Build it

    Step 1: the phase implementation

    A task phase extends uvm_task_phase (function phases extend uvm_topdown_phase or uvm_bottomup_phase). Follow the library's own pattern: a protected constructor, a static get() that returns the single instance, and exec_task, which UVM calls once for every component in the domain. uvm_component has no training_phase method, so exec_task casts to a base class that declares one and does nothing for anyone else.

    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
    // snippet
    // Components that take part in link training extend this.
    virtual class training_component extends uvm_component;
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      pure virtual task training_phase(uvm_phase phase);
    endclass
    
    class training_phase_imp extends uvm_task_phase;
      local static training_phase_imp m_inst;
    
      protected function new(string name = "training");
        super.new(name);
      endfunction
    
      static function training_phase_imp get();
        if (m_inst == null) m_inst = new();
        return m_inst;
      endfunction
    
      virtual function string get_type_name();
        return "training_phase_imp";
      endfunction
    
      // Called for every component in the schedule's domain.
      virtual task exec_task(uvm_component comp, uvm_phase phase);
        training_component t;
        if ($cast(t, comp)) t.training_phase(phase);
      endtask
    endclass

    A base class works only for components you write. For a component that must extend something else, such as uvm_driver, an SV interface class with the training_phase task does the same job on simulators that support casting to interface classes.

    Step 2: a component that implements it

    The phase is a task phase, so it ends when its objections drop, exactly like main_phase. A component that does work there must raise first.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // snippet
    class phy_trainer extends training_component;
      `uvm_component_utils(phy_trainer)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      virtual task training_phase(uvm_phase phase);
        phase.raise_objection(this, "PHY: link training");
        `uvm_info("TRAIN", "training the link", UVM_LOW)
        #200ns;                                  // stands in for the training sequence
        `uvm_info("TRAIN", "link up", UVM_LOW)
        phase.drop_objection(this, "PHY: link up");
      endtask
    endclass

    Step 3: insert it into a schedule

    Insert the node once, before the runtime phases start, naming its neighbours. Adding it to the uvm domain's schedule puts it in front of every component in that domain:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet
    function void build_phase(uvm_phase phase);       // in the test
      uvm_phase sched;
      super.build_phase(phase);
      sched = uvm_domain::get_uvm_schedule();
      sched.add(training_phase_imp::get(),
                .after_phase(uvm_post_configure_phase::get()),
                .before_phase(uvm_pre_main_phase::get()));
    endfunction

    To limit it to one subtree, put that subtree in its own domain and insert into that domain's schedule instead. set_domain creates the domain's runtime schedule, named uvm_sched, if it does not exist yet. It can be called in build_phase (children built later inherit the domain) or later; here the test does it in connect_phase, in the same place it edits the schedule, which must happen before the runtime phases start:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // snippet
    function void connect_phase(uvm_phase phase);     // in the test
      uvm_phase sched;
      phy_dom = new("phy_dom");
      env.phy.set_domain(phy_dom);
      sched = phy_dom.find_by_name("uvm_sched");
      sched.add(training_phase_imp::get(),
                .after_phase(uvm_post_configure_phase::get()),
                .before_phase(uvm_pre_main_phase::get()));
    endfunction

    Watch it run

    Added to the DMA bench with a phy_trainer in the env that also logs its configure_phase and main_phase entries, either version gives the same order (Verilator, times in ps):

    Diagram
    UVM_INFO cp_tb.sv(51) @ 0: uvm_test_top.env.phy [PH] configure
    UVM_INFO cp_tb.sv(45) @ 0: uvm_test_top.env.phy [TRAIN] training the link
    UVM_INFO cp_tb.sv(47) @ 200000: uvm_test_top.env.phy [TRAIN] link up
    UVM_INFO cp_tb.sv(55) @ 200000: uvm_test_top.env.phy [PH] main starts

    main_phase starts at 200 ns, exactly when the trainer drops its objection. In the first version, exec_task was called for every component in the uvm domain, ports included (they are components too), and did nothing for all but one. That is the bus route: everyone rides through the new stop.

    Why it is rarely justified

    The phase worked. Most teams would still reject it, for reasons that only show up at scale:

    • Nobody else knows the phase. Vendor VIP, other teams' agents and your own existing components implement only the standard phases. Most of them do their real work in run_phase anyway, so a runtime-phase barrier does not even pause them.

    • Reuse carries the schedule change. Every bench that reuses the env must perform the same insertion, exactly once, or the trainer's task never runs. The library does not stop a second insertion from another env either; it simply adds a second training node to the schedule.

    • Jumps and domains get harder. A jump back to reset re-runs the custom phase; a sync between domains must now consider it; every reader must learn it.

    • The ordering is easy to get without it. A test or virtual sequence that runs the training sequence after configuration and before traffic gives the same order, visibly, in one place.

    The industry default is the shape from Reset phase and phase jump: components live in run_phase and react to reset and to events, and the test sequences the scenario (configure, train, traffic) explicitly. Custom phases appear mostly in large in-house methodologies that control every component in the bench, and in some VIP that ships with its own domain.

    What breaks: the phase that half the bench skipped

    A team added a calibrate_phase for an analog front-end model and implemented it in their own agents. At SoC integration, the third-party PCIe VIP in the same domain started its link-up traffic in run_phase at time 0, straight through the calibration period, because it knew nothing about the new phase. The calibration results were corrupted intermittently, depending on how fast PCIe trained.

    The fix was not a better phase. They replaced calibrate_phase with a calibration sequence started by the SoC virtual sequence, gated the PCIe link-up on an event that the calibration triggered, and deleted the custom phase. The order became visible in one file, and the next integration did not need to know about any schedule changes.

    Interview

    "Have you ever written a custom phase? When would you?"

    A weak answer: "Yes, you extend uvm_phase and add it to the schedule." It shows syntax and no judgement.

    A strong answer: "I know how: a uvm_task_phase subclass with a singleton get() and exec_task that casts each component to a type declaring the method, inserted once with schedule.add using after_phase and before_phase, in the uvm schedule or a dedicated domain. I almost never would. Every component in the domain rides through the new phase; third-party VIP ignores it and usually works in run_phase anyway; every reusing bench must repeat the insertion. A sequence started by the test, plus reset-aware run_phase components, gives the same ordering visibly and survives reuse."

    Follow-ups: What ends a custom task phase? (its objections, exactly like main_phase; no raise means it ends at once). What does exec_task get called on? (every component in the domain, so it must ignore the ones that do not implement the phase).

    Check yourself You insert training_phase into the uvm schedule, but phy_trainer's task never runs, and no error is reported. What are the likely causes?

    Either exec_task's cast fails for the trainer (it does not extend the type exec_task casts to, or a method name differs), or the trainer is in a different domain whose schedule has no training node. A third possibility is that the insertion ran after the runtime phases had already started; insert it in build or connect.

    Check yourself The trainer raises no objection in training_phase and calls a 200 ns task. What happens?

    The phase ends at once, because nothing objects to it, and the trainer's process is killed when the phase ends. main_phase starts without waiting for training. A custom task phase follows the same objection rules as every runtime phase.

    Check yourself Why does a custom runtime phase often fail to delay third-party VIP traffic?

    Most VIP drives from run_phase, which is in the common domain and runs in parallel with all runtime phases, standard or custom. A runtime phase only orders code that lives in runtime phases. Gate the VIP's traffic on an event or a sequence instead.

    Next: this completes the phases part. Factory and configuration picks up the question every lesson here took for granted: how a test swaps components and settings without editing them.

    Related topics

    • Reset phase and phase jump

    • Phase domains and sync

    • Next: Factory and configuration

    Previous

    44 / 280

    Next: Factory & Configuration

    Up next

    Up nextFactory & Configuration

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • End-of-Test Chain: Extract, Check, Report and Final
    • Phase Domains and Sync
    • Reset Phase and Phase Jump