Part 2 · Phases & Lifecycle · Intermediate
Life Without Phases: Ordering Chaos and Failure Modes
Concrete anti-patterns when construction, connection, and run-time activity are interleaved by hand — and the failure modes they produce.
The manual lifecycle anti-pattern
Plain SystemVerilog testbenches often interleave construct, connect, and start in one procedural script. One misplaced line creates silent or catastrophic failures.
module tb;
driver drv;
monitor mon;
scoreboard scb;
initial begin
// construct — order chosen by today's integrator
mon = new("mon");
scb = new("scb");
drv = new("drv");
// connect — hope everything from build exists
drv.ap.connect(scb.exp_imp);
mon.ap.connect(scb.act_imp);
fork
drv.run();
mon.run();
scb.run();
join_none
#10000ns; // completion = guesswork
$finish;
end
endmodule[UVM][PHASE] chaos checklist
construction:
[ ] parent before child enforced?
[ ] factory overrides applied before create?
connection:
[ ] all endpoints non-null?
[ ] passive/active branches consistent?
run:
[ ] reset complete before stimulus?
[ ] all threads started?
stop:
[ ] real quiescence or magic delay?Construction and connect are interleaved without a phase barrier.
Simulation end is a hard-coded delay, not a work-complete signal.
Adding a third agent forces a full re-audit of ordering.
Key takeaways
Manual scripts encode temporal policy in one fragile place.
Magic delays hide both premature end and infinite hang classes of bugs.
Every VIP insertion is an integration re-verification event.
Common pitfalls
Believing 'it worked once' proves ordering is correct.
Using join_none without a completion protocol.
Connecting before all participants are constructed.
Common failure modes without phase barriers
Null-pointer connect crashes
If scb is created after drv.ap.connect(scb.exp_imp), the connect dereferences null. Without connect_phase's bottom-up guarantee, this is routine.
// reorder lines — same 'testbench', different result
drv.ap.connect(scb.exp_imp); // scb == null
scb = scoreboard::type_id::create("scb", this);Race at run start
[PHASE][RUN] fork order hazard
t=0 drv.run() starts driving
t=0 mon.run() starts sampling
t=5 reset still asserted
result:
first transactions violate reset assumptions
scoreboard flags false mismatchesfork
drv.main_stimulus(); // no reset wait
mon.sample_forever();
join_noneFalse pass on early $finish
Timeout ends simulation before the last burst completes.
Scoreboard queues still hold unchecked transactions.
Coverage goals appear met because sampling stopped early.
Key takeaways
Phase barriers turn implicit dependencies into explicit scheduler rules.
Connect crashes and run races are symptoms of missing structural/behavioral sync.
Completion without objections is almost always a false-positive generator.
Common pitfalls
Patching chaos with longer delays instead of structural fixes.
Adding null checks that mask ordering bugs in connect.
Documenting 'required call order' in a wiki instead of enforcing it in code.