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. Ordering Bugs and Silent Overrides

Part 2/Phases & LifecycleIntermediate

Ordering Bugs and Silent Overrides

Overrides and config sets that arrive too late, phase methods and analysis writes that never run, and races inside a single time step.

On this page

    While the course bench was being built, a test swapped in an end-of-test scoreboard with a factory override. The topology table printed at the start of the run showed dma_scoreboard_eot in the env, exactly as intended. Yet its extended write_apb and write_out never ran: no words were ever counted as owed, and the end-of-test hold never happened. The override had worked, and the new code was dead.

    This lesson is about code that runs at the wrong moment or not at all, although everything compiles and the schedule did exactly what it promised. There are three families: things that happen too late for the phase that needed them, methods that never get called, and races inside a single time step.

    The idea: the schedule keeps its promises, your code may not

    Think of a relay race. The rules are fixed: runners go in order and hand over the baton in the exchange zone. When a relay goes wrong it is almost never the rules; it is a runner who started too early, a baton handed to someone not on the team, or two runners who each thought the other had it. UVM's phase schedule is the rulebook: build runs top-down, connect bottom-up, run in parallel, then the post-run phases. Ordering bugs come from code that assumes something the rules never promised.

    The picture: when a build-time action is too late

    Action

    Must happen before

    Too late if done in

    Symptom

    Factory override for a component

    The create call for that component runs

    After the parent created it (for the env: after super.build_phase() in the test), or in connect_phase

    Original type built; override silently unused

    config_db set for a component

    That component's build_phase calls get

    connect_phase or later

    get fails or reads a default

    set_domain, schedule edits

    Runtime phases start

    run_phase

    No effect on the current run

    Raising an objection

    The first blocking statement in the phase

    After any wait

    Phase ends at time 0

    Build it: the override that lands in time

    The bench's dma_truncate_test shows the correct order. The override is registered first, then super.build_phase() runs the base test's build, which creates the env, whose build creates the driver. Because build is top-down, everything below the test is created after the override exists.

    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
    // snippet: from the course DMA bench
    // Factory override: the stream driver cuts each packet short, so the DMA must
    // stop early and report ERR. No sequence or env file changes.
    class dma_truncate_test extends dma_base_test;
      `uvm_component_utils(dma_truncate_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        axis_driver::type_id::set_type_override(axis_truncating_driver::get_type());
        super.build_phase(phase);                // the env is created after the override
      endfunction
    
      task run_phase(uvm_phase phase);
        dma_job_cfg job = dma_job_cfg::type_id::create("job");
        bit [31:0] status;
        phase.raise_objection(this);
        if (!job.randomize() with { len == 8; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
        run_job(job, 8, status);
        if (status[2] !== 1'b1) `uvm_error("STATUS", $sformatf("ERR not set after a short packet: STATUS=%08h", status))
        else `uvm_info("TRUNC", "DMA reported ERR for the short packet, as expected", UVM_LOW)
        #100ns;
        phase.drop_objection(this);
      endtask
    endclass

    The precise rule is that an override must exist before the create call for that component runs. Here the driver is created two levels down, in the agent's build_phase, which runs after the test's build_phase returns, so even swapping the two lines would still work. It would not work for the env itself, which super.build_phase() creates. Override the env type after that call and the original env runs, nothing is truncated, and the test fails with ERR not set after a short packet; a test with a weaker check would simply pass. Registering first is the habit that is right for every level.

    Do not skip super.build_phase() either. The base uvm_component::build_phase applies field-macro configuration (apply_config_settings), and uvm_agent::build_phase reads is_active that way. Leaving it out does not stop children from being built by your own code, but it silently loses that configuration.

    Methods that never run

    A misspelled phase name

    function void build_phsae(uvm_phase phase) compiles: it is simply a new method that nobody calls. Only the name typo is silent. A wrong argument type, or a function where the base declares a task (function void run_phase is a compile error, because run_phase is a task), fails at compile time because a virtual override must match the base declaration.

    A non-virtual method behind an override

    This was the opening bug. The analysis imp that uvm_analysis_imp_decl(_out) creates holds a handle of the base scoreboard type and calls write_out through it. In the first version of the bench, the base class's write_out was not virtual, so the call always went to the base version, whatever object the factory had built. The fix, now in the bench, is one keyword:

    design.sv
    // snippet
    // In dma_scoreboard: virtual, so a derived scoreboard's version is the one called.
    virtual function void write_out(axis_item t);

    A factory override replaces the object. Whether your new code runs still depends on virtual dispatch. Any method meant to be extended by an override (analysis writes, driver hooks such as the bench's pre_drive) must be virtual in the base class.

    Races inside one time step

    Phases order whole stages. Inside a stage, threads that wake on the same clock edge run in an order SystemVerilog does not define. The bench met this too. The APB driver finished the GO write on a clock edge; in the same time step the stream driver took its first item and waited on the stream interface's own clocking block. Two clocking blocks on the same edge fire in an undefined order, so the stream driver sometimes caught the current edge, "saw" TREADY on a beat the DUT never took, and each packet arrived one word short. The DMA reported ERR on 2 of 3 random jobs.

    The fix is the rule the bench now follows everywhere: drivers wait on @(posedge clk), one shared event, and drive with nonblocking assignments; monitors sample through a clocking block with #1step input skew. Know which event each thread waits on, and keep every thread that hands work to another on the same event.

    Watch it run: the trace switches

    Switch or call

    Shows

    Use it for

    +UVM_PHASE_TRACE

    Every phase start and end, per domain

    Which phase never ended; jumps; domain drift

    +UVM_OBJECTION_TRACE

    Every raise and drop at every level

    Who holds a phase; late or missing raises

    +UVM_CONFIG_DB_TRACE

    Every config_db set and get, with paths

    A get that fails or reads the wrong value

    print_topology()

    The built tree with each component's type

    Whether an override built the right type

    uvm_factory::get().print()

    Registered overrides

    Whether an override was registered at all

    There is no +UVM_FACTORY_TRACE; use the factory's print() and debug_create_by_type(). The bench's base test calls print_topology() once, in its end_of_elaboration_phase. Print the tree in one place, the test, so a bench with ten agents does not print it ten times.

    What breaks: the config set that arrived after the get

    A team's agent configuration came from an env-level helper that ran in connect_phase, "once everything exists". Every agent's config_db::get in build_phase failed and fell back to defaults, so all agents were active, including the one that should have been passive and now fought the DUT for a bus. +UVM_CONFIG_DB_TRACE showed every get logged before every set. Build is top-down and finishes for the whole tree before connect starts, so a set in connect is always too late for a get in build. Moving the sets into the env's build_phase, before creating the agents, fixed it.

    "An override shows up in print_topology, but the new behaviour never happens. What do you check?"

    A weak answer: "The override must not have been registered." The topology already proves it was.

    A strong answer: "The right object was built, so the problem is dispatch or timing. First, are the methods I overrode virtual in the base class? An analysis imp or a base-class handle calls through the base type, so a non-virtual write_* or hook always runs the base version. Second, is the method name exactly right? A misspelled phase or hook compiles as a new method. Third, does the new code depend on configuration that was set too late for build?"

    Follow-ups: Must the override come before super.build_phase()? (for the env, yes: super.build_phase creates it; for components further down, created later in their parents' build_phase, it is a safe habit rather than a requirement). What does skipping super.build_phase lose? (apply_config_settings: field-macro config and is_active).

    Check yourself build_phsae compiles cleanly but its log line never prints. What happened, and how would you have caught it sooner?

    The misspelling defines a new method; the scheduler calls build_phase, which is the inherited base version. An entry log at UVM_HIGH in each phase override catches it on the first run, as does a lint rule for methods ending in _phase that override nothing.

    Check yourself A test calls set_type_override in connect_phase. Which components get the new type?

    None that already exist. All components are created during build, which has finished for the whole tree before connect starts. Only objects created later by the factory, such as sequence items, would pick it up.

    Check yourself Two drivers on the same clock each use their own interface's clocking block, and one of them occasionally acts on the wrong edge. Why, and what is the fix?

    Different clocking blocks on the same edge trigger in an undefined order within the time step, so a thread that starts waiting in that step may catch the current edge instead of the next. Have drivers wait on one shared event, @(posedge clk), drive with nonblocking assignments, and keep clocking blocks for monitor sampling.

    Next: the most common debugging job of all is the test that never ends. Objection triage and hang forensics is the playbook.

    Related topics

    • Objection triage and hang forensics

    • Override debug patterns

    • Passive sampling

    Previous

    42 / 280

    Next: Objection Triage and Hang Forensics

    Up next

    Up nextObjection Triage and Hang Forensics

    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
    • Custom Phases