Part 6 · Testbench Architecture · Intermediate

Driver Design

The mailbox consumption loop, clocking-block pin driving, handshake timing, idle-state driving, and reset-aware flush and re-init.

The driver's contract

The driver is the only stimulus-side class that touches pins. Its contract: consume transactions from the mailbox one at a time , expand each into legal protocol cycles through the virtual interface's clocking block, drive a well-defined idle state between transactions, and react to reset at any moment. Everything is timing; nothing is decision-making — the generator already decided what to send.

diagram
DRIVER TIMELINE (valid/ready handshake)

  clk     ─┐_┌─┐_┌─┐_┌─┐_┌─┐_┌─┐_┌─┐_┌─
            │   │   │   │   │   │
  state    IDLE│DRIVE──────►│IDLE│DRIVE
                │           │
  valid   ______/▔▔▔▔▔▔▔▔▔▔▔\_____/▔▔▔
  ready   __________/▔▔▔\__________/▔▔
                     ▲
                     └ txn accepted here (valid && ready)

  gap=1 idle cycle ──┘
  driver holds addr/wdata stable while valid && !ready (stall)

A complete reset-aware driver

systemverilog
class driver;
  mailbox #(bus_txn) in;
  virtual bus_if vif;
  int unsigned driven_n;

  function new(mailbox #(bus_txn) in);
    this.in = in;
  endfunction

  task run();
    drive_idle();
    forever begin
      bus_txn t;
      in.get(t);

      // honor generator-requested spacing
      repeat (t.gap) @(vif.drv_cb);

      fork : drive_or_reset
        drive_one(t);
        watch_reset();
      join_any
      disable drive_or_reset;

      if (vif.rst_n !== 1) handle_reset();
    end
  endtask

  task drive_one(bus_txn t);
    @(vif.drv_cb);
    vif.drv_cb.valid <= 1;
    vif.drv_cb.write <= (t.kind == WRITE);
    vif.drv_cb.addr  <= t.addr;
    vif.drv_cb.wdata <= t.wdata;
    t.t_driven = $time;

    // hold everything stable until the DUT accepts
    do @(vif.drv_cb); while (vif.drv_cb.ready !== 1);

    drive_idle();
    driven_n++;
  endtask

  // idle is an explicit, driven state — never Z, never stale
  task drive_idle();
    vif.drv_cb.valid <= 0;
    vif.drv_cb.write <= 0;
    vif.drv_cb.addr  <= '0;
    vif.drv_cb.wdata <= '0;
  endtask

  task watch_reset();
    @(negedge vif.rst_n);
  endtask

  task handle_reset();
    bus_txn dropped;
    drive_idle();                       // pins safe immediately
    while (in.try_get(dropped))         // flush stale stimulus
      $display("DRV: flushed txn%0d on reset", dropped.id);
    @(posedge vif.rst_n);               // wait for release
    repeat (2) @(vif.drv_cb);           // post-reset settle
  endtask
endclass

Code walkthrough

  1. All pin writes go through vif.drv_cb with nonblocking assignments — the clocking block applies the output skew, eliminating drive races.

  2. The ready wait uses !== 1 so an X on ready stalls (and the watchdog catches it) instead of being accepted as truthy garbage.

  3. drive_one holds addr/wdata stable during a stall — many handshake protocols require stability while valid && !ready.

  4. fork...join_any races the transaction against reset; whichever finishes first wins and the other is disabled.

  5. handle_reset does three jobs in order: pins to idle now, flush the mailbox of pre-reset stimulus, re-synchronize after release.


Protocol timing beyond valid/ready

Request/grant protocols add an arbitration wait before driving: request, block until grant, then transfer. The shape of the driver stays identical — only drive_one changes.

systemverilog
task drive_one_req_gnt(bus_txn t);
  @(vif.drv_cb);
  vif.drv_cb.req <= 1;
  do @(vif.drv_cb); while (vif.drv_cb.gnt !== 1);  // arbitration wait
  vif.drv_cb.addr  <= t.addr;                       // transfer phase
  vif.drv_cb.wdata <= t.wdata;
  @(vif.drv_cb);
  vif.drv_cb.req <= 0;
endtask

Interview angle

  • “What does your driver do during reset?” — the three-step answer: idle the pins, flush the mailbox, re-sync after release. Most candidates only have step one.

  • “Why nonblocking through a clocking block?” — the clocking block schedules the drive with output skew after the edge; the design samples the old value at the edge, so no race.

  • “What is your idle state?” — explicitly driven zeros/known values; floating or stale pins make monitor and assertion behavior unrepeatable.

Key takeaways

  • Drivers expand one transaction at a time into protocol cycles; all decisions already happened upstream.

  • Drive only via the clocking block with <= — output skew makes pin driving race-free by construction.

  • Idle is a driven state, applied before the first transaction and after every transaction.

  • Reset handling = idle pins immediately, flush stale mailbox stimulus, re-synchronize after release.

Common pitfalls

  • Driving interface signals directly (vif.valid = 1) instead of through the clocking block — reintroduces the races layering paid to remove.

  • while (!vif.drv_cb.ready) — an X on ready passes the ! test as true-ish in some coding mistakes; compare explicitly with !== 1.

  • Continuing to drive a transaction through reset — the post-reset DUT sees half a protocol transaction.

  • No gap honoring — driver ignores t.gap and the carefully weighted back-to-back distribution never reaches the pins.