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.
2. The Full Phase Order
UVM has three groups of phases, executed in sequence:
| Group | Phases | Type | When |
|---|---|---|---|
| Build (construction) | build, connect, end_of_elaboration, start_of_simulation | Function | Time zero, before any clock activity |
| Run (execution) | run | Task | Entire simulation |
| Cleanup (check/report) | extract, check, report, final | Function | After 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)
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)
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.
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
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
`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)
| Symptom | Likely Cause |
|---|---|
| Simulation ends at 0 ns | No raise_objection in test's run_phase |
| Simulation hangs forever | raise_objection without matching drop_objection |
Null pointer in connect_phase | Child component never created in parent's build_phase |
config_db::get returns 0 in driver | Wrong path string in set(), or the set happened after build |
| Wrong phase order observed | Assuming 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 viaconfig_db.fifo_monitor::build_phase— same, plus creates the analysis port.fifo_agent::build_phase— creates driver, monitor, sequencer.fifo_agent::connect_phase— connectsdrv.seq_item_porttosqr.seq_item_export.fifo_env::connect_phase— connectsagent.mon.aptosb.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.
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:
Official and reference material
- Accellera UVM Standard (official spec) — the primary source. Section 9 covers phasing in detail; it's dense but authoritative.
- Verification Academy — UVM Resources — Siemens' free library of UVM tutorials, webinars, and whitepapers. The "UVM Phasing" talks are worth the time.
- Verification Guide — UVM Phases — concise written walkthrough of the phase order with diagrams; good for a quick refresher.
- UVM source on GitHub — read
uvm_phase.svhif you want to see how objection counting is actually implemented. - ChipVerify — UVM Phases — another solid tutorial with small code snippets for each phase.
- Doulos — UVM KnowHow — free articles and example code; the "UVM Phase Mechanism" article pairs well with today's post.
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, anditem_doneeach actually do, and in what order they fire. - Why
start_itemcan block — sequencer arbitration, grab/lock, and how to detect a deadlocked sequence. - Why
randomize()goes afterstart_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