Skip to content

SystemVerilog · Module 13

Mailboxes

SystemVerilog mailboxes — the typed FIFO that decouples producer from consumer. Bounded vs unbounded back-pressure, typed vs untyped type safety, put / get / peek / try_* / num semantics, the canonical generator → driver → monitor → scoreboard pipeline, mailbox-in-a-class wiring, the handle-aliasing bug every shop ships at least once, and the UVM TLM-FIFO connection.

Module 13 · Page 13.4

A mailbox is a first-in-first-out queue that passes objects safely between concurrent processes. The producer never has to know when the consumer is ready; the consumer never has to know when the producer will deliver. That decoupling is what makes the generator → driver → monitor → scoreboard pipeline possible — and it is exactly what UVM's TLM ports abstract over.

This is the third and final IPC primitive. Where event signals moments and semaphore gates access, mailbox carries items.

1. Engineering Problem — Why Mailboxes Exist

A SystemVerilog testbench generator runs at host-CPU speed and can emit a million transactions per simulation second. The driver is bound to the DUT's clock and pushes one transaction every ~5 cycles. The monitor sees responses arrive at arbitrary times. The scoreboard wants to compare expected against actual in order — independent of whichever side ran first.

Without a queue between them, the only options are:

  • A shared Txn variable. The producer overwrites it before the consumer reads. Every fast burst of stimulus silently corrupts the test.
  • A dynamic_array of items with hand-rolled put / get indices. Race-prone, full of off-by-one bugs, no built-in blocking semantics.
  • An event + handshake. Works for synchronisation but does not carry data; you still need somewhere to put the object.

The mailbox is the IEEE 1800 answer: a built-in typed FIFO with put() / get() blocking semantics, optional bounded depth for back-pressure, and peek() / try_* / num() for everything in between. Every non-UVM testbench is wired with mailboxes; every UVM testbench wraps them in TLM ports.

2. Mental Model — A Post Office FIFO

The picture every engineer carries:

A mailbox is a post office queue. Senders walk up and put() letters at the back; the recipient picks them up from the front with get() in the order they arrived. If the recipient is slow, letters pile up. If the queue has a fixed capacity (new(N)), senders wait at the door once it is full. If the queue is empty, the recipient sits and waits.

Three invariants this picture preserves:

  • FIFO is contractual. IEEE 1800 specifies first-in-first-out delivery. Unlike semaphores, the order across multiple waiters is defined: get() calls are served in the order they blocked.
  • Each item has one recipient. A put() unblocks exactly one get(), even if ten consumers are waiting. This is unicast, distinct from event broadcast.
  • The queue stores handles, not copies. Class-typed mailboxes pass references. If the producer mutates the object after put(), the consumer sees the mutated state. The fix is to allocate a fresh new() per put() or to clone() — covered in §10 debug labs.

3. Visual Explanation — Producer, FIFO, Consumer

Mailbox FIFO — generator puts at the tail, driver gets from the head

mailbox
Mailbox FIFO — generator puts at the tail, driver gets from the headgeneratorq0q1q2q3driverput()get()FIFO order
Items enter at the tail via put() and leave from the head via get() in FIFO order. The highlighted slot (q0) is the next item the consumer will receive. If the queue is bounded and full, put() blocks; if empty, get() blocks. The handle stored in each slot is a reference to the producer's object — mutations after put() are visible to the consumer.

The diagram captures the canonical pattern. Most testbench wiring is some composition of this figure with the components renamed: monitor → scoreboard, agent → sequencer, reference-model → checker.

4. Syntax & Semantics — new(), put, get, peek, try_*, num

4.1 Declaration forms

SystemVerilog — the four mailbox declaration forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Unbounded, untyped ────────────────────────────────────────
mailbox mb = new();              // grows without limit; can hold any type
 
// ── Bounded, untyped ──────────────────────────────────────────
mailbox mb = new(8);             // max 8 items; put() blocks when full
 
// ── Typed (parameterised) — preferred ─────────────────────────
mailbox #(ApbTxn) mb = new();    // only ApbTxn objects allowed; compile-time check
 
// ── Typed AND bounded — most common in production ─────────────
mailbox #(ApbTxn) mb = new(4);   // typed, bounded — hardware-accurate back-pressure

4.2 The complete method set

SystemVerilog — every mailbox method, blocking and non-blocking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mb.put(item);                // add item — BLOCKS if mailbox is full (bounded only)
mb.get(item);                // remove first item — BLOCKS if mailbox is empty
mb.peek(item);               // copy first item WITHOUT removing — BLOCKS if empty
 
int ok = mb.try_put(item);   // non-blocking put: 1=success, 0=full (bounded) or always 1 (unbounded)
int ok = mb.try_get(item);   // non-blocking get: 1=success, 0=empty
int ok = mb.try_peek(item);  // non-blocking peek: 1=success, 0=empty
 
int n  = mb.num();           // number of items currently in the queue

put adds at the tail; get removes from the head; peek copies the head without removing. The blocking trio (put, get, peek) suspends the calling process; the try_* trio returns immediately with a 0/1 success code.

4.3 Bounded vs unbounded — back-pressure semantics

AspectUnbounded new()Bounded new(N)
put() blocks?Never — always succeeds immediatelyYes — blocks when queue holds N items
get() blocks?Yes — blocks when queue is emptyYes — blocks when queue is empty
Memory riskYes — can grow without bound if producer outruns consumerNo — capped at N items
Models hardware?No — real interfaces always have bounded buffersYes — models actual hardware FIFO depth
Use whenGenerator-to-driver where the bus speed naturally rate-limitsBack-pressure paths, hardware-accurate models, preventing runaway queues

4.4 Typed vs untyped — type safety

SystemVerilog — typed mailbox catches errors at compile time
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── TYPED mailbox (always preferred) ──────────────────────────
mailbox #(ApbTxn) mb_typed = new();
 
ApbTxn t = new();
mb_typed.put(t);              // OK
// mb_typed.put(42);          // COMPILE ERROR — type mismatch caught immediately
 
ApbTxn out;
mb_typed.get(out);            // no cast needed — type already guaranteed
 
// ── UNTYPED mailbox (use only for polymorphic infrastructure) ──
mailbox mb_untyped = new();
 
ApbTxn  apb = new();
AhbTxn  ahb = new();
mb_untyped.put(apb);          // stores a handle of type ApbTxn
mb_untyped.put(ahb);          // stores a handle of type AhbTxn — untyped allows both
 
BaseTxn base;
mb_untyped.get(base);         // get returns a handle — base could be ApbTxn or AhbTxn
 
ApbTxn cast_result;
if (!$cast(cast_result, base))
    $error("Expected ApbTxn but got something else!");

4.5 peek() — inspect without consuming

peek() copies the front item without removing it. Use it when the consumer needs to decide whether to consume — priority routing, conditional draining, ordered processing across multiple queues.

SystemVerilog — peek then conditionally consume
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ApbTxn t;
mb.peek(t);                   // copies the front item into t — item stays in the queue
                              // blocks if queue is empty (just like get)
 
if (t.write)
    mb.get(t);                // conditionally consume only if it is a write transaction
 
// ── Priority router pattern ───────────────────────────────────
task automatic route_transactions();
    Txn t;
    forever begin
        mb.peek(t);                  // look at the head without committing
        if (t.priority == HIGH) begin
            mb.get(t);
            high_prio_queue.put(t);
        end else begin
            mb.get(t);
            low_prio_queue.put(t);
        end
    end
endtask

5. Simulation View — Blocking Semantics and FIFO Order

get() and put() block at the Active region of the current time slot. When put() adds an item, any process suspended on get() is unblocked immediately — in the same time step, no time advance — and resumes in FIFO order across waiters. This is the rule that distinguishes mailboxes from semaphores: with semaphores the wake-up order is unspecified; with mailboxes IEEE 1800 §15.4.3 guarantees first-blocked / first-served.

The same applies to put() on a bounded mailbox: when get() drains a slot, the longest-blocked put() is the one that proceeds.

try_put() and try_get() never block — they read the current state, attempt the operation atomically, and return the 0/1 result in the same Active region.

6. Waveform — Bounded Back-Pressure in Action

A bounded mailbox is the IPC primitive that models hardware FIFO depth. The waveform below shows a fast producer (writes every clock) feeding a slow consumer (reads every 4 clocks) through a mailbox #(Packet) = new(4). The producer fills the queue in 4 cycles, then put() blocks at cycle 5. The consumer drains one slot at cycle 7; the producer's blocked put() immediately unblocks and adds the next packet at cycle 8. The queue oscillates near full thereafter — natural back-pressure with no race, no lost data, no runaway memory.

Figure — bounded mailbox(4): fast producer stalls when full, resumes on each drain

12 cycles
Figure — bounded mailbox(4): fast producer stalls when full, resumes on each drainqueue full → next put() blocksqueue full → next put(…consumer.get() → producer unblocksconsumer.get() → produ…steady-state back-pressuresteady-state back-pres…clkmb.num()012344434443producerputputputputBLOCKBLOCKBLOCKputBLOCKBLOCKBLOCKputconsumergetgetgetgetgetgett0t1t2t3t4t5t6t7t8t9t10t11
Producer enqueues every cycle until num()=4 at cycle 4. The next put() blocks (cycle 5). The slow consumer drains one item every 4 cycles; each drain unblocks the producer's pending put(). The queue oscillates between 3 and 4 items — the hardware analogue is an AXI write-data FIFO with a bus running at line rate and a slow downstream sink.
SystemVerilog — bounded mailbox demonstrates the back-pressure above
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox #(Packet) tx_fifo = new(4);   // max 4 packets in flight
 
initial fork
 
    begin : producer
        for (int i = 0; i < 20; i++) begin
            Packet p = new(); p.id = i;
            tx_fifo.put(p);                       // blocks when 4 items are in the queue
            $display("[GEN] Put packet %0d (queue=%0d)", i, tx_fifo.num());
        end
    end
 
    begin : consumer
        Packet p;
        forever begin
            tx_fifo.get(p);
            #20;                                  // slow consumer — 20 time units per packet
            $display("[DRV] Drove packet %0d", p.id);
        end
    end
 
join_none
// Producer sends quickly but pauses when 4 packets queue up.
// Consumer drains the queue; producer resumes. Natural back-pressure.

7. Synthesis — Not Applicable

mailbox is a simulation-only construct. It has no hardware footprint, no synthesised gates, and no place in synthesisable RTL — synthesis tools reject the keyword outright. This section is intentionally omitted; the topic does not warrant it.

The corresponding hardware idiom in RTL is a synchronous FIFO with full / empty flow-control flags (covered separately under the FIFO and CDC lessons), or an AXI write-data buffer behind a ready / valid handshake. The mailbox is the simulation-side analog of those structures — the verification environment models the hardware FIFO's depth with new(N).

8. Verification View — The Canonical Testbench Pipeline

8.1 Multiple producers and consumers — unicast distribution

Any number of processes can call put() on the same mailbox; items queue in FIFO order regardless of producer. Any number of processes can call get(); each item is consumed by exactly one of them. This is the work-queue pattern — N workers competing for jobs from a shared queue.

SystemVerilog — multi-producer / multi-consumer (work queue)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox #(Job) job_queue = new();
 
initial fork
    // Job generator: fills the queue
    begin
        for (int i = 0; i < 100; i++) begin
            Job j = new(); j.id = i;
            job_queue.put(j);
        end
    end
    // Worker 0: takes jobs as they become available
    begin : worker_0
        Job j;
        forever begin
            job_queue.get(j);              // one item, one worker
            process_job("W0", j);
        end
    end
    // Worker 1: competes with Worker 0 for the same queue
    begin : worker_1
        Job j;
        forever begin
            job_queue.get(j);              // each job goes to exactly ONE worker
            process_job("W1", j);
        end
    end
join_none
// Workers share the load — each job is processed exactly once

8.2 The complete generator → driver → monitor → scoreboard pipeline

The standard SystemVerilog testbench architecture uses mailboxes at every connection point. Each component is an independent process; mailboxes carry data across the boundaries. This is the blueprint for every non-UVM verification environment.

SystemVerilog — full APB testbench pipeline wired with mailboxes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Environment: declares all shared IPC objects ──────────────
mailbox #(ApbTxn) gen2drv  = new();      // generator → driver
mailbox #(ApbTxn) gen2sb   = new();      // generator → scoreboard (reference)
mailbox #(ApbTxn) mon2sb   = new();      // monitor   → scoreboard (actual)
semaphore         bus_lock = new(1);     // one driver on bus at a time
event             all_sent;              // generator signals completion
 
task generator(int n_txns);
    repeat (n_txns) begin
        ApbTxn t = new(); assert(t.randomize());
        gen2drv.put(t);                  // send to driver
        gen2sb.put(t);                   // send reference copy to scoreboard
    end
    ->all_sent;
endtask
 
task driver();
    ApbTxn t;
    forever begin
        gen2drv.get(t);                  // wait for next transaction
        bus_lock.get();                  // acquire bus (the semaphore lesson)
        drive_apb_txn(t);                // apply to DUT
        bus_lock.put();                  // release bus
    end
endtask
 
task monitor();
    ApbTxn t;
    forever begin
        sample_apb_output(t);            // observe DUT response
        mon2sb.put(t);                   // forward to scoreboard
    end
endtask
 
task scoreboard();
    ApbTxn expected, actual;
    int pass_count = 0, fail_count = 0;
    forever begin
        gen2sb.get(expected);            // reference from generator
        mon2sb.get(actual);              // actual from monitor
        if (expected.data == actual.data) pass_count++;
        else begin
            fail_count++;
            $error("[SB] MISMATCH: exp=0x%h got=0x%h at %0t",
                   expected.data, actual.data, $time);
        end
    end
endtask
 
initial begin
    fork
        generator(200);                  // 200 transactions
        driver();
        monitor();
        scoreboard();
    join_none
 
    wait(all_sent.triggered);            // wait for generator (safer than @)
    #5000;                               // drain: let last transactions propagate
 
    if (gen2sb.num() != 0)
        $error("[END] Scoreboard has %0d unchecked transactions", gen2sb.num());
 
    $display("[END] Test complete at %0t", $time);
    $finish;
end

8.3 Mailbox in a class — the building block for reusable agents

Mailboxes are objects; they can be stored as class properties and wired through constructors. This is how reusable verification components — drivers, monitors, scoreboards — are connected without global variables.

SystemVerilog — mailbox as a class property, wired via constructor
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ApbDriver;
    mailbox #(ApbTxn) mbx;             // handle to the input mailbox
 
    function new(mailbox #(ApbTxn) m);
        mbx = m;                       // wired in at construction
    endfunction
 
    task run();
        ApbTxn t;
        forever begin
            mbx.get(t);
            drive_apb(t);
        end
    endtask
endclass
 
class Scoreboard;
    mailbox #(ApbTxn) expected_mbx;
    mailbox #(ApbTxn) actual_mbx;
    int pass_cnt, fail_cnt;
 
    function new(mailbox #(ApbTxn) exp, mailbox #(ApbTxn) act);
        expected_mbx = exp;
        actual_mbx   = act;
    endfunction
 
    task run();
        ApbTxn e, a;
        forever begin
            expected_mbx.get(e);
            actual_mbx.get(a);
            if (e.data == a.data) pass_cnt++;
            else begin fail_cnt++; $error("Mismatch"); end
        end
    endtask
endclass
 
class Env;
    mailbox #(ApbTxn) gen2drv, gen2sb, mon2sb;
    ApbDriver   drv;
    Scoreboard  sb;
 
    function new();
        gen2drv = new(); gen2sb = new(); mon2sb = new();
        drv = new(gen2drv);                  // pass mailbox handle to driver
        sb  = new(gen2sb, mon2sb);           // pass both handles to scoreboard
    endfunction
endclass

Mailbox handles are class references — passing one to a constructor or task does not copy the underlying queue. Both holders see the same queue and the same items.

9. Industry Usage — From Raw SV to UVM

  • UVM uvm_tlm_fifo is a mailbox with TLM ports bolted on. The underlying mechanism is identical: a typed FIFO between two components. UVM adds put_export / get_export for wiring visualisation in Verdi, peek / try-peek API parity, and integration with the phase and reset machinery.
  • UVM uvm_analysis_port + uvm_tlm_analysis_fifo is the broadcast-then-buffer pattern: an analysis port multicasts every monitor observation to every subscribed scoreboard's analysis-FIFO. Each subscribed FIFO is itself a typed mailbox.
  • Sequencer ↔ Driver handshake uses an internal mailbox for the get_next_item / item_done pattern. Understanding raw mailboxes is the prerequisite for debugging UVM sequencer stalls.
  • Block-level testbench pipelines in every protocol VIP (APB / AHB / AXI / DDR / Ethernet / PCIe) use mailboxes at the agent's monitor → scoreboard boundary, even when the rest of the agent is UVM-wrapped.
  • Regression infrastructure (transaction recorders, log aggregators, coverage merging) often uses mailboxes between simulator and external Python / Tcl post-processors via DPI wrappers.
  • Cross-thread debug instrumentation — the +ipc_trace plusarg pattern from the semaphores lesson typically puts trace events into a mailbox the test-end drains and writes to a file.

10. Design Review Notes — What a Senior Will Flag

Pattern in the diffWhat review will say
mailbox mb = new(); (untyped) in new code"Use mailbox #(MyType) — every shop's style guide requires typed mailboxes. Untyped is reserved for genuinely polymorphic infrastructure."
mailbox #(Pkt) mb = new(); (unbounded) where the producer can outrun the consumer"Bound it. Unbounded mailboxes are how regressions silently OOM after hour two — visible only at scale."
Txn t = new(); forever begin t.randomize(); mb.put(t); end (handle aliasing)"Re-using one handle aliases every slot. Allocate a fresh new() each iteration or call mb.put(t.clone())."
mb.get(t); in test-level code with no timeout"Wrap in fork ... join_any with a watchdog. A hung mailbox is harder to debug than a failed one."
if (mb.try_get(t)) ... in a zero-delay forever loop"Busy-spin — no time advance. Add @(posedge clk) or #1. Use the blocking get() whenever possible."
mb.num() checked then mb.get(item) called"Race — between the num() read and the get(), another consumer may have drained the queue. Use try_get or hold the consumer count separately."
mb.put(item); mb.get(item); in the same thread"Pointless — you put then immediately took it back. Either the producer is in a different thread or the mailbox is unnecessary."
Unbounded mailbox with no mb.num() watchdog"Add a debug-only watchdog that fails fast when num() > expected_max. Catches the producer-outpaces-consumer bug at minute one, not hour six."
mailbox mb; with no mb = new(...)"Null handle — mailbox must be constructed. The first put() / get() will throw a null-handle exception."

The single highest-value rule: always typed, always bounded, always cloned unless you have a specific reason otherwise. Three rules cover 90% of mailbox bug classes.

11. Debugging Guide — Real Failures, Real Fixes

1

Random scoreboard mismatches on long tests only

HANDLE-ALIASING
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ApbTxn txn = new();
forever begin
    assert(txn.randomize());           // SAME handle, re-randomized
    gen2drv.put(txn);
    gen2sb.put(txn);                   // both mailboxes hold the same handle
end
Symptom
Tests under 100 txns pass. Tests of 10,000 txns randomly fail at a ~15% rate. The scoreboard's "expected" matches actual on most transactions but the wrong fields on a few. The mismatches cluster around fast-driver intervals.
Root Cause
Single class handle reused across put()s. When the consumer is delayed, the producer re-randomises the same object the consumer is still holding — the scoreboard's expected and the DUT's actual diverge based on whoever wins the race for the handle.
Fix
Allocate a fresh new() each iteration, or pass mb.put(txn.clone()). A 3-line patch eliminates the entire class of bug. UVM coding-style guides at every shop mandate clone() before analysis-port writes for exactly this reason.
2

Simulator OOMs at hour 6 of regression

UNBOUNDED-MAILBOX
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox #(Pkt) gen2drv = new();        // unbounded
 
forever begin
    Pkt p = new(); p.id = id++;
    gen2drv.put(p);                    // 1 Gbps producer
end
 
forever begin
    Pkt p; gen2drv.get(p);
    scoreboard.slow_check(p);          // 10 Mbps consumer
end
Symptom
Regression runs cleanly for ~5 hours, then the simv process is OOM-killed by the LSF grid. Logs show no error; the grid wrapper reports "killed by signal 9". No assertion fired; no $finish reached.
Root Cause
Unbounded mailbox with producer running 100× faster than consumer. Queue grows ~200 MB/s of object allocation. Six hours × 200 MB/s ≈ the grid's per-job memory limit.
Fix
Convert to bounded: mailbox #(Pkt) gen2drv = new(1024). Add a watchdog process that asserts gen2drv.num() < 512 every microsecond and $fatals if violated. Long regressions now fail in under a minute when the consumer dies, instead of OOM-ing silently after hours.
3

Scoreboard hangs after exactly N transactions; N varies per seed

MISSING-GENERATOR-SIGNAL
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task scoreboard();
    forever begin
        gen2sb.get(expected);
        mon2sb.get(actual);            // blocks forever if monitor missed one
        compare(expected, actual);
    end
endtask
Symptom
Test runs to within a few transactions of the planned count, then hangs. Watchdog fires. Log shows N-1 transactions checked successfully; the last one never reaches the scoreboard.
Root Cause
Generator and monitor count divergence — usually the monitor missed a packet (clock-gating window, reset corner, an extra bus_lock.put() releasing too early). The scoreboard's gen2sb.get() succeeds — there's the reference. The mon2sb.get() blocks forever because the actual never arrived. No assertion catches it; the scoreboard simply waits.
Fix
Wrap each get in a timeout: fork mon2sb.get(actual); #1000 $fatal(1, "[SB] mon2sb starved"); join_any disable fork;. The test now fails fast with the exact mailbox and time, dropping debug from days to minutes.
4

Untyped-mailbox runtime cast error in regression

UNTYPED-WRONG-TYPE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox mb = new();                    // untyped
ApbTxn  apb = new(); mb.put(apb);
AhbTxn  ahb = new(); mb.put(ahb);      // both types allowed
 
ApbTxn out;
mb.get(out);                           // gets the AhbTxn first (FIFO)
                                       // — runtime error: cannot assign AhbTxn handle to ApbTxn variable
Symptom
Regression fails sporadically with $cast errors in the scoreboard. The failure depends on producer ordering — visible only when the AHB agent puts before the APB agent. A $cast exception stack trace shows the failure point but not the cause.
Root Cause
Untyped mailbox accepts both ApbTxn and AhbTxn handles. The consumer assumes one type and fails the cast on the other. Type errors that would have been compile-time on a typed mailbox become runtime errors at scale.
Fix
Convert to mailbox #(BaseTxn) mb = new(); if both types share a base, then $cast at the consumer with explicit handling. Better: use two typed mailboxes — mailbox #(ApbTxn) and mailbox #(AhbTxn) — and peek / route at the source.
5

Test hangs only when verbosity is set to NONE

BUSY-SPIN-TRY_GET
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial forever begin
    int ok;
    Pkt p;
    ok = mb.try_get(p);
    if (verbose) $display("got %0d", p.id);
    if (ok) handle(p);
end
Symptom
With +UVM_VERBOSITY=HIGH the test passes. With +UVM_VERBOSITY=NONE the test loops at zero-time, hits the simulator's iteration limit, and the run is killed.
Root Cause
A bare try_get loop with no time advance. The verbose $display call was the only thing yielding to other processes via the underlying I/O. Strip it and you get a busy-spin that starves every other process at t=0.
Fix
Use the blocking get() — the simulator naturally suspends until a put() arrives. If you genuinely need try_get (e.g. you have a fallback), add a @(posedge clk) or #1 at the bottom of the loop to make the time advance explicit.

12. Interview Insights — What Interviewers Actually Probe

A mailbox new() (unbounded) never blocks put() — the queue grows as needed. A mailbox new(N) (bounded) blocks put() when the queue holds N items; the producer waits until the consumer drains a slot.

Prefer unbounded when the producer is naturally rate-limited and you do not need to model hardware back-pressure (e.g. a generator-to-driver mailbox where the driver's bus speed acts as the bottleneck). Prefer bounded whenever the producer can outrun the consumer — unbounded mailboxes silently accumulate handles and turn a slow consumer into a memory leak that surfaces only at regression scale.

13. Exercises

1. Design — bounded mailbox as a hardware FIFO model (Foundation)
Build a mailbox #(Pkt) tx_fifo = new(16) that models an AXI write-data FIFO behind a slow downstream sink. The producer pushes one packet per clock; the consumer drains one per four clocks. Add mb.num() instrumentation that prints the queue depth every cycle. Confirm the queue saturates at 16 and the producer's put() blocks correctly.

2. Debug — the silent OOM (Intermediate)
A regression OOMs after 5 hours with no error message. The testbench uses mailbox #(Pkt) gen2drv = new(); (unbounded) feeding a slow scoreboard. Walk through the diagnostic: which mailbox property would have warned you at minute one, and what is the canonical bounded-mailbox + watchdog fix?

3. Code review — the work queue (Intermediate)
A teammate submits a job-queue implementation: mailbox jobs = new(); (untyped, unbounded) with two worker threads each calling Job j; jobs.get(j);. Identify the three bugs and propose the canonical fixes.

4. Trade-off — typed unbounded vs typed bounded (Advanced)
A junior teammate proposes: "always use mailbox #(T) = new(8) everywhere — bounded by default." Argue the case against the blanket rule. When does an unbounded mailbox legitimately beat a bounded one, and what cost does the blanket rule pay in producer-throughput behaviour, debug complexity, or test-determinism?

14. Summary

A mailbox is a typed FIFO between concurrent processes. The producer put()s at the tail; the consumer get()s from the head; the queue decouples their rates. Bounded mailboxes model hardware back-pressure; typed mailboxes catch errors at compile time. peek() inspects without consuming; try_* are the non-blocking variants; num() reports the depth.

Defaults to memorise. Always typed (mailbox #(MyType)). Always bounded if the producer can outrun the consumer. Always allocate a fresh new() per put() to avoid handle aliasing, or clone() if you must keep the local handle. Wrap test-level blocking get() / put() calls in fork ... join_any with a watchdog — a hung mailbox is harder to debug than a failed test.

This closes Module 13. Across the three IPC primitives:

  • Events signal moments — broadcast, no data, no history. Use wait(triggered) over @ for one-shot triggers.
  • Semaphores gate access — counting credits, atomic multi-key, no fairness guarantee. Single global lock order per environment.
  • Mailboxes carry items — typed FIFO, optional back-pressure, FIFO wake-up order. The shape every UVM TLM port abstracts over.

The next module covers Process Controlfork-join, fork-join_any, fork-join_none, disable fork, wait fork, and the process class.