TO Be coming

Oct 29, 2026

UVM Course: Day 2: UVM Phases — The Simulation Lifecycle

Day 2: UVM Phases — The Simulation Lifecycle
Advertisement
Top banner — drop your 728×90 or responsive ad unit here.

Day 2: UVM Phases — The Simulation Lifecycle

A 30-day SystemVerilog/UVM verification series. Yesterday we built the skeleton. Today we learn how UVM decides when each piece runs.

Yesterday's FIFO testbench had build_phase, connect_phase, and run_phase scattered through it. Today we answer the question a beginner asks next: what is the full list of phases, in what order do they run, and why does UVM need so many?

By the end of this post you'll know why build_phase runs top-down but connect_phase runs bottom-up, what "objection" actually means, and how a simulation terminates cleanly.


1. The Big Picture: Why Phases Exist

In a plain SystemVerilog testbench, you just fork a bunch of initial blocks and hope. In UVM with 50+ components, "hope" doesn't scale — someone has to guarantee that all components are built before any connect, and that all connects happen before any run.

UVM phases solve exactly that. They are a fixed, ordered set of callbacks that UVM invokes on every registered component. The order is guaranteed by the framework — you never write "wait until the driver is built." You just trust the phase order.

Key idea: phases are what turn a collection of classes into a coordinated testbench. Every component gets every phase, in the same order, automatically.

2. The Full Phase Order

UVM has three groups of phases, executed in sequence:

GroupPhasesTypeWhen
Build (construction)build, connect, end_of_elaboration, start_of_simulationFunctionTime zero, before any clock activity
Run (execution)runTaskEntire simulation
Cleanup (check/report)extract, check, report, finalFunctionAfter run_phase completes

In code, that's the sequence every component experiences:

build_phase           → top-down  (functions)
connect_phase         → bottom-up (functions)
end_of_elaboration    → bottom-up
start_of_simulation   → bottom-up
run_phase             → parallel, task-based, entire sim
extract_phase         → bottom-up
check_phase           → bottom-up
report_phase          → bottom-up
final_phase           → top-down (last thing that runs)
The most confusing fact for beginners: build_phase is the only phase that runs top-down. Everything else runs bottom-up. Yes, that includes final_phase. Yes, it's surprising. Yes, you'll forget it once.

3. Why build_phase is Top-Down

A parent must exist before its children can be created. That's the whole reason.

When fifo_test::build_phase runs, it creates the env. When fifo_env::build_phase runs, it creates the agent and scoreboard. When fifo_agent::build_phase runs, it creates the driver, monitor, and sequencer. Top-down construction is the only order that works — a child cannot create itself before its parent exists.

uvm_test::build_phase
  └── creates uvm_env
uvm_env::build_phase
  └── creates agent, scoreboard
uvm_agent::build_phase
  └── creates driver, monitor, sequencer

4. Why connect_phase is Bottom-Up

Every component's ports must exist before any port is connected. In build_phase, children create their own ports (like monitor.ap = new("ap", this)). But those ports are created during build, at the bottom of the tree — after all parents have already started their build.

So by the time connect_phase runs, all ports are guaranteed to exist. Now we want the env to connect monitor.ap to scoreboard.exp_imp — but the env's connect_phase should only run after its children's connect_phase, because a child may itself connect internal ports. Bottom-up is the only order that respects "connect the leaves first."

uvm_agent::connect_phase
  └── drv.seq_item_port.connect(sqr.seq_item_export)
uvm_env::connect_phase
  └── agent.mon.ap.connect(sb.exp_imp)
Memory hook: "Build down, connect up, run parallel, clean up (bottom-up)."

Advertisement
Mid-post banner — drop your responsive ad unit here.

5. The run_phase and Runtime Sub-Phases

run_phase is the only task-based phase. It can contain @(posedge clk), forever, wait, anything time-consuming. And it runs in parallel across all components — every component's run_phase is forked simultaneously at the start of simulation and allowed to run to completion.

UVM also defines twelve runtime sub-phases that run in parallel with run_phase:

pre_reset → reset → post_reset → pre_configure → configure →
post_configure → pre_main → main → post_main → pre_shutdown →
shutdown → post_shutdown

These exist because real SoCs have a bring-up sequence: reset, then configure, then run, then shutdown. You can put logic in reset_phase that only runs during reset, and in main_phase that only runs during steady-state traffic.

Common trap: run_phase and the runtime sub-phases run simultaneously. If you put a forever loop in run_phase and also override reset_phase, both run at once. Pick one style — most projects use either plain run_phase or the sub-phases, rarely both.

6. Objections — How Simulation Ends

Here's the tricky part. If UVM just runs run_phase until every component's run_phase ends, then a component with a forever loop would never let the simulation finish. So how does UVM decide when to stop?

Objections. A component "raises" an objection to say "I'm still doing something, don't end yet," and "drops" it when done. When the global objection count returns to zero, UVM considers the run phase done and moves on to extract_phase.

task run_phase(uvm_phase phase);
    phase.raise_objection(this);          // "I'm working"
    seq.start(env.agent.sqr);             // run the sequence
    phase.drop_objection(this);           // "I'm done"
endtask
Rule: the test (not the env, not the agent) raises objections. The test is what knows "how long the test should run." Everything else just executes.

If you forget to raise an objection in the test, run_phase ends instantly at time zero and the simulation stops before anything happens — the classic "simulation ran for 0 ns" symptom.

If you raise but never drop, the simulation hangs. UVM will eventually time out and warn you — 9200 seconds by default, which is about 2.5 hours of your life you won't get back.


7. The Cleanup Phases

After run_phase ends (objection count → 0), UVM runs the cleanup phases in bottom-up order:

  • extract_phase — pull final data out of the DUT and scoreboards. Rarely overridden.
  • check_phase — assert "no unaccounted data" / "no failed comparison." This is where you put real checks.
  • report_phase — print summaries, coverage numbers, pass/fail counts. Log only, no checks.
  • final_phase — last hook before simulation exits. Only runs top-down.
function void check_phase(uvm_phase phase);
    if (writes_seen == 0 && reads_seen == 0)
        `uvm_error("SB", "no traffic observed")
    if (error_count != 0)
        `uvm_error("SB", $sformatf("%0d mismatches", error_count))
endfunction
Important: do not put `uvm_error in report_phase unless you really mean it. Report is for logging; checks belong in check_phase. Mixing them makes failures hard to attribute.

8. A Concrete Example — Objection and Timeout

Here's a compact example showing how a test controls simulation length with objections and how to handle a scenario where you want to run for a fixed wall-clock time rather than "until sequence ends."

class fifo_timeout_test extends fifo_test;
    `uvm_component_utils(fifo_timeout_test)

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

    task run_phase(uvm_phase phase);
        fifo_write_seq seq;
        phase.raise_objection(this, "running writes");

        // start 50 writes in the background
        fork
            begin
                repeat (50) begin
                    seq = fifo_write_seq::type_id::create("seq");
                    seq.start(env.agent.sqr);
                end
            end
            // watchdog: force stop after 10 us regardless
            begin
                #10us;
                `uvm_warning("TIMEOUT", "hit 10us watchdog, ending simulation")
            end
        join_any
        disable fork;

        phase.drop_objection(this, "writes done or timed out");
    endtask
endclass

The join_any + disable fork pattern is how you give a test a hard upper bound. This is the safe way to write a long-running random test without risking an infinite hang.


9. Common Phase Bugs (and How to Spot Them)

SymptomLikely Cause
Simulation ends at 0 nsNo raise_objection in test's run_phase
Simulation hangs foreverraise_objection without matching drop_objection
Null pointer in connect_phaseChild component never created in parent's build_phase
config_db::get returns 0 in driverWrong path string in set(), or the set happened after build
Wrong phase order observedAssuming top-down for something other than build_phase

10. Where Yesterday's FIFO Testbench Fits

Every phase yesterday's files used has a purpose:

  • fifo_driver::build_phase — fetches the virtual interface via config_db.
  • fifo_monitor::build_phase — same, plus creates the analysis port.
  • fifo_agent::build_phase — creates driver, monitor, sequencer.
  • fifo_agent::connect_phase — connects drv.seq_item_port to sqr.seq_item_export.
  • fifo_env::connect_phase — connects agent.mon.ap to sb.exp_imp.
  • fifo_test::run_phase — raises objection, starts sequence, drops objection.
  • fifo_scoreboard::check_phase — asserts traffic actually happened.

Nothing in yesterday's code was arbitrary. Every phase has a specific job in the flow.


11. Quiz

Ten questions on UVM phases. One question at a time — use Previous / Next to navigate. Options are shuffled on each page load. When you reach the last question, hit Submit Quiz to reveal all correct answers with explanations.

Question 1 of 10
Loading…
    0 / 10
    Advertisement
    Bottom banner — drop your 728×90 or responsive ad unit here.

    12. Further Reading

    If you want to go deeper on phases and objections than a blog post allows, these are the resources I keep coming back to:


    Coming Up Next: Day 3 — Sequences, Sequencer, Driver

    Day 1 built the skeleton, Day 2 wired the lifecycle. Day 3 puts it in motion. We'll cover:

    • The three roles — sequence item, sequence, sequencer, driver — and why confusing them is the #1 source of beginner bugs.
    • The handshake in detail — what start_item, finish_item, get_next_item, and item_done each actually do, and in what order they fire.
    • Why start_item can block — sequencer arbitration, grab/lock, and how to detect a deadlocked sequence.
    • Why randomize() goes after start_item — the UVM LRM rule that almost every tutorial gets wrong.
    • Virtual sequences — coordinating stimulus across multiple agents, and when to reach for them.
    • The full write + read sequence for the FIFO — so the scoreboard from Day 1 finally has something to compare.

    See you there.

    Series: 30 days of SystemVerilog/UVM verification, project-based. Day 1 built the skeleton. Day 2 wired the lifecycle. Day 3 puts it in motion.

    No comments:

    Post a Comment

    SiteMap