Part 6 · Testbench Architecture · Intermediate

Phasing a Hand-Built TB

Reset, config, main, drain, and report phases by convention; env-orchestrated run(); fork-join of component loops; clean shutdown.

Phases by convention

UVM enforces phases with machinery; a hand-built bench enforces them with convention plus one orchestrating task . The env owns a run() task that calls per-phase tasks strictly in order — components never decide for themselves when a phase starts. Five phases cover almost every bench:

  1. reset_phase — assert/deassert DUT reset, clear TB state (model state, scoreboard queues, counters).

  2. config_phase — program DUT registers and TB knobs before any traffic; zero-time or bus-config traffic only.

  3. main_phase — start all component run() loops, run stimulus to completion.

  4. drain_phase — stimulus done; wait for in-flight transactions to flush (activity-window or objection-based).

  5. report_phase — final checks, statistics, PASS/FAIL banner; zero-time, functions only.

diagram
ENV-ORCHESTRATED PHASE TIMELINE

  time ──────────────────────────────────────────────────────►

  reset ─► config ─► main ─────────────────────► drain ─► report
  │        │        │                            │        │
  │        │        fork: gen.run()              │        scb.final_check()
  │        │              drv.run()              │        cov.report()
  │        │              mon.run()              │        banner
  │        │              scb.run()              │
  │        │        join_none (free-running)     │
  │        │        wait: stimulus complete      │
  │        │                                     │
  rst=1    regs     monitors/scb keep running    idle-window
  N cycles written  through drain ───────────────┘
  rst=0

The orchestrating run() task

systemverilog
class env;
  generator   gen;
  driver      drv;
  bus_monitor mon_in, mon_out;
  ref_model   mdl;
  scoreboard  scb;
  virtual bus_if vif;

  task run();
    reset_phase();
    config_phase();
    main_phase();
    drain_phase();
    report_phase();
  endtask

  task reset_phase();
    vif.rst_n <= 0;
    scb.flush(); mdl.reset();          // TB state mirrors DUT reset
    repeat (10) @(posedge vif.clk);
    vif.rst_n <= 1;
    repeat (3) @(posedge vif.clk);     // post-reset settle
  endtask

  task config_phase();
    drv.write_reg(CTRL_ADDR, 32'h0000_0001);   // enable DUT
    drv.write_reg(MODE_ADDR, cfg.mode);
  endtask

  task main_phase();
    fork : main_threads
      drv.run();       // forever loops — never return
      mon_in.run();
      mon_out.run();
      mdl.run();
      scb.run();
    join_none
    gen.run();                          // returns when last item generated
    wait (drv.items_driven == gen.n_items);  // true stimulus completion
  endtask

  task drain_phase();
    int idle = 0;
    int unsigned seen = mon_out.txn_count;
    while (idle < 100) begin
      @(posedge vif.clk);
      if (mon_out.txn_count != seen) begin seen = mon_out.txn_count; idle = 0; end
      else idle++;
    end
    disable main_threads;               // clean shutdown of forever loops
  endtask

  task report_phase();
    scb.final_check();
    scb.report();
  endtask
endclass

Code walkthrough

  1. Service components (driver, monitors, model, scoreboard) are forever loops started with fork...join_none — they never return, so nothing may join on them.

  2. The generator is the only run() that returns; main_phase then waits on the driver's completion counter, not the generator's return.

  3. Monitors and scoreboard stay alive through drain_phase — that is the whole point of draining.

  4. disable main_threads only after the idle window proves quiescence; killing threads mid-transaction corrupts the last compares.

  5. reset_phase resets TB state too — a scoreboard holding pre-reset expected entries fails every post-reset compare.


Clean shutdown and mid-test reset

The labeled fork : main_threads plus disable main_threads idiom is the standard kill switch, but it is a blunt one: threads die wherever they happen to be. The discipline that makes it safe is ordering — only disable after quiescence is proven, and never hold a semaphore or a bounded-mailbox slot across a point where the thread may be killed. For benches that need mid-test reset , the same machinery loops: detect reset, disable the traffic threads, flush mailboxes and TB state, then re-fork and continue — which is exactly why TB state cleanup lives in reset_phase rather than scattered through the components.

  • Label every fork you may need to kill — disable fork (unlabeled) in a class context can kill more than you intended, including innocent sibling threads.

  • Flush mailboxes after a kill: a dead consumer leaves items that the next phase would misinterpret as live traffic.

  • Keep report_phase function-only — anything time-consuming there delays $finish and can race the watchdog.

Interview angle

The classic prompt is "You have no UVM — how do you structure a test?" Walk the five phases in order, name who starts the forever loops (env, fork...join_none), what defines stimulus completion (driver counter, not generator return), and when threads are killed (after the drain idle-window). This maps one-to-one onto UVM's run-time phases, which is exactly the point interviewers want you to make.

Key takeaways

  • One env-owned run() task calls reset/config/main/drain/report in order — components never self-phase.

  • Forever-loop components start under fork...join_none; only the generator returns.

  • Stimulus completion = driver's counter reaching the generated count, never the generator returning.

  • disable a labeled fork only after quiescence — and reset TB state wherever you reset the DUT.

Common pitfalls

  • join (not join_none) on forever loops — main_phase hangs forever on threads that never return.

  • Killing monitor/scoreboard threads at end of stimulus — in-flight transactions are never checked.

  • Forgetting scb.flush()/mdl.reset() in reset_phase — stale expected entries fail every compare after reset.

  • Unlabeled disable fork — kills sibling threads (e.g., the watchdog) you meant to keep alive.