Module 2 — Procedural Constructs
How SystemVerilog models combinational and sequential logic, controls concurrent processes, and synchronizes parallel threads: always_comb/ff/latch, fork-join flavors, wait, disable, events, mailboxes, and semaphores.
📑 Table of Contents
16always_comb
always_comb models purely combinational logic. Unlike a plain Verilog always @(*), it is checked at compile/elaboration time to ensure the block is truly combinational, and it triggers once automatically at time 0 (no need to wait for a first input change).
| Aspect | always @(*) | always_comb |
|---|---|---|
| Sensitivity List | Automatic, but can miss reads inside function calls | Automatic, includes signals read inside called functions/tasks too |
| Compile-Time Checks | None | Errors if block contains sequential elements (e.g. latches unintentionally, blocking after non-blocking mix) |
| Triggers at Time 0 | No — needs first input event | Yes — executes once automatically at start |
| Multiple Drivers Check | Not enforced | Tool flags if a signal is driven from more than one always_comb |
Always use always_comb instead of always @(*) in new SystemVerilog RTL — it catches unintended latches and incomplete sensitivity lists at compile time, both classic sources of simulation-vs-synthesis mismatches.
17always_ff
always_ff explicitly models edge-triggered sequential logic (flip-flops). The tool enforces that the block contains only non-blocking assignments and a proper edge-based sensitivity list — making synthesis intent unambiguous.
Always use non-blocking (<=) assignments inside always_ff, and blocking (=) inside always_comb. Mixing them is one of the most common RTL coding-guideline violations and a frequent interview question.
The keyword documents design intent for both the reader and the tool — a linter can flag a combinational assignment or blocking assignment accidentally placed inside an always_ff block, something a generic always @(posedge clk) block cannot check.
18always_latch
always_latch explicitly models level-sensitive latch behavior. It's rarely used intentionally in synchronous ASIC design (latches complicate STA), but the construct exists so that when a latch is intended, it's clearly documented rather than an accidental inference.
The #1 reason beginners accidentally infer a latch: an incomplete if (no else) or incomplete case (no default) inside always_comb. The tool will flag this as an error precisely because always_comb disallows latch inference — use that as a safety net.
| Block Type | Models | Assignment Type | Typical Use in ASIC Flow |
|---|---|---|---|
| always_comb | Combinational logic | Blocking (=) | Very common |
| always_ff | Edge-triggered flops | Non-blocking (<=) | Very common |
| always_latch | Level-sensitive latches | Blocking (=) | Rare — clock gating cells, specific low-power structures |
19Process Control
SystemVerilog testbenches run many concurrent "processes" (initial/always blocks, tasks spawned via fork). Process control constructs let you start, pause, terminate, and synchronize these processes.
Process control in SystemVerilog is built from a handful of primitives that work together: fork...join family (spawn concurrency), wait/event triggers (synchronize), disable (terminate named blocks), and the process class (fine-grained suspend/resume/kill). The next sections cover each in depth.
20fork...join (and its variants)
fork...join spawns multiple statements/blocks to run concurrently as separate processes. SystemVerilog offers three join flavors that differ in when the parent process resumes.
| Construct | Parent Resumes When... | Typical Use |
|---|---|---|
| fork...join | All child processes complete | Parallel stimulus that must finish together before proceeding |
| fork...join_any | The first child process completes | Timeout/race patterns (ack vs timeout) |
| fork...join_none | Immediately — doesn't wait for any child | Fire-and-forget background monitors/loggers |
"How do you implement a timeout on a handshake in SystemVerilog?" — Answer: fork...join_any racing the actual wait condition against a delay/timeout branch, followed by disable fork to clean up the loser.
21wait
The wait statement blocks a process until a given expression becomes true — a level-sensitive alternative to edge-triggered @ event control.
@(posedge sig) and wait(sig) are not interchangeable. If sig is already high when execution reaches the statement, wait(sig) proceeds immediately, while @(posedge sig) blocks until the signal toggles low then high again — a frequent source of testbench hangs.
22disable
disable terminates a named block or task/process before it completes naturally — useful for aborting in-progress activity (e.g., on a reset or timeout).
disable <label> kills one specifically named block by its label. disable fork kills all still-running child processes spawned by the current process — it doesn't need (or accept) a label.
23Events
An event is a lightweight synchronization object used to signal occurrences between concurrent processes, decoupled from any specific data value.
Events are common for driver-to-monitor handoffs (e.g., "drive complete" signaling), phase synchronization in custom testbenches, and building simple condition variables before reaching for the heavier mailbox/semaphore classes.
24Mailbox
A mailbox is a built-in class implementing a thread-safe FIFO message queue — the standard mechanism for passing transactions between concurrent testbench components (e.g., generator → driver, monitor → scoreboard).
| Method | Blocking? | Behavior |
|---|---|---|
| put(msg) | Yes | Blocks if mailbox is full (bounded) |
| get(msg) | Yes | Blocks if mailbox is empty |
| try_put(msg) | No | Returns 0 immediately if full instead of blocking |
| try_get(msg) | No | Returns 0 immediately if empty instead of blocking |
| peek(msg) | Yes | Reads without removing from the mailbox |
| num() | N/A | Returns number of items currently queued |
Mailboxes are the conceptual (and sometimes literal, in raw SV testbenches) predecessor to UVM's TLM ports/exports/FIFOs — uvm_tlm_fifo is essentially a parameterized mailbox with added analysis-port broadcast semantics.
25Semaphore
A semaphore is a built-in class that manages a pool of keys, used to control access to a shared resource among multiple concurrent processes (mutual exclusion, N-way resource pooling).
| Feature | Mailbox | Semaphore |
|---|---|---|
| Purpose | Pass data/messages between processes | Control access to a shared resource |
| Carries Data? | Yes (typed messages) | No — just manages a count of keys |
| Typical Use | Generator→Driver, Monitor→Scoreboard | Mutex on a shared bus, limiting concurrent DMA/agents |
| Core Methods | put(), get(), try_get(), peek() | get(), put(), try_get() |
"How do you implement a mutex in SystemVerilog?" — Answer: a semaphore initialized with exactly 1 key. get(1) before the critical section, put(1) after — functionally identical to a binary mutex.
🎯 Module 2 Recap
always_comb/always_ff/always_latchmake design intent explicit and let the tool catch coding mistakes (unintended latches, blocking/non-blocking misuse) at compile time.fork...join / join_any / join_nonegive three flavors of concurrency depending on how long the parent should wait.wait(expr)is level-sensitive and returns immediately if already true — different from edge-sensitive@.disableterminates a named block;disable forkkills all still-running children of the current process.events provide lightweight, data-less synchronization between processes.mailboxmoves typed data safely between concurrent producer/consumer processes — the backbone of generator/driver/monitor/scoreboard communication.semaphoreguards shared resources via a key-based get/put mechanism — a 1-key semaphore is a mutex.
📚 Next up — Module 3: Object-Oriented Programming in SystemVerilog (classes, inheritance, polymorphism, virtual methods, and this vs super)
↑ Back to Table of Contents
Comments
Post a Comment