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.

systemverilog
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
diagram
[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.

systemverilog
// 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

diagram
[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 mismatches
systemverilog
fork
  drv.main_stimulus(); // no reset wait
  mon.sample_forever();
join_none

False 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.