Interprocess Synchronization and Communication
Semaphores for mutual exclusion, mailboxes for inter-process message passing, the SV event type with persistent .triggered, wait_order() for event sequencing, and event variable assignment and merging.
🔗 Why These Primitives?
Concurrent testbench processes — generators, drivers, monitors, scoreboards — run simultaneously and must coordinate without race conditions. SystemVerilog provides three built-in synchronisation primitives, each solving a specific problem:
Semaphore — mutual exclusion
When only one process at a time should access a shared resource (e.g. a bus driver, a scoreboard). Think of a semaphore as a key dispenser — only the process holding a key can proceed.
Mailbox — message passing
When one process produces data that another consumes (generator → driver, monitor → scoreboard). A FIFO queue with blocking and non-blocking access. The producer never has to wait for the consumer to be ready.
Event — synchronisation signal. When one process needs to signal another that something has happened (e.g. reset done, packet sent). Unlike Verilog events, SV events have a persistent triggered state that lasts the entire time step, eliminating the classic race condition where the waiter runs just after the trigger fires.
🔑 Semaphores
A semaphore is a bucket of keys. A process that wants to enter a critical section calls get() to take a key; when done, it calls put() to return the key. If no keys are available, get() blocks until another process calls put().
// Creating a semaphore with N initial keys semaphore sema4 = new(1); // 1 key = binary semaphore (mutex) semaphore pool = new(4); // 4 keys = counting semaphore // Typical critical-section pattern sema4.get(); // acquire — blocks if key unavailable // ... critical section: only one process here at a time ... sema4.put(); // release — unblocks a waiting process
get() are queued in the order they arrive. When keys are returned, the process that has been waiting longest is unblocked first.
🔨 Semaphore Methods
semaphore sm = new(1); // Blocking get sm.get(); // blocks if no key sm.get(2); // blocks until 2 keys available // Non-blocking try if(!sm.try_get()) $display("Semaphore busy — try later"); // Returning multiple keys at once sm.put(3); // return 3 keys in one call
📋 Semaphore Patterns
Pattern 1 — mutex: protect a shared bus driver
class Driver; semaphore bus_lock = new(1); task drive(Packet p); bus_lock.get(); // take the bus send_on_bus(p); // only one process here at a time bus_lock.put(); // release the bus endtask endclass
Pattern 2 — counting semaphore: limit concurrent workers
semaphore slots = new(4); // max 4 concurrent operations for(int i=0; i<100; i++) begin slots.get(); // wait for a free slot fork automatic int k = i; begin do_work(k); slots.put(); // free the slot when done end join_none end wait fork;
Pattern 3 — non-blocking try with fallback
task automatic try_access(semaphore sm); if(sm.try_get()) begin do_work(); sm.put(); end else begin use_fallback(); // resource busy — do something else instead end endtask
📧 Mailboxes
A mailbox is a FIFO message queue between processes. One process puts messages in; another gets them out. The queue can be bounded (blocks the producer when full) or unbounded (never blocks the producer).
// Unbounded mailbox (default): put() never blocks mailbox mbx = new(); // Bounded mailbox: put() blocks when queue is full mailbox mbxBound = new(16); // max 16 messages // Mailbox is type-less by default — can hold any type Packet p_in = new; Packet p_out; mbx.put(p_in); // stores handle to Packet mbx.get(p_out); // retrieves handle — p_out now points to same object as p_in // A single untyped mailbox can hold different types — but type mismatch = runtime error mbx.put(42); // put an integer mbx.get(p_out); // RUNTIME ERROR — type mismatch: expected Packet, got int
mbx.put(pkt). The driver loops on mbx.get(pkt) and drives each packet onto the DUT interface. The mailbox decouples their rates — the generator can run ahead of the driver without blocking.
🔨 Mailbox Methods
mailbox mbx = new(8); // bounded: 8 messages max Packet p, q; // Producer: send packets foreach(pkts[i]) begin p = new; p.fill(i); mbx.put(p); // blocks if 8 messages already queued end // Consumer: receive and check forever begin mbx.get(q); // blocks if empty check(q); end // Non-blocking check: inspect without consuming if(mbx.try_peek(q) == 1) $display("Next message: len=%0d", q.length);
peek() does not remove the message, any number of processes blocked on peek() or get() for the same mailbox may all unblock when a message arrives. The first process to call get() will remove the message; subsequent get() callers will block again if no more messages remain.
📧 Parameterised Mailboxes
A typed mailbox catches type mismatches at compile time instead of runtime, making testbench bugs easier to find.
// Define a mailbox that only holds strings typedef mailbox #(string) s_mbox; s_mbox sm = new; sm.put("hello"); // OK string s; sm.get(s); // s = "hello" // sm.put(42); // COMPILE ERROR — type mismatch caught at compile time // Parameterised Packet mailbox typedef mailbox #(Packet) pkt_mbx_t; pkt_mbx_t gen2drv = new(); // generator → driver channel pkt_mbx_t mon2scb = new(); // monitor → scoreboard channel
mailbox requires runtime type checking — a mismatch is only caught when get() is called, potentially far from the source of the bug. A mailbox#(Packet) catches mismatches at the point where you try to put() a wrong type, making the error immediately visible.
⚡ Events
SV events extend Verilog named events with three key improvements: a persistent triggered state (lasts the whole time step), the ability to be passed as arguments and assigned to each other, and a non-blocking trigger operator ->>.
// Declare events event done, blast; // Alias: done_too is another name for the SAME synchronisation object as done event done_too = done; // Blocking trigger: -> fires immediately ->done; // Non-blocking trigger: ->> fires in the NBA region (no blocking) ->>done; // Wait for the event: @ blocks until triggered @done; // blocks — may MISS the trigger if it already fired wait(done.triggered); // level-sensitive — NEVER misses the trigger
⚡ Triggering Events — -> vs ->>
-> blocking trigger
// Executes immediately in Active region // All processes waiting on @ev unblock now // Trigger state is instantaneous ->ev; // Statement after trigger executes // in the same time step
->> non-blocking trigger
// Schedules the trigger in the NBA region // Does NOT block the calling process // Useful when triggering from always_ff ->>ev; // Like NBA assignment: fires after active // region completes
Passing events as task arguments
task trigger(event ev); ->ev; endtask event done, done_too = done; fork @done_too; // process A: wait via alias #1 trigger(done); // process B: trigger via task argument join // Both done and done_too refer to the same object. // Triggering done via the task unblocks the waiter on done_too.
✅ Persistent triggered Property — Eliminating the Race
In Verilog, if the trigger fires before the waiter reaches @ev, the waiter is permanently blocked. SV solves this with ev.triggered — a Boolean that stays true for the entire time step after the trigger fires.
@ operator — can miss the trigger
fork ->ev; // P1: triggers ev @ev; // P2: if P1 ran first, P2 BLOCKS FOREVER join
wait(ev.triggered) — never misses
fork ->blast; // P1: triggers wait(blast.triggered); // P2: unblocks even if P1 ran first join // always terminates
// triggered persists through the current time step then resets. // Use @ev when you want edge-triggered (new trigger) semantics. // Use wait(ev.triggered) when you want level-sensitive (was it triggered this step?) semantics. // Common testbench pattern: wait for reset to be released event reset_done; // In the driver: wait safely for reset even if it already fired wait(reset_done.triggered); start_driving(); // In the reset monitor: trigger when reset de-asserts always @(negedge rst_n) ->reset_done;
📋 wait_order() — Event Sequencing
wait_order(a, b, c) suspends the calling process until events a, b, and c have all triggered in that exact left-to-right order. If any event fires out of order, the construct either generates a runtime error (no else clause) or executes the fail statement.
event a, b, c; // Wait for a → b → c in order; runtime error if out of order wait_order(a, b, c); // With a fail statement: display message on failure, no hard error wait_order(a, b, c) else $display("Error: events out of order"); // Capture success/failure in a variable bit success; wait_order(a, b, c) success = 1; else success = 0; // Practical: verify a three-phase handshake fires in the right order // Phase 1 (req) → Phase 2 (grant) → Phase 3 (ack) wait_order(ev_req, ev_grant, ev_ack) $display("Handshake completed correctly"); else $error("Protocol violation: events out of order");
.triggered property — so if the first event already fired before wait_order is reached, the construct still works. Later events in the list must fire in order after the construct starts waiting for them. A preceding event can fire again without causing failure — the ordering only applies to subsequent unforced events.
⚡ Event Variables — Merging, null, and Comparison
Merging events via assignment
Assigning one event to another makes them share the same underlying synchronisation queue. Triggering either one unblocks waiters on both.
event a, b, c; a = b; // a and b now share a synchronisation queue ->a; // also triggers b ->b; // also triggers a a = c; // a now shares c's queue (a and b are no longer merged) b = a; // b now shares a's (= c's) queue // -> a triggers b and c // -> b triggers a and c // -> c triggers a and b
@E2 when you execute E2 = E1, that process will never unblock — the merge only redirects future @E2 operations. To unblock existing waiters through a merge, the merge must happen before the wait.
// The timing trap: merge AFTER the wait starts fork T1: while(1) @E2; // blocks on E2's original queue T2: while(1) @E1; T3: begin E2 = E1; // T1 is ALREADY blocked — merge won't help it while(1) ->E2; // T1 NEVER unblocks; T2 unblocks each time end join
Reclaiming events with null
event E1; E1 = null; // detach E1 from its synchronisation queue @E1; // undefined: may block forever or not at all wait(E1.triggered); // triggered is false when E1 is null ->E1; // no effect (safe to trigger null event)
Event comparison
event E1, E2; if(E1) // 0 if null, 1 otherwise — boolean test E1 = E2; if(E1 == E2) // true if both share the same synchronisation queue $display("E1 and E2 are the same event"); if(E1 != null) ... // explicitly check for null // Legal comparison operators: == != === !== (and boolean test) // Arithmetic and relational operators are NOT legal on event variables
📋 Quick Reference
Semaphore methods
| Method | Type | Behaviour |
|---|---|---|
| new(N) | function | Create with N initial keys |
| get(N=1) | task (blocking) | Acquire N keys; block if unavailable |
| try_get(N=1) | function int | Return 1 if acquired, 0 if not; never blocks |
| put(N=1) | task | Return N keys; unblock waiting processes |
Mailbox methods
| Method | Type | Behaviour |
|---|---|---|
| new(bound=0) | function | 0=unbounded, N>0=bounded |
| num() | function int | Current message count |
| put(msg) | task (blocking) | Enqueue; blocks if bounded and full |
| try_put(msg) | function int | 1=success, 0=full; never blocks |
| get(ref msg) | task (blocking) | Dequeue; blocks if empty; −error on type mismatch |
| try_get(ref msg) | function int | 1=ok, 0=empty, −1=type mismatch |
| peek(ref msg) | task (blocking) | Copy without removing; blocks if empty |
| try_peek(ref msg) | function int | 1=ok, 0=empty, −1=type mismatch |
Event rules at a glance
->ev— blocking trigger; fires in Active region.->>ev— non-blocking trigger; fires in NBA region.@ev— edge-sensitive wait; can miss trigger if it already fired.wait(ev.triggered)— level-sensitive; never misses; stays true whole time step.- Assign one event to another to merge their synchronisation queues.
- Merge only affects future waiters — already-blocked processes are not redirected.
- Assign
nullto break the association; triggering null is a no-op. - Legal comparisons:
==,!=,===,!==, boolean test (if(ev)).
