Skip to content

SystemVerilog · Module 13

Introduction to Inter-Process Communication

Why IPC exists in concurrent SystemVerilog testbenches — the three mechanisms (events, semaphores, mailboxes), where each one fits, the IEEE 1800-2017 scheduling-region rules that govern them, and the canonical bugs (missed-trigger race, cross-locked semaphores, handle aliasing, unbounded mailboxes) every verification engineer ships at least once.

Module 13 · Page 13.1

A real testbench is not one process — it is dozens of them running simultaneously: a driver, a monitor, a scoreboard, a coverage collector, multiple sequencers. Inter-Process Communication (IPC) is the set of tools that lets those concurrent processes talk to each other, wait for each other, and share resources safely. This page introduces the problem, the three mechanisms, and the pitfalls that catch every team that ships a testbench without an IPC discipline.

The Problem IPC Solves

A SystemVerilog testbench is built from multiple concurrent threads — processes launched by fork...join, initial blocks, and always blocks, all running at the same simulation time. When those threads need to coordinate, three fundamental problems appear:

Without IPC — The Problems
  • Race conditions. Two processes write to the same variable at the same simulation time. The result is indeterminate.
  • Deadlock. Process A waits for B to finish, and B waits for A. Neither ever runs.
  • Data loss. A producer generates transactions faster than the consumer processes them. Some get dropped or overwritten.
  • Tight coupling. A driver needs to know exactly when the scoreboard is ready — creating a brittle, hardwired dependency.
With IPC — The Solutions
  • Events. "Signal me when you're done." One-way notification with no data. Pure synchronisation.
  • Semaphores. "Only one process at a time may enter." Mutual exclusion and resource counting. Prevents races.
  • Mailboxes. "Put your data here; I'll pick it up when I'm ready." First-in-first-out queue that buffers data between producer and consumer.

A concrete race the language allows by default, and the canonical fix with a semaphore:

SystemVerilog — the race condition IPC prevents
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Without IPC: two threads updating a shared counter ────────
int shared_count = 0;
 
initial fork
    begin : thread_a
        repeat (1000) begin
            shared_count++;      // READ-MODIFY-WRITE — not atomic
            #1;
        end
    end
    begin : thread_b
        repeat (1000) begin
            shared_count++;      // Thread B races thread A
            #1;
        end
    end
join
// Expected: 2000.  Actual: anywhere from 1000 to 2000. Non-deterministic.
 
// ── With a semaphore: protected critical section ──────────────
semaphore count_lock = new(1);   // 1 key = binary semaphore (mutex)
 
initial fork
    begin : thread_a_safe
        repeat (1000) begin
            count_lock.get();    // acquire the key — wait if taken
            shared_count++;
            count_lock.put();    // release the key
            #1;
        end
    end
    begin : thread_b_safe
        repeat (1000) begin
            count_lock.get();
            shared_count++;
            count_lock.put();
            #1;
        end
    end
join
// Result: always exactly 2000. Deterministic.

The Three IPC Mechanisms

SystemVerilog ships three primitives. Pick by intent — data, signal, or resource — and the rest of the design falls out.

MechanismPurposeCarries Data?Blocks On
eventOne-way trigger between threadsNo@event blocks until next trigger; wait(e.triggered) reads a per-step flag
semaphoreMutual exclusion + counting creditsNoget() blocks when all keys are taken
mailboxFIFO queue of typed objectsYesget() blocks on empty; put() blocks on full (bounded)

Events are a named trigger point — one process fires ->event_name, another waits with @event_name or wait(event_name.triggered). The starting-pistol analogy: it says "go" and nothing more.

Semaphores are a counter that controls access to a shared resource. Initialised with N keys; each get() takes one, each put() returns one. A get() when no keys remain blocks until a key is returned. Think parking lot with N spaces — cars queue at the entrance when full.

Mailboxes are a first-in-first-out queue for passing objects between processes. A producer calls put(item); a consumer calls get(item). The consumer blocks if the mailbox is empty; the producer blocks if a bounded mailbox is full. Think real mailbox — letters pile up until the recipient collects them.

A First Look at Each Mechanism

3.1 Events — One-Way Synchronisation

SystemVerilog — event: driver signals scoreboard when packet sent
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Declare a shared event ────────────────────────────────────
event pkt_sent;     // any process can trigger or wait on this
 
initial fork
 
    // ── Driver process: sends a packet then fires the event ───
    begin : driver
        send_packet();          // task that drives the DUT
        ->pkt_sent;             // trigger: "I just sent one"
        $display("[DRV] Packet sent and event triggered at %0t", $time);
    end
 
    // ── Scoreboard process: waits for the event, then checks ──
    begin : scoreboard
        @pkt_sent;              // block until driver fires ->pkt_sent
        check_output();         // now safe to compare DUT output
        $display("[SB] Check triggered by event at %0t", $time);
    end
 
join

3.2 Semaphores — Mutual Exclusion

SystemVerilog — semaphore: two drivers sharing one bus
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── One key = only one driver on the bus at a time ────────────
semaphore bus_lock = new(1);
 
task automatic drive_bus(string name, int n_transactions);
    repeat (n_transactions) begin
        bus_lock.get();                         // acquire — block if busy
        $display("[%s] Acquired bus at %0t", name, $time);
        drive_transaction();                     // access the bus
        bus_lock.put();                         // release for the other driver
        $display("[%s] Released bus at %0t", name, $time);
    end
endtask
 
initial fork
    drive_bus("DRV-A", 5);    // 5 transactions from driver A
    drive_bus("DRV-B", 5);    // 5 transactions from driver B
join
// Result: DRV-A and DRV-B take turns. They never collide on the bus.

3.3 Mailboxes — Producer-Consumer Queue

SystemVerilog — mailbox: generator feeds driver via a transaction queue
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Shared mailbox: generator puts, driver gets ───────────────
mailbox #(ApbTransaction) gen2drv = new();   // unbounded, typed
 
initial fork
 
    // ── Generator: creates transactions and puts them in the mailbox
    begin : generator
        repeat (20) begin
            ApbTransaction t = new();
            void'(t.randomize());
            gen2drv.put(t);                 // put into the queue — never blocks (unbounded)
            $display("[GEN] Put txn id=%0d", t.id);
        end
    end
 
    // ── Driver: picks up transactions and drives them to the DUT
    begin : driver
        ApbTransaction t;
        forever begin
            gen2drv.get(t);             // get — blocks if mailbox is empty
            drive_apb(t);               // drive to DUT at the bus speed
            $display("[DRV] Drove txn id=%0d at %0t", t.id, $time);
        end
    end
 
join_any
// Generator finishes in 20 iterations; driver runs until generator is done.

Intermediate Behaviour — Where Each Mechanism Gets Tricky

The first-look code above hides the three details that catch every engineer once: events have a missed-trigger race, semaphores have no fairness guarantee, and mailboxes pass class objects by handle. The deep treatment lives in pages 13.2 – 13.4; this is the warning shot.

4.1 Events — the missed-trigger race

@event is an edge-sensitive wait — it blocks until the next trigger. If the trigger fires before the waiter reaches @event, the waiter blocks forever. This is the single most common IPC bug in junior testbenches.

SystemVerilog — the @event missed-trigger race and three fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
event done;
 
// ── Buggy: producer wins the race, consumer hangs ────────────
initial fork
    begin : producer
        #10;
        ->done;                       // fires at t=10
    end
    begin : consumer
        #20;                          // consumer starts late
        @done;                        // waits for NEXT trigger — never comes
        $display("never printed");
    end
join
 
// ── Fix #1: wait(e.triggered) — persistent flag for the full step
@(posedge clk);
wait(done.triggered);          // true if done fired anywhere in THIS step
 
// ── Fix #2: NBA trigger ->> defers the trigger to the NBA region
->>done;                       // consumer reaching @done in Active region still sees it
 
// ── Fix #3: replace the event with a handshake mailbox
mailbox #(int) hs = new(1);    // bounded(1): put blocks until consumed
hs.put(1);                     // producer
hs.get(dummy);                 // consumer — never misses

4.2 Semaphores — counting, not just locking

A semaphore initialised with N keys is a counting semaphore. Passing N=1 degenerates it into a mutex (binary semaphore). Counting semaphores model "at most N concurrent X" — e.g., at most 4 outstanding AXI read requests, at most 8 in-flight DDR commands per bank, at most 16 active DMA descriptors.

SystemVerilog — counting semaphore models AXI outstanding-read limit
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// AXI4 master allowed up to 4 outstanding read requests at once.
semaphore outstanding_reads = new(4);   // 4 credits
 
task automatic issue_read(AxiRdReq r);
    outstanding_reads.get();             // blocks if 4 already in flight
    drive_ar_channel(r);
    // the R-channel monitor calls .put() when the response arrives:
endtask
 
task automatic read_resp_done();
    outstanding_reads.put();             // release one credit
endtask

Two facts that bite engineers:

  • Partial gets are atomic. s.get(3) on a semaphore holding 2 keys blocks — it does not take 2 and wait for 1 more. Either you get all 3 or you wait.
  • No fairness guarantee. IEEE 1800 does not specify which waiting process is unblocked when the lock is released. Two simulators may pick differently; a test that passed on VCS can deadlock on Questa under heavy contention.

4.3 Mailboxes — class objects pass by handle

A typed mailbox of a class type passes handles, not copies. If the producer mutates the object after put(), the consumer sees the mutated version — even retroactively. This is the second most common IPC bug after the missed-trigger race.

❌ Wrong — handle aliasing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Txn t = new();
repeat (3) begin
    void'(t.randomize());
    mb.put(t);   // SAME handle 3x
end
// consumer pulls 3 handles to
// the SAME object — sees only
// the LAST randomization.

All three slots alias one object. Compiles, runs, silently corrupts every test result.

✅ Right — fresh handle per put
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
repeat (3) begin
    Txn t = new();         // fresh
    void'(t.randomize());
    mb.put(t);
end
// Or: mb.put(t.clone()) when
// the local handle must live on.

Each slot holds an independent object. Three distinct randomizations land in the queue.

Choosing the Right IPC Mechanism

SituationUseReason
Notify the scoreboard that a packet just left the drivereventPure trigger, no data to pass
Only one agent should drive the shared APB bus at a timesemaphoreMutual exclusion on a shared resource
Allow up to 4 parallel read requests simultaneouslysemaphore (4 keys)Resource counting — limit concurrent access
Pass transaction objects from the generator to the drivermailboxData transfer between producer and consumer
Scoreboard needs to receive both sent and received packetsTwo mailboxesOne from driver, one from monitor — two queues, one scoreboard
Wait for all 4 channel tests to complete before printing resultsevent or semaphoreBarrier synchronisation — wait for N things to finish
Protect a shared register model from simultaneous reads and writessemaphoreMutual exclusion on a shared data structure

IPC in a Typical Testbench Architecture

The three IPC mechanisms appear at predictable connection points in a well-structured testbench. Understanding where each one belongs makes the architecture immediately readable to any SystemVerilog engineer.

SystemVerilog — typical testbench with all three IPC mechanisms placed correctly
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Shared IPC objects (declared at environment level) ────────
mailbox #(ApbTxn) gen2drv   = new();    // generator  → driver
mailbox #(ApbTxn) mon2sb    = new();    // monitor    → scoreboard
mailbox #(ApbTxn) ref2sb    = new();    // ref model  → scoreboard
semaphore         bus_lock  = new(1);   // one agent on bus at a time
event             test_done;            // generator fires when all txns created
 
// ── Component tasks (simplified) ──────────────────────────────
task generator();
    repeat (100) begin
        ApbTxn t = new(); void'(t.randomize());
        gen2drv.put(t);                  // mailbox → driver
        ref2sb.put(t);                   // mailbox → reference model input
    end
    ->test_done;                          // event → test done
endtask
 
task driver();
    ApbTxn t;
    forever begin
        gen2drv.get(t);                  // wait for mailbox item
        bus_lock.get();                   // semaphore → exclusive bus access
        drive_apb(t);
        bus_lock.put();                   // release bus
    end
endtask
 
task monitor();
    ApbTxn t;
    forever begin
        sample_apb(t);
        mon2sb.put(t);                   // mailbox → scoreboard
    end
endtask
 
task scoreboard();
    ApbTxn expected, actual;
    forever begin
        ref2sb.get(expected);            // wait for reference
        mon2sb.get(actual);              // wait for actual
        compare(expected, actual);
    end
endtask
 
// ── Top-level test ────────────────────────────────────────────
initial begin
    fork
        generator();
        driver();
        monitor();
        scoreboard();
    join_any                              // run until generator fires test_done
    @test_done;                           // event → wait for all txns generated
    #1000;                                // drain the pipeline
    $finish;
end

Simulator Scheduling — Which Region IPC Runs In

Race conditions in concurrent testbenches are always solvable once you map every IPC call to a scheduling region from IEEE 1800-2017 §4.4. The full region order in one time step is: Preponed → Active → Inactive → NBA → Observed → Reactive → Postponed. The IPC operators land in two of them.

IPC OperatorRegion Where It Fires / ResumesWhy it matters
->e (blocking trigger)Active region of the current time stepAny process sitting on @e in the same Active region is unblocked immediately — but a process that reaches @e later in the same Active region misses the trigger.
->>e (non-blocking trigger)NBA region of the current time stepDefers the trigger so that waiters about to evaluate @e in the same Active region all see it. Use when producer and consumer share the same clock edge.
wait(e.triggered)Persistent flag for the full time stepReturns true if e was triggered any time during this step — Preponed through Postponed. Immune to the missed-trigger race.
mailbox.put(t)Active region; unblocks any waiting get() in the same regionIf producer and consumer share the same simulation time, the consumer resumes inside the same Active region — no time advance.
mailbox.get(t)Active region; blocks until a corresponding put()Resumption is FIFO across waiters (IEEE 1800-2017 §15.4.3), unlike semaphores.
semaphore.get()Active region; resumption order is unspecifiedTwo waiters competing for the same released key may be served in any order — tool-dependent. Never rely on FIFO semaphores.

Common Mistakes — Wrong vs Right

Each pair below compiles cleanly on every commercial simulator. The lesson is in what happens at run-time, in coverage closure, or at the next regression sweep.

8.1 Using an event when you needed a mailbox

❌ Wrong — event carries no data
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
event got_pkt;
Pkt   shared_pkt;
 
initial fork
    begin
        shared_pkt = recv();
        ->got_pkt;
    end
    begin
        @got_pkt;
        sb.check(shared_pkt);     // stale?
    end
join

Producer overwrites shared_pkt before consumer reads it. Two packets in fast succession corrupt the second check.

✅ Right — mailbox carries the data
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox #(Pkt) pkts = new();
 
initial fork
    begin
        Pkt p = recv();
        pkts.put(p);
    end
    begin forever begin
        Pkt p;
        pkts.get(p);
        sb.check(p);
    end end
join

Every produced packet lands in its own slot. The consumer never sees stale data, even at full producer rate.

8.2 Forgetting to release a semaphore on the failure path

❌ Wrong — early return leaks the key
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task drive_one(Txn t);
    bus.get();
    if (t.illegal) return;   // LEAK
    drive_apb(t);
    bus.put();
endtask

After the first illegal transaction the bus key is gone forever. Every subsequent driver hangs.

✅ Right — put on every exit path
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task drive_one(Txn t);
    bus.get();
    if (!t.illegal)
        drive_apb(t);
    bus.put();              // always
endtask

Single exit point. The acquire/release pair is balanced regardless of which branch ran.

8.3 Unbounded mailbox with a slow consumer

❌ Wrong — memory grows forever
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox #(Pkt) q = new();      // unbounded
 
initial forever begin
    q.put(gen.next());         // 1 Gbps
end
initial forever begin
    Pkt p; q.get(p);
    scoreboard.slow_check(p);  // 10 Mbps
end

Producer outruns consumer 100×. Mailbox grows without bound; simulator OOMs around hour two of regression.

✅ Right — bounded backpressure
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mailbox #(Pkt) q = new(256);   // bounded
 
initial forever begin
    q.put(gen.next());         // blocks at 256
end
initial forever begin
    Pkt p; q.get(p);
    scoreboard.slow_check(p);
end

Producer stalls naturally when the queue fills. Memory stays bounded; the simulation slows but never crashes.

8.4 Acquiring two semaphores in opposite orders

❌ Wrong — cross-locked deadlock
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task A();
    bus.get();
    reg_model.get();
    ...
endtask
task B();
    reg_model.get();
    bus.get();              // deadlock
    ...
endtask

A holds bus and waits for reg_model. B holds reg_model and waits for bus. Classic textbook deadlock.

✅ Right — fixed global lock order
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Convention: bus before reg_model.
// Document this once in env_pkg.
task A();
    bus.get();
    reg_model.get();
    ...
endtask
task B();
    bus.get();
    reg_model.get();
    ...
endtask

Both threads request the locks in the same order. One waits politely behind the other; no cycle is ever formed.

Debugging Academy — IPC Bugs from Real Projects

Five post-mortems from production verification environments. Each is a real failure mode that has shipped at least once; the names are generic, the mechanics are exact.

1

Scoreboard misses the last packet of every test

MISSED-TRIGGER
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    repeat (N) drive_one();
    ->test_done;                       // Active region
end
initial forever begin
    @sb_in;
    check(...);                        // last @sb_in shares $time with ->test_done
end
initial begin @test_done; disable fork; $finish; end
Symptom
Coverage shows 99.4% across 5,000 random seeds. The missing 0.6% is always a check that should have fired for transaction N. Regression "looks fine" — only the cumulative coverage report exposes it.
Root Cause
->test_done in the Active region triggers @test_done in the closer process before the scoreboard fork can re-arm @sb_in. disable fork kills the scoreboard mid-check.
Fix
Replace the test-closer with wait(test_done.triggered); #1; disable fork;. The extra #1 lets the scoreboard fork drain at the same time step.
2

VCS regression passes, Questa regression deadlocks

LOCK-ORDER
Buggy Code
Two sequences acquire bus_lock and reg_lock in opposite orders. VCS happened to schedule them in a way that never triggered the cycle; Questa, after a routine version upgrade, did.
Symptom
Nightly regression on Questa hangs at simv_time = 3.2 ms across 41 of 800 tests, repeatable. VCS reports clean. Two weeks chasing a non-existent simulator bug.
Root Cause
Cross-locked semaphores. IEEE 1800 leaves wake-up order unspecified; different simulators make different choices and either one is conformant.
Fix
Define one global lock-acquisition order in env_pkg and review every get() against it. Add a lint rule: any task that calls .get() on two semaphores must call them in the documented order.
3

Random scoreboard mismatches on long tests only

HANDLE-ALIASING
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
forever begin
    void'(txn.randomize());           // SAME handle, re-randomized
    mb.put(txn);
end
Symptom
Tests under 100 txns pass. Tests of 10,000 txns randomly fail at a ~15% rate. Diff log shows "expected" matches actual on most txns but the wrong fields on a few.
Root Cause
Single class handle reused across put()s. When the consumer is delayed, the producer randomises the same object the consumer is still holding — the "expected" model and the DUT diverge on whoever wins the race.
Fix
Allocate a fresh new() each iteration, or call mb.put(txn.clone()). A 5-line patch eliminated the entire failure class.
4

Simulator OOMs at hour 6 of regression

UNBOUNDED-MAILBOX
Buggy Code
Generator runs forever; coverage closer skipped one coverage_done condition; consumer task never reaches its mb.get() loop because it sits behind a forgotten wait(0); in a fix that never got removed.
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".
Root Cause
Unbounded mailbox. Producer at 200 MB/s of object allocation; consumer at 0 B/s. Six hours × 200 MB/s ≈ the grid's per-job memory limit.
Fix
Convert mailbox to bounded (new(1024)) and add a watchdog: a separate process that asserts mb.num() < 512 every microsecond and fails fast if the queue grows beyond half-full. Long regressions now fail in under a minute when the consumer dies.
5

Test hangs only when verbosity is set to NONE

BUSY-SPIN
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial forever begin
    int ok;
    ok = mb.try_get(t);
    if (verbose) $display("got %0d", t.id);
    if (ok) handle(t);
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.
Fix
Use the blocking get() instead — the simulator naturally suspends until a put(). If you genuinely need try_get, put a @(posedge clk) or #1 at the bottom of the loop to make the time advance explicit.

Senior VE Tips & Industry Insights

Interview Questions — IPC

Twelve questions drawn from recent rotations at Intel, Cadence, NVIDIA, Qualcomm, AMD, Apple, and Arm. Difficulty ramps from beginner to debugging.

fork/join only launches concurrent threads; it does not let them coordinate. IPC supplies the three primitives — event, semaphore, mailbox — that let those threads signal each other (event), serialise access to shared state (semaphore), and transfer data (mailbox). Without IPC every multi-thread testbench reduces to either a race or a deadlock.

Best Practices Checklist

  • Pick the mechanism by intent, not convenience. Data → mailbox. Signal → event. Shared resource → semaphore. Mixing them produces brittle code that the next engineer cannot read.
  • Always parameterise mailboxes. mailbox #(MyType) mb = new(); — never the bare mailbox. Type errors caught at put() are debuggable; errors caught at get()'s cast are not.
  • Default to wait(e.triggered) for one-shot events. Reserve @e for cases where the waiter is guaranteed to be armed before the producer fires.
  • Bound mailboxes if the producer can outrun the consumer. Unbounded mailboxes turn a slow consumer into a memory leak that surfaces only at regression scale.
  • Pair every get(N) with a matching put(N) on every exit path. Early returns and exceptions inside a critical section are how testbenches leak resources.
  • Define one global lock-acquisition order per environment. Document it in the env-package header. Lint every multi-semaphore task against it.
  • Wrap blocking IPC in fork ... join_any with a timeout. A hung simulation is harder to debug than a failed one; the watchdog tells you which IPC primitive stalled and at what depth.
  • Clone class objects before putting into a cross-thread mailbox. Mailboxes carry handles. Re-using a handle aliases the queue.
  • In UVM, prefer TLM primitives over raw IPC. uvm_tlm_fifo, uvm_analysis_port, and uvm_event integrate with reset, phasing, and Verdi visualisation; raw mailboxes do not.
  • Add a +ipc_trace plusarg that logs every put/get/-> with $time and depth. Off in regression, on for bring-up. Four out of five IPC bugs are diagnosable in minutes with the trace, hours without it.

What This Module Covers

This page was the overview. The remaining pages go deep into each mechanism — every method, every edge case, every interview-relevant detail.

  • 13.2 Events — Declaring events, triggering with -> and ->>, waiting with @ and wait(triggered), the triggered-vs-persistent distinction, named events passed as arguments, and the race condition that catches beginners.
  • 13.3 Semaphores — Creating semaphores, get()/put()/try_get(), binary semaphores (mutex), counting semaphores, deadlock prevention, and real testbench patterns including bus arbitration and register model protection.
  • 13.4 Mailboxes — Bounded vs unbounded mailboxes, typed vs untyped, put()/get()/peek()/try_get()/try_put(), multiple producers and consumers, and the full generator → driver → monitor → scoreboard pipeline.

Quick Reference — IPC at a Glance

SystemVerilog — IPC mechanisms cheat sheet
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Events ────────────────────────────────────────────────────
event  e;                    // declaration
->e;                         // trigger (fires and returns immediately)
->>e;                        // non-blocking trigger (scheduled in NBA region)
@e;                          // wait for the NEXT trigger of e (blocking)
wait(e.triggered);           // true if e was triggered THIS time step (non-blocking)
 
// ── Semaphores ────────────────────────────────────────────────
semaphore s = new(N);        // N initial keys
s.get(k);                    // get k keys — blocks if unavailable (default k=1)
s.put(k);                    // return k keys                         (default k=1)
int ok = s.try_get(k);       // non-blocking get: returns 1 on success, 0 if unavailable
 
// ── Mailboxes ─────────────────────────────────────────────────
mailbox          mb  = new();     // untyped, unbounded
mailbox          mb  = new(N);    // untyped, bounded (N slots)
mailbox #(MyType) mb = new();     // typed, unbounded
mb.put(item);                // put — blocks if full (bounded)
mb.get(item);                // get — blocks if empty
mb.peek(item);               // peek — like get but does NOT remove from queue
int ok = mb.try_get(item);   // non-blocking get: 1=success, 0=empty
int ok = mb.try_put(item);   // non-blocking put: 1=success, 0=full
int n  = mb.num();           // number of items currently in the mailbox
 
// ── Decision ──────────────────────────────────────────────────
// No data, just a signal   → event
// Protect shared resource  → semaphore
// Pass data between procs  → mailbox