Part 2 · OOP for Verification · Intermediate

Lab: Generator → Driver Handshake

A complete runnable mini-lab: transaction class, mailbox pipe, driver with vif stub, and event-based completion.

What we are building

This lab assembles the whole chapter into one runnable testbench: a generator randomizes transactions and puts them into a bounded mailbox #(txn); a driver gets them and wiggles a small interface stub; an event signals per-transaction completion back, and the top-level test waits for the full count before finishing. Copy it into a single file and run it on any simulator (or EDA Playground) — no DUT required.

diagram
LAB SEQUENCE DIAGRAM  (one transaction, repeated N times)

  generator            mailbox(2)            driver               clk/vif
     │                     │                   │                    │
     │ randomize txn       │                   │                    │
     ├── put(t) ──────────►│                   │                    │
     │ (blocks if full)    ├── get(t) ────────►│                    │
     │                     │                   ├── drive addr/data ─►│
     │                     │                   │   @(posedge clk)   │
     │                     │                   │◄── ack (stub) ──────┤
     │                     │                   │                    │
     │◄──────────── -> drv_done (event) ───────┤                    │
     │ done_count++        │                   │                    │
     │                     │                   │                    │
  test: wait (done_count == N)    $finish

The complete code

systemverilog
// ---------- interface stub (stands in for a real DUT connection) ----------
interface bus_if (input bit clk);
  logic        valid;
  logic [15:0] addr;
  logic [31:0] data;
endinterface

// ---------- transaction ----------
class txn;
  rand bit [15:0] addr;
  rand bit [31:0] data;
  constraint c_addr { addr inside {[16'h0000:16'h00FF]}; }
  function string convert2string();
    return $sformatf("addr=%04h data=%08h", addr, data);
  endfunction
endclass

// ---------- generator ----------
class generator;
  mailbox #(txn) mb;
  int unsigned   n;

  function new(mailbox #(txn) mb, int unsigned n);
    this.mb = mb;  this.n = n;
  endfunction

  task run();
    repeat (n) begin
      txn t = new();                 // fresh object EVERY iteration
      assert (t.randomize())
        else $fatal(1, "randomize failed");
      mb.put(t);                     // backpressure: blocks when mb full
      $display("[%0t] GEN : put  %s", $time, t.convert2string());
    end
    $display("[%0t] GEN : all %0d txns generated", $time, n);
  endtask
endclass

// ---------- driver ----------
class driver;
  mailbox #(txn)  mb;
  virtual bus_if  vif;
  event           drv_done;          // fired once per completed txn

  function new(mailbox #(txn) mb, virtual bus_if vif);
    this.mb = mb;  this.vif = vif;
  endfunction

  task run();
    txn t;
    vif.valid <= 0;
    forever begin
      mb.get(t);                            // blocks until work arrives
      @(posedge vif.clk);
      vif.valid <= 1;
      vif.addr  <= t.addr;
      vif.data  <= t.data;
      @(posedge vif.clk);                   // one-cycle "transfer"
      vif.valid <= 0;
      $display("[%0t] DRV : done %s", $time, t.convert2string());
      -> drv_done;                          // completion pulse
    end
  endtask
endclass

// ---------- top-level test ----------
module top;
  localparam int N = 8;
  bit clk;
  always #5 clk = ~clk;

  bus_if bif (clk);

  initial begin
    mailbox #(txn) mb   = new(2);           // small bound → see backpressure
    generator      gen  = new(mb, N);
    driver         drv  = new(mb, bif);
    int unsigned   done_count = 0;

    fork
      gen.run();                            // producer
      drv.run();                            // consumer daemon (forever)
      forever begin                         // completion counter
        @(drv.drv_done);                    // safe: waiter armed before
        done_count++;                       // any trigger can occur
      end
    join_none

    wait (done_count == N);                 // level wait — no race
    $display("[%0t] TEST: %0d/%0d transactions complete", $time,
             done_count, N);
    $finish;
  end
endmodule

Walkthrough — why each piece is shaped this way

  1. txn constructs a NEW object per generator iteration — the mailbox carries handles, so reusing one object would alias every queued entry (the mailbox lesson's pitfall).

  2. mailbox #(txn) new(2) — deliberately small bound so you can watch GEN put messages stall while DRV is mid-transfer; raise N and the bound to see steady-state throttling.

  3. driver gets via virtual bus_if — the class world reaches pins only through a virtual interface handle; here the interface is a stub, but the wiring is exactly what a real bench uses.

  4. -> drv_done with an @(drv.drv_done) waiter is race-free HERE because the counter process is forked and blocked at @ before the first trigger can fire (drives take two clock edges); for same-timestep signaling you would switch to wait(drv_done.triggered).

  5. wait (done_count == N) is a level-sensitive end-of-test condition — immune to ordering, unlike counting edges.

  6. join_none + wait: the driver and counter are forever-loop daemons, so plain join would hang; the test instead blocks on the level condition and exits with $finish.

Experiments to run

  • Set the mailbox bound to 0 (unbounded) and add a #50 delay in the driver — watch mb.num() grow without limit.

  • Move -> drv_done before the waiter is forked and at time 0 — reproduce the lost-trigger race, then fix with .triggered.

  • Replace the done event with a second mailbox of completed txns — that is one step from a scoreboard.

  • Wrap the wait in the watchdog pattern from the process-control lesson, with a 10us limit.

Interview angle

  • "Sketch a generator-driver handshake" is a standard whiteboard task — this lab IS the expected answer: mailbox for data, event or count for completion.

  • Be ready to explain why the mailbox is bounded and what would change with new(0).

  • Map the pieces forward: generator → uvm_sequence, mailbox+get → seq_item_port, drv_done → item_done.

Key takeaways

  • Mailbox for data flow, event for completion, level wait for end-of-test — each primitive doing its one job.

  • A fresh transaction object per put is non-negotiable; handle aliasing is the lab's hidden trap.

  • Bounded mailbox depth is a tuning knob: small to expose flow control, larger for throughput.

  • This structure maps one-to-one onto UVM's sequencer-driver handshake — learn it here, recognize it there.

Common pitfalls

  • Reusing one txn handle in the generator loop — all queued entries mutate together.

  • Joining on the driver's forever loop with plain join — the test never ends.

  • Counting completions with @(drv_done) when triggers can precede the wait — undercounts; use .triggered or a flag.

  • Forgetting $finish after the level wait — daemons keep the simulation alive indefinitely.