Part 2 · OOP for Verification · Intermediate

Events, Semaphores, Mailboxes & Processes

Hub — events, semaphores, mailboxes, fork variants, process control, and a generator-driver handshake lab.

Overview

A testbench is a collection of concurrent processes — a generator producing transactions, a driver wiggling pins, a monitor watching them, a scoreboard comparing. None of these run in isolation; they must hand data to each other, share resources, and signal completion. SystemVerilog provides three built-in synchronization primitives — event, semaphore, and mailbox — plus the fork...join family and std::process for spawning and controlling threads.

Choosing the right primitive matters: an event carries no data and signals a moment in time; a semaphore arbitrates access to a shared resource; a mailbox moves data between threads with optional backpressure. Interviewers love this trio because the failure modes — the trigger-before-wait race, semaphore deadlock, unbounded mailbox memory growth — separate engineers who have debugged real testbenches from those who memorized syntax.

Sub-topics

  1. Events & event Triggering — -> vs @, .triggered, handle passing, wait_order.

  2. Semaphores — keys, get/put/try_get, bus arbitration, deadlock.

  3. Mailboxes — bounded vs unbounded, parameterized mailbox, backpressure.

  4. fork...join, join_any, join_none — semantics and the loop-variable bug.

  5. disable fork, wait fork & process — scoping, isolation, fine-grained control.

  6. Lab: Generator → Driver Handshake — complete runnable mini-environment.

diagram
IPC TOPIC MAP

  WHO TALKS TO WHOM, AND HOW

  generator ──── mailbox #(txn) ────► driver         (data transfer)
      │                                  │
      │                                  ▼
      │                            virtual interface ──► DUT pins
      │
      └──── event done ◄──────────── driver          (moment-in-time signal)

  driver A ──┐
             ├──► semaphore bus_sem (1 key) ──► shared bus   (mutual exclusion)
  driver B ──┘

  test ── fork
           ├── generator.run()    \
           ├── driver.run()        ├─ join_none / wait fork  (thread lifecycle)
           └── watchdog timeout   /

  Primitive   Carries data?   Blocking?         Typical use
  ─────────   ─────────────   ───────────────   ─────────────────────────
  event       no              @ / wait blocks   done flags, phase sync
  semaphore   no (keys)       get blocks        resource arbitration
  mailbox     yes             put/get block     producer-consumer pipes

Key takeaways

  • Events signal moments, semaphores arbitrate resources, mailboxes move data — pick by job, not habit.

  • Every fork variant has a distinct blocking semantic; misusing join_none causes most thread bugs.

  • The classic races (trigger-before-wait, loop-variable capture, disable fork killing siblings) are standard interview questions.