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.
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.
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
endmoduleHandles, 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.