Part 6 · Testbench Architecture · Intermediate

Component Handshakes

Generator-driver done signaling, request-response paths, blocking vs nonblocking inter-component calls, and deadlock patterns.

How does the generator know the driver is done?

The naive generator loop — put N transactions into a mailbox and return — finishes long before the driver has driven anything. If the env treats the generator's return as "stimulus done," it starts draining while most transactions still sit in the mailbox. Three mechanisms close the loop, in increasing explicitness.

  • Bounded mailbox — new(1) makes put() block until the driver get()s. The generator's last put returning means the driver has accepted (not finished driving) the last item. Cheapest, slightly loose.

  • Per-item event or done flag — driver triggers ->item_done after fully driving each item; the generator (or env) waits for the last one. Exact, a little more wiring.

  • Completion counter — driver increments items_driven; env waits until items_driven == items_generated. Most explicit, works with any number of observers, and doubles as an end-of-test statistic.

systemverilog
class generator;
  mailbox #(bus_txn) mbx;       // bounded: new(1) in env
  int unsigned n_items = 100;
  event        gen_done;

  task run();
    repeat (n_items) begin
      bus_txn t = new();
      void'(t.randomize());
      mbx.put(t);               // blocks while driver is busy (depth 1)
    end
    -> gen_done;                // last item ACCEPTED by driver
  endtask
endclass

class driver;
  mailbox #(bus_txn) mbx;
  int unsigned items_driven;    // completion counter for the env

  task run();
    forever begin
      bus_txn t;
      mbx.get(t);
      drive_one(t);             // full pin-level protocol
      items_driven++;
    end
  endtask
endclass

// env: wait for true completion, not generator return
// wait (drv.items_driven == gen.n_items);

Request-response and call directions

Some components must talk in both directions: a driver requests the next item and returns a response (read data, error status) that the generator may need before constructing the next transaction. Two mailboxes — one per direction — give the classic request-response channel; the generator blocks on the response when (and only when) the next item depends on it.

systemverilog
// req/rsp channel: generator ⇄ driver
mailbox #(bus_txn) req_mbx = new(1);
mailbox #(bus_txn) rsp_mbx = new(1);

// generator side — reactive stimulus
task generator::run();
  bus_txn req, rsp;
  repeat (n_items) begin
    req = new();
    void'(req.randomize());
    req_mbx.put(req);
    rsp_mbx.get(rsp);                 // block: next item depends on rsp
    if (rsp.resp != 0) error_seen++;  // adapt future stimulus
  end
endtask

// driver side
task driver::run();
  bus_txn req;
  forever begin
    req_mbx.get(req);
    drive_one(req);                   // fills req.data on reads
    rsp_mbx.put(req);                 // same object back as response
  end
endtask

Blocking vs nonblocking calls between components

  • Blocking task call (drv.drive_one(t)) — caller waits for protocol completion; simple, but serializes caller and callee into one logical thread.

  • Nonblocking function call (scb.push_expected(t)) — must complete in zero time; right for handing over data, wrong for anything that consumes time.

  • Mailbox between free-running threads — full decoupling with buffering; the default for any producer-consumer pair in this course.

  • Rule of thumb: functions to transfer data, mailboxes to transfer work, blocking tasks only when the caller genuinely cannot proceed without completion.


Deadlock patterns

diagram
CLASSIC TB DEADLOCKS

  1. CROSSED BLOCKING PUTS (mutual wait)
     gen:    req_mbx.put(req);  rsp_mbx.get(rsp);
     driver: rsp_mbx.put(old);  req_mbx.get(req);
                │                     │
                └──── each waits for the other; both bounded  FROZEN

  2. MISSED EVENT (trigger before wait)
     thread A:  -> done;            // fires at time T
     thread B:  @(done);            // starts waiting at time T (later)
                └─ B waits forever: @ only sees FUTURE triggers

  3. CONSUMER DIED, BOUNDED MAILBOX FULL
     driver thread killed by disable fork
     generator: mbx.put() blocks forever  watchdog is the only escape

  Diagnosis: when the bench hangs, print every mailbox's num()
  and every thread's last-reached line — the full mailbox or the
  un-fired event identifies the loop instantly.

Avoiding them

  1. Order request-response consistently: requester always put-then-get, responder always get-then-put.

  2. Prefer wait(flag == 1) over @(event) when the waiter might start late — a level check cannot miss a past trigger.

  3. Keep a watchdog (previous topic) running — deadlocks then cost one timeout, not a hung regression slot.

  4. When killing threads with disable, drain or rebuild their mailboxes — a dead consumer leaves a full mailbox as a landmine.

Interview angle

Expect "How does your generator know the driver finished?" — name all three mechanisms (bounded mailbox, per-item event, completion counter) and say which signals acceptance vs completion. The follow-up is usually a deadlock hunt: be ready to spot the crossed put/get ordering and the trigger-before-wait race in code on a whiteboard.

Key takeaways

  • Generator returning ≠ stimulus driven — close the loop with a counter, event, or bounded mailbox.

  • Request-response = two mailboxes with a consistent put/get order on each side.

  • Functions move data, mailboxes move work, blocking tasks only when completion truly gates the caller.

  • wait() on a flag beats @(event) whenever the waiter might arrive after the trigger.

Common pitfalls

  • Treating generator return as end of stimulus — drain starts with a mailbox full of undriven items.

  • Crossed put/get ordering on a req/rsp pair of bounded mailboxes — instant mutual deadlock.

  • @(event) after the trigger already fired — the waiter sleeps forever; use a flag plus wait().

  • Unbounded mailbox hiding a slow consumer — memory grows for hours, then the run dies far from the cause.