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
      • Why build_phase Is Top-Down
      • config_db Gets and Conditional Construction
      • Connect Phase: Bottom-Up TLM Wiring
      • Elaboration, Start and the Pre-Time-Zero Contract
      • The Run Phase: Parallel Execution and Duration
      • Runtime Sub-Phases: Reset, Configure, Main and Shutdown
      • What super.build_phase() Really Does
      • run_test, Timeouts and How a Simulation Ends
    • 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

‹UVM Phases: Build, Connect, Elaborate and Run
  1. Topics
  2. UVM Verification
  3. UVM Phases & Simulation Lifecycle
  4. UVM Phases: Build, Connect, Elaborate and Run
  5. The Run Phase: Parallel Execution and Duration

Part 2 · Phases & Lifecycle · Intermediate

The Run Phase: Parallel Execution and Duration

Every component's run_phase runs at once; the test owns how long the window stays open.

On this page

    Count the run_phase threads that do the work in the DMA smoke test: two drivers, three monitors, the test. Six threads, started at the same instant, and five of them never finish. (The phaser starts a run_phase thread for every other component too, the env, agents, sequencers and scoreboard, but those return at once or just wait.) Yet the test ends at exactly 655 ns, every time. Something decides that moment, and it is not any of the five loops.

    The idea: the window and who holds it open

    run_phase is a window of simulated time. When it opens, the phaser starts every component's run_phase task as its own thread, all at once and in no particular order. Parents do not start their children's threads, and super.run_phase starts nothing: the phaser forks every component directly.

    How long the window stays open is a separate question with a separate owner. A component that needs the window open raises an objection before its work and drops it after. The window closes when the count returns to zero (after an optional drain time). Think of a meeting room booked by whoever is presenting: the projector, the note-taker and the coffee machine are all running, but the meeting ends when the last presenter says "that's all", not when the coffee runs out.

    Component

    Its run_phase

    Holds the window?

    dma_smoke_test

    Programs the DMA, sends a packet, waits for the interrupt, checks STATUS

    Yes: one objection for the whole scenario

    apb_driver, axis_driver

    Wait for reset, then forever get, drive, item_done

    No

    apb_monitor, two axis_monitors

    forever sample and write

    No

    dma_scoreboard

    No run_phase: it reacts to write_* calls

    No

    dma_env, the agents

    No run_phase

    No

    Objection mechanics (counting, propagation, drain time) are the subject of the Objections Deep Dive. Here the point is ownership: exactly one place decides the duration, and it is the place that knows what "done" means.

    The picture: the smoke test's window

    Sequence diagram: the test raises an objection at time 0, programs the DMA through the APB driver, sends a packet through the stream driver, waits for the interrupt, checks status, waits 100 ns and drops the objection at 655 ns.
    The DMA smoke test's run window. The test raises at time 0 and drops after the interrupt and a 100 ns settle; every other thread just serves requests until the phaser kills it.

    Build it: the test that owns the window

    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
    // Program KEY and LEN, start a job with the interrupt enabled, stream one
    // packet of LEN words, wait for the interrupt, then check and clear STATUS.
    class dma_smoke_test extends dma_base_test;
      `uvm_component_utils(dma_smoke_test)
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      task run_phase(uvm_phase phase);
        bit [31:0] status, key = 32'hA5A5_0F0F;
        int unsigned n_words = 8;
        axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
    
        phase.raise_objection(this, "smoke test running");
        reg_write(KEY, key);
        reg_write(LEN, n_words);
        reg_write(CTRL, 32'h3);                  // IRQ_EN | GO
    
        if (!pkt.randomize() with { n == n_words; })
          `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
        pkt.start(env.in_agt.sqr);
    
        wait (irq_vif.irq === 1'b1);
        reg_read(STATUS, status);
        if (status[1] !== 1'b1) `uvm_error("STATUS", $sformatf("DONE not set: STATUS=%08h", status))
        reg_write(STATUS, 32'h2);                // write 1 to clear DONE
        reg_read(STATUS, status);
        if (status[2:0] !== 3'b000) `uvm_error("STATUS", $sformatf("STATUS not clear: %08h", status))
        #100ns;                                  // let the last output word drain
        phase.drop_objection(this, "smoke test done");
      endtask
    endclass
    • The raise is the first statement, before any register write. The writes block until the APB driver comes out of reset, and the objection must already be held while they wait.

    • wait (irq_vif.irq === 1'b1) is the test's definition of done: the DMA says the job is finished. It is better than any fixed delay because it scales with the job length.

    • #100ns before the drop lets the last output words leave the DMA and reach the scoreboard. A fixed settle is the simplest end-of-test strategy; the drain test shows a scoreboard that owns the end instead.

    • Every path to the end reaches the drop. A test with an early return that skips its drop hangs until the timeout.

    Drivers do not raise objections. A driver serves whoever is sending items; the sequence or test that sends them knows when the work is done. A driver that raised around each item would also work, but it spreads the definition of "done" across the bench and makes every hang harder to read.

    Order inside the window is not given to you

    All run_phase threads start at the same simulated time, and their order within that time step is not defined. Nothing about the phase says the APB driver is ready before the test's first register write. The bench does not need it to be: the test's reg_write starts a sequence, the sequence blocks in start_item until the driver calls get_next_item, and the driver does that only after its own reset wait. Order comes from the handshake, not from the phase, and that is also why the test's objection must be raised before the first reg_write: the write may block for the whole reset period. Any cross-component order you need at run time must be built the same way, with a sequencer handshake, a TLM call, an event or a wait on a signal.

    Watch it run

    Diagram
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    UVM_ERROR :    0
    UVM_FATAL :    0

    655000 ps is the moment the test dropped its objection. The scoreboard's report runs then, in zero time, after every run thread has been killed. If you add a second objection elsewhere, for example a sequence that raises and drops for itself, the window closes at whichever drop comes last.

    What breaks: two owners, one drop

    A test started a background traffic sequence with fork bg.start(env.in_agt.sqr); join_none next to its main register sequence, and raised one objection around the main sequence only. Background traffic was supposed to run for the whole test. In practice it stopped mid-packet the moment the main sequence finished, and the scoreboard reported SB_LEFTOVER for the half-sent packet.

    The background sequence never held the window open, so when the main sequence's drop brought the count to zero, run_phase ended and the phaser killed the background thread along with everything else. Two fixes are standard: raise a second objection for the background work before forking it (and drop it when it completes), or give the background sequence its own objection with set_automatic_phase_objection(1). Either way, every independent piece of work that defines "done" must hold its own objection, raised before the last other drop could happen.

    Common pitfall

    A test that raises nothing at all ends at time 0, because the count is zero from the start. That is not a hang; it is an empty pass, and it has its own lesson: run_test, Timeouts and How a Simulation Ends.

    Interview

    "Who should raise objections in a UVM testbench?"

    A weak answer: "Every component that does work in run_phase, so the test doesn't end while it's busy." That makes monitors and drivers keep the test alive forever.

    A strong answer: "Whoever knows when the work is done: the test, or the top-level sequence it runs. Drivers and monitors loop forever and never object; the phaser kills them when run ends. Every independent duration source raises its own objection before stimulus and drops it on every exit path. If the end must wait for the DUT to drain, a scoreboard can extend it with phase_ready_to_end based on what the DUT still owes, not on what the bench has seen."

    Follow-ups: Does super.run_phase start child threads? (no; the phaser forks every component) and run_phase objections are zero but the simulation continues: why? (a runtime phase is still held).

    Check yourself The env's run_phase calls super.run_phase(phase). Which threads does that start?

    None. The base run_phase does nothing (it calls the old run() hook, empty by default). The phaser forks every component's run_phase itself, env and children alike.

    Check yourself The smoke test's #100ns is removed. What risk appears, and what is the more robust alternative?

    The test may drop while the last output words are still inside the DMA; run ends, and check_phase reports SB_LEFTOVER (or worse, words that were never predicted are never compared). A robust alternative is a scoreboard that extends the phase until every word the DMA owes has come out, as dma_scoreboard_eot does in the drain test.

    Check yourself A monitor raises an objection at the start of its run_phase "to make sure it sees everything" and drops it after its forever loop. What happens?

    The drop is never reached, so the count never returns to zero and the test runs until the timeout. Monitors never object; end of test belongs to the test or the scoreboard.

    Next: run_phase is one window. Runtime Sub-Phases divides the same time into twelve named stages, and explains why most teams use them only for tests and sequences.

    Related topics

    • Runtime Sub-Phases

    • Objections Deep Dive

    • Function Phases vs Task Phases

    Previous

    29 / 280

    Next: Runtime Sub-Phases: Reset, Configure, Main and Shutdown

    Up next

    Up nextRuntime Sub-Phases: Reset, Configure, Main and Shutdown

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why build_phase Is Top-Down
    • config_db Gets and Conditional Construction
    • Connect Phase: Bottom-Up TLM Wiring