Part 2 · OOP for Verification · Intermediate

Mailboxes

Bounded vs unbounded mailboxes, put/get/peek/try_*, parameterized mailbox #(type), and backpressure.

A FIFO with blocking semantics

A mailbox is a built-in FIFO class for passing data between processes — the primitive events and semaphores lack. new() or new(0) creates an unbounded mailbox (put never blocks); new(N) with N > 0 creates a bounded mailbox holding at most N entries, where put blocks when full. That blocking-put behavior is not a limitation — it is the mechanism for backpressure , automatically throttling a fast producer to the pace of a slow consumer.

The method set

  • put(item) — blocking append; suspends when a bounded mailbox is full.

  • get(item) — blocking remove of the oldest entry; suspends when empty.

  • peek(item) — blocking copy of the oldest entry WITHOUT removing it.

  • try_put / try_get / try_peek — non-blocking variants returning success status.

  • num() — current entry count; a snapshot only, stale by the time you act on it.

diagram
BOUNDED MAILBOX, DEPTH 4 — BACKPRESSURE IN ACTION

  generator (1 txn / 5ns)         mailbox #(txn) mb = new(4)        driver (1 txn / 20ns)

  put put put put put...   ───►   ┌────┬────┬────┬────┐   ───►     get ... get ...
                                  │ t4 │ t3 │ t2 │ t1 │
                                  └────┴────┴────┴────┘
  t=0..15 : puts t1..t4 succeed       FULL at t=15
  t=20    : 5th put BLOCKS ──────►  driver gets t1 ──► slot frees ──► put resumes
  steady state: generator auto-throttled to 1 txn / 20ns (driver's rate)

  Unbounded (new(0)): puts never block  queue grows without limit if
  consumer is slower  memory creep over a long soak run.

Parameterized mailboxes: mailbox #(type)

A bare mailbox is typeless — it accepts any class handle and any singular type, and type errors surface only at runtime when a get target mismatches. The parameterized form mailbox #(bus_txn) moves that check to compile time and lets the compiler infer the element type at get. Always use the parameterized form in new code; the typeless form survives mainly in legacy benches.

systemverilog
class bus_txn;
  rand bit [31:0] addr;
  rand bit [31:0] data;
  function string convert2string();
    return $sformatf("addr=%0h data=%0h", addr, data);
  endfunction
endclass

class generator;
  mailbox #(bus_txn) mb;
  int unsigned       count;

  function new(mailbox #(bus_txn) mb, int unsigned count);
    this.mb = mb;  this.count = count;
  endfunction

  task run();
    repeat (count) begin
      bus_txn t = new();
      assert (t.randomize());
      mb.put(t);                       // blocks if bounded mailbox is full
      $display("[%0t] gen: put %s", $time, t.convert2string());
    end
  endtask
endclass

class driver;
  mailbox #(bus_txn) mb;

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

  task run();
    bus_txn t;
    forever begin
      mb.get(t);                       // blocks until a txn is available
      $display("[%0t] drv: got %s", $time, t.convert2string());
      #20;                             // pin-level driving takes time
    end
  endtask
endclass

module top;
  initial begin
    mailbox #(bus_txn) mb  = new(4);   // depth 4 → natural backpressure
    generator          gen = new(mb, 20);
    driver             drv = new(mb);
    fork
      gen.run();
      drv.run();
    join_any                            // generator finishing ends the test
    wait (mb.num() == 0);               // drain before stopping
  end
endmodule

Handles, peek, and sizing decisions

A mailbox of class objects transfers handles, not copies . If the generator keeps mutating a transaction after putting it, the driver sees the mutations — a notorious heisenbug. The producer must construct a new object per put (or put a copy()), never recycle one handle in a loop.

peek for non-destructive inspection

peek lets an arbiter or scoreboard inspect the head entry without consuming it — useful when one of several consumers must decide whether the next item is theirs. Combined with try_peek it enables polling designs that never steal another consumer's data.

Interview angle

  • "Bounded vs unbounded — which for generator-to-driver?" — bounded; backpressure bounds memory and models realistic flow control.

  • "Why does the driver see fields it never received?" — handle aliasing; producer reused one object instead of constructing per put.

  • "mailbox vs queue with semaphore?" — mailbox bundles the FIFO, the blocking, and the atomicity into one primitive.

Key takeaways

  • Mailboxes move data; put/get block, try_* poll, peek inspects without consuming.

  • Bounded mailboxes give free backpressure — the producer auto-throttles to the consumer's rate.

  • Always parameterize: mailbox #(type) turns runtime type mismatches into compile errors.

  • Class mailboxes pass handles — construct a fresh object per put or aliasing bugs follow.

Common pitfalls

  • Reusing one transaction handle in the produce loop — every queued entry aliases the same mutating object.

  • Unbounded mailbox with a slow consumer — unnoticed memory growth across a long regression.

  • Trusting num() for flow decisions — another process can change the count between the read and your act.

  • Calling get in two consumers expecting broadcast — a mailbox delivers each item exactly once.