Mailbox in System Verilog

In short: A mailbox in SystemVerilog is a built in class that lets two parallel processes pass data safely, like a FIFO with a producer on one side and a consumer on the other. The producer calls put() to add an item and the consumer calls get() to take one. put() and get() block when the mailbox is full or empty; the try_ versions do not block. A mailbox can be bounded (limited depth) or unbounded, and it can be type parameterised for safety.

In a testbench a generator often builds transactions while a driver sends them to the design. These run in parallel, so they need a safe way to hand data across. A mailbox is that hand off. It holds items in order and manages the waiting for you, so the producer and consumer never step on each other.

A simple way to picture it

Think of a pass through window between a kitchen and a dining room. The cook places finished plates on the window ledge, and the waiter picks them up. If the ledge is full, the cook waits for space. If the ledge is empty, the waiter waits for the next plate. Neither one needs to know what the other is doing moment to moment; the ledge keeps the flow orderly. The mailbox is that window, the cook is the producer, and the waiter is the consumer.

Mailbox methods

A mailbox is a built in class with a small set of methods. The blocking ones pause the calling process until the action can complete. The non blocking ones return right away with a success flag.

MethodBlocking?What it does
new(bound)n/aCreates a mailbox. A bound of 0 means unbounded.
put(item)YesAdds an item; waits if the mailbox is full
get(item)YesRemoves an item; waits if the mailbox is empty
peek(item)YesCopies the front item without removing it
try_put(item)NoAdds if there is room; returns 0 if full
try_get(item)NoRemoves if available; returns 0 if empty
num()NoReturns the current number of items

Bounded vs unbounded mailbox

When you create a mailbox with new(N), N sets the maximum number of items. A bounded mailbox becomes full at N items, so a put() beyond that waits until the consumer frees a slot. Creating with new() or new(0) makes it unbounded, so put() never waits for space.

A bound is useful when you want back pressure, that is, you want a fast producer to slow down and match a slower consumer. Leave it unbounded when the producer should never be held back.

Example 1: generator and driver with a parameterised mailbox

This is the pattern you will use most: a generator randomises transactions and puts them in a mailbox, while a driver gets them and sends them on. Using a parameterised mailbox mailbox#(transaction) means the tool checks the type at compile time, so you cannot accidentally put the wrong object in.

class transaction;
  rand bit [7:0] addr;
  rand bit [7:0] data;
endclass

class generator;
  task run(int n, mailbox #(transaction) mb);
    transaction tr;
    repeat (n) begin
      tr = new();
      if (!tr.randomize())
        $error("randomize failed");
      mb.put(tr);                 // blocks if mb is full
      $display("GEN: put addr=%02h data=%02h", tr.addr, tr.data);
    end
  endtask
endclass

class driver;
  task run(int n, mailbox #(transaction) mb);
    transaction tr;
    repeat (n) begin
      mb.get(tr);                 // blocks if mb is empty
      $display("DRV: got addr=%02h data=%02h", tr.addr, tr.data);
    end
  endtask
endclass

module tb;
  initial begin
    mailbox #(transaction) mb = new();   // unbounded
    generator gen = new();
    driver    drv = new();
    fork
      gen.run(3, mb);
      drv.run(3, mb);
    join
  end
endmodule

The generator and driver run in the same fork join. When the generator puts a transaction, the driver that is waiting in get() wakes up and processes it. This is a clean producer and consumer link.

Example 2: back pressure with a bounded mailbox

Here the mailbox holds only two items. The producer runs fast but is forced to wait whenever the buffer is full, so it naturally matches the slower consumer.

module bounded_demo;
  mailbox #(int) mb = new(2);   // depth of 2
  initial begin
    fork
      // Producer: tries to push 5 items quickly
      begin
        for (int i = 0; i < 5; i++) begin
          mb.put(i);            // waits when 2 items are pending
          $display("[%0t] put %0d (num=%0d)", $time, i, mb.num());
        end
      end
      // Consumer: reads one every 10 ns
      begin
        int v;
        repeat (5) begin
          #10ns;
          mb.get(v);
          $display("[%0t] got %0d", $time, v);
        end
      end
    join
  end
endmodule

Example 3: non blocking check with try_get

Sometimes a process should not stop and wait. Use try_get() to peek for work without blocking; it returns 0 when the mailbox is empty so the process can do something else and check again later.

module nonblock_demo;
  mailbox #(int) mb = new();
  initial begin
    int v;
    if (mb.try_get(v))
      $display("got %0d", v);
    else
      $display("mailbox empty, doing other work");   // this branch runs
  end
endmodule

Expected output, in plain words

In example 1 you see three GEN lines and three DRV lines interleaved, each matching pair sharing the same addr and data, because the driver takes exactly what the generator put. In example 2 the producer prints its first two puts right away, then pauses; each time the consumer takes one every 10 ns, the producer is freed to put the next, so puts and gets alternate. In example 3 the mailbox is empty, so try_get returns 0 and the empty branch prints.

Note: these outputs describe what the code is written to produce from a read of the IEEE 1800 LRM. Run it on your own simulator or EDA Playground to confirm.

Generic vs parameterised mailbox

A plain mailbox is type less: it can hold any type, and you get flexibility at the cost of type checking. A parameterised mailbox#(type) only accepts one type, so the compiler catches a wrong put. For testbench work, prefer the parameterised form because it removes a whole class of bugs.

Pointmailboxmailbox#(type)
Types allowedAny typeOne fixed type
Type checkingAt run time onlyAt compile time
Best forQuick or mixed dataMost testbench code

When to use a mailbox

  • You have a producer and a consumer running in parallel that must pass data in order.
  • You want built in waiting so the producer blocks when full and the consumer blocks when empty.
  • You want back pressure between a fast producer and a slow consumer (use a bounded mailbox).
  • You want a clean hand off between a generator and a driver, or a monitor and a scoreboard.

Common mistakes to avoid

  • Forgetting to call new(): using a null mailbox handle gives a run time error.
  • Mixing put and get counts: if the consumer expects more items than the producer sends, its get() blocks forever.
  • Using a type less mailbox by habit: prefer mailbox#(type) so type errors are caught early.
  • Confusing peek() and get(): peek copies without removing, so a loop on peek without get never drains the mailbox.
  • Assuming put() never waits: on a bounded mailbox it does wait when full.

Mailbox vs queue

A queue is a plain data structure you index directly, with no built in waiting or thread safety. A mailbox is built for communication between parallel processes and manages the blocking for you. Use a queue for storage inside one process, and a mailbox to pass data between two processes.

To coordinate the processes that share a mailbox, see our guide on fork join. If you need mutual exclusion instead of data passing, read about the semaphore. For the data structures a mailbox carries, see array types and queues. More in our SystemVerilog tutorials.

Frequently asked questions

What is a mailbox in SystemVerilog?

A mailbox is a built in class that lets two parallel processes pass data safely, like a FIFO with a producer and a consumer. The producer calls put to add items and the consumer calls get to take them.

What is the difference between put and try_put?

put is blocking: it waits if the mailbox is full. try_put is non blocking: it adds the item if there is room and returns a nonzero value, or returns 0 immediately if the mailbox is full.

What is the difference between a bounded and unbounded mailbox?

A bounded mailbox is created with new(N) and holds at most N items, so put waits when it is full. An unbounded mailbox is created with new() and never blocks a put for lack of space.

What is the difference between get and peek?

get removes and returns the front item. peek copies the front item without removing it, so the item stays in the mailbox for the next reader.

What is the difference between a mailbox and a queue?

A queue is a plain data structure with no built in waiting. A mailbox is built for communication between parallel processes and blocks automatically when full or empty. Use a queue inside one process and a mailbox between two.

Why use a parameterised mailbox?

mailbox#(type) accepts only one type, so the compiler catches a wrong put at compile time. A plain mailbox is type less and only checks at run time, which is more error prone.

Similar Posts