Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
      • Events: Edge Waits and Level Fixes
      • Semaphores: One Key and Many
      • Mailboxes: Typed Pipes with Backpressure
      • Threads and Control: Fork, Disable, Wait, Process
      • Synchronization Patterns: Handshakes, Phasing and Objections
      • Lab: Generator-to-Driver Handshake
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Events, Semaphores, Mailboxes and Processes
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Events, Semaphores, Mailboxes and Processes
  5. Mailboxes: Typed Pipes with Backpressure

Part 2 · OOP for Verification · Intermediate

Mailboxes: Typed Pipes with Backpressure

Bounded and unbounded pipes, backpressure, typed handles and sizing.

On this page

    Why this matters

    Memory climbed all night in a soak test. The generator filled an unbounded mailbox much faster than the driver emptied it.

    After this lesson you can: pass objects through typed mailboxes, choose bounded or unbounded, and avoid the reused-handle trap.

    A pipe with blocking ends

    A mailbox is a FIFO for data between processes. put blocks when a bounded box is full, get blocks when it is empty, and peek copies the head entry without removing it.

    The nonblocking try_put, try_get and try_peek return status instead of blocking. num reports a snapshot count that can go stale before it is acted on.

    Call

    When full or empty

    Use

    put and get

    Block

    Default producer-consumer flow

    peek

    Blocks when empty

    Inspect without consuming

    try_put, try_get, try_peek

    Return 0

    Polling without stealing

    num

    Snapshot only

    Debug, never flow control

    Bounded means backpressure

    new(0) builds an unbounded box where put never blocks. new(N) caps the depth, so a fast producer stalls until the consumer frees a slot.

    That stall is backpressure, and it throttles the producer to the consumer rate for free. An unbounded box with a slow consumer instead grows memory all soak run.

    A depth-4 mailbox between a fast generator and a driver that takes one item every 20 ns fills after four puts. The fifth put blocks until the driver frees a slot, and from then on the generator runs at the driver's rate, not its own.

    Handles, peek and sizing

    A mailbox of class objects moves handles, not copies. The producer must construct a fresh object per put, or every queued entry aliases one mutating object.

    Always write mailbox #(T) with a type parameter. The bare form defers type checks to run time, while the parameterized form fails at compile time.

    Predict first The mailbox below has depth 4, the generator puts 20 items as fast as it can, and the consumer takes one every 20 ns. Does the generator finish long before the consumer, or near the end?

    Near the end. After four puts the box is full, so every later put waits for a get; the generator ends only a few items ahead of the consumer, which is what the check measures.

    A slow consumer drains the box, and the check proves the generator was throttled. randomize() fills each item's rand fields with random values; the Randomization part teaches it.

    top_mbx.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    class bus_txn_c;
      rand bit [31:0] addr;
      rand bit [31:0] data;
    endclass
    class generator_c;
      mailbox #(bus_txn_c) mb;
      int unsigned count;
      function new(mailbox #(bus_txn_c) m, int unsigned c);
        mb = m;
        count = c;
      endfunction
      task run();
        repeat (count) begin
          bus_txn_c t = new();
          void'(t.randomize());
          mb.put(t);
        end
      endtask
    endclass
    module top_mbx;
      timeunit 1ns;
      timeprecision 1ps;
      mailbox #(bus_txn_c) mb;
      generator_c gen;
      time gen_done_at;
      int got = 0;
      initial begin
        mb  = new(4);          // declare above, construct here in order
        gen = new(mb, 20);
        fork
          begin
            gen.run();
            gen_done_at = $time;
          end
          repeat (20) begin    // slow consumer: one item per 20 ns
            bus_txn_c t;
            #20;
            mb.get(t);
            got++;
          end
        join
        if (got != 20) $fatal(1, "FAIL: consumer got %0d of 20", got);
        if (gen_done_at < 300) $fatal(1, "FAIL: generator finished at %0d ns, never throttled", gen_done_at);
        $display("PASS: 20 random items through a depth-4 mailbox, generator throttled until %0d ns", gen_done_at);
        $finish;
      end
    endmodule
    Terminal
    PASS: 20 random items through a depth-4 mailbox, generator throttled until 320 ns

    Common pitfall

    Two classic faults: reusing one transaction handle in the produce loop so all entries mutate together, and trusting num for flow decisions while another process changes the count between the read and the act.

    Interview

    Likely questions: bounded versus unbounded for generator-to-driver, a driver seeing fields it never received, and mailbox versus a queue plus semaphore.

    • Bounded, since backpressure bounds memory and models flow control.

    • Handle aliasing from a recycled producer object.

    • The mailbox bundles FIFO, blocking and atomicity in one primitive.

    Check yourself A generator reuses one transaction object for every put. What does the driver see?

    Every queued entry is the same object, so all of them show whatever the generator wrote last. Build a fresh object for each put.

    Check yourself The scoreboard needs the head item but must leave it for the real consumer. Which call fits, and why not get?

    peek, since it copies without removing. get would steal the item and starve the consumer.

    Without a mailbox: the same backpressure by hand

    Six ordered items through a depth-2 store with an instant producer and a 20ns consumer. The stall flag proves backpressure engaged.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    module tb_pipe_backpressure;
      timeunit 1ns;
      timeprecision 1ps;
      int mem[2];
      int head = 0;
      int tail = 0;
      int count = 0;
      int errors = 0;
      int stalled = 0;
      int consumed = 0;
      initial begin : producer
        for (int i = 0; i < 6; i++) begin
          if (count == 2) stalled = 1;
          wait (count < 2);
          mem[tail] = i;
          tail = (tail + 1) % 2;
          count++;
        end
      end
      initial begin : consumer
        int want = 0;
        int got = 0;
        repeat (6) begin
          wait (count > 0);
          got = mem[head];
          head = (head + 1) % 2;
          count--;
          if (got !== want) errors++;
          want++;
          consumed++;
          #20;
        end
      end
      initial begin
        wait (consumed == 6);
        if (stalled !== 1) errors++;
        if (count !== 0) errors++;
        if (errors == 0) $display("PASS: 6 items in order through a depth-2 pipe with stalls");
        else $fatal(1, "FAIL: consumed=%0d stalled=%0d", consumed, stalled);
        $finish;
      end
    endmodule
    Terminal
    PASS: 6 items in order through a depth-2 pipe with stalls

    The lab at the end of this topic builds PacketFIFO's stimulus path on this pattern: generator, mailbox, driver.

    Remember

    • A bounded mailbox blocks put when full: free backpressure.

    • Mailboxes move handles: build a fresh object for every put.

    • Write mailbox #(T); use num() for debug only.

    Rule of thumb: bound the mailbox to the rate you want.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Related topics

    • Next: Threads and Control

    Previous

    56 / 259

    Next: Threads and Control: Fork, Disable, Wait, Process

    Up next

    Up nextThreads and Control: Fork, Disable, Wait, Process

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Events: Edge Waits and Level Fixes
    • Synchronization Patterns: Handshakes, Phasing and Objections
    • Lab: Generator-to-Driver Handshake