Skip to main content

Module 2 — Procedural Constructs

VLSI Verification  •  SystemVerilog Series

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.

🧩 Intermediate ⏱ ~28 min read 💻 Code Examples Included

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).

SystemVerilog
logic [3:0] a, b; logic [3:0] sum; logic cout; always_comb begin {cout, sum} = a + b; // sensitivity list is automatic — includes a, b end // Priority mux example logic [1:0] sel; logic [7:0] d0, d1, d2, d3, y; always_comb case (sel) 2'b00: y = d0; 2'b01: y = d1; 2'b10: y = d2; default: y = d3; endcase
Aspectalways @(*)always_comb
Sensitivity ListAutomatic, but can miss reads inside function callsAutomatic, includes signals read inside called functions/tasks too
Compile-Time ChecksNoneErrors if block contains sequential elements (e.g. latches unintentionally, blocking after non-blocking mix)
Triggers at Time 0No — needs first input eventYes — executes once automatically at start
Multiple Drivers CheckNot enforcedTool flags if a signal is driven from more than one always_comb
💡 Best Practice

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.

SystemVerilog
// Simple D flip-flop with async active-low reset always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) q <= '0; // non-blocking assignment ONLY else q <= d; end // Synchronous reset variant always_ff @(posedge clk) begin if (!rst_n) counter <= 8'd0; else if (enable) counter <= counter + 1'b1; end // Shift register always_ff @(posedge clk) begin shift_reg <= {shift_reg[6:0], serial_in}; end
⚠️ Golden Rule

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.

📘 Why "always_ff" over plain "always"?

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.

SystemVerilog
logic gate_en, d_in, q_out; always_latch begin if (gate_en) q_out = d_in; // transparent while gate_en is high // implicit "else: hold previous value" -- that's what makes it a latch end
🚫 Accidental Latch Bug

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 TypeModelsAssignment TypeTypical Use in ASIC Flow
always_combCombinational logicBlocking (=)Very common
always_ffEdge-triggered flopsNon-blocking (<=)Very common
always_latchLevel-sensitive latchesBlocking (=)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.

SystemVerilog
// process class handle — obtained via process::self() initial begin process p; p = process::self(); $display("status = %s", p.status().name()); // suspend / resume / kill another process's execution // p.suspend(); p.resume(); p.kill(); end // process::status returns an enum: FINISHED, RUNNING, // WAITING, SUSPENDED, KILLED
📘 The Building Blocks

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.

SystemVerilog
// join — parent waits for ALL forked processes to complete fork drive_reset(); drive_clock(); monitor_bus(); join $display("all 3 processes finished"); // join_any — parent resumes as soon as ANY one process finishes fork wait_for_ack(); wait_for_timeout(); join_any $display("first of the two completed"); // note: the other process keeps running in background unless disabled // join_none — parent continues IMMEDIATELY, doesn't wait at all fork background_logger(); join_none $display("fired off logger, moving on right away"); // Common pattern: fork/join_any + disable fork to implement a timeout fork begin fork wait_for_ack(); begin #1000; $display("TIMEOUT!"); end join_any disable fork; // kill whichever sibling is still running end join
ConstructParent Resumes When...Typical Use
fork...joinAll child processes completeParallel stimulus that must finish together before proceeding
fork...join_anyThe first child process completesTimeout/race patterns (ack vs timeout)
fork...join_noneImmediately — doesn't wait for any childFire-and-forget background monitors/loggers
💡 Interview Favorite

"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.

SystemVerilog
// Basic wait — blocks until condition is true (checked immediately; // if already true, does NOT block at all) initial begin wait (ready == 1'b1); $display("ready asserted, proceeding"); end // wait vs @ — key difference @(posedge ready); // blocks until the NEXT rising edge, even if ready is already 1 wait (ready); // returns immediately if ready is already 1 // wait fork — waits for all processes spawned by the CURRENT // process (via join_none) to complete initial begin fork task_a(); task_b(); join_none wait fork; // now block until task_a and task_b both finish end
⚠️ Classic Gotcha

@(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).

SystemVerilog
// Disable a named block initial begin : main_block fork begin : timeout_watch #500; $display("timeout — aborting drive_seq"); disable drive_seq; // kill the named block below end begin : drive_seq drive_transaction(); drive_transaction(); end join end // disable fork — kills ALL processes spawned by the calling process // that haven't completed yet (commonly paired with join_any) task automatic run_with_timeout(); fork wait_for_response(); begin #1000; end join_any disable fork; // terminate whichever branch is still active endtask
📘 disable vs disable fork

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.

SystemVerilog
event data_ready; // declare an event // Process A: waits for the event initial begin @(data_ready); // block until triggered $display("data_ready fired at time %0t", $time); end // Process B: triggers the event initial begin #100; -> data_ready; // trigger (non-blocking style) end // Persistent trigger (stays "triggered" this delta -- useful with wait) initial begin ->> data_ready; // non-blocking trigger scheduled in NBA region end // event.triggered — check if an event fired in the current time step if (data_ready.triggered) $display("already triggered this timestep"); // Event as a task synchronization handoff (common in scoreboards) event e1 = e2; // event aliasing — e1 and e2 now refer to same sync object
💡 Where Used

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).

SystemVerilog
// Unbounded mailbox mailbox #(transaction) gen2drv_mbx; gen2drv_mbx = new(); // Bounded mailbox — put() blocks if the mailbox is full mailbox #(transaction) bounded_mbx; bounded_mbx = new(4); // capacity of 4 // Producer (e.g. generator) task automatic generate_items(); transaction tr; repeat (10) begin tr = new(); tr.randomize(); gen2drv_mbx.put(tr); // blocking put end endtask // Consumer (e.g. driver) task automatic drive_items(); transaction tr; forever begin gen2drv_mbx.get(tr); // blocking get -- waits if empty drive_to_dut(tr); end endtask // Non-blocking variants and introspection if (gen2drv_mbx.try_get(tr)) $display("got item without blocking"); $display("num pending = %0d", gen2drv_mbx.num());
MethodBlocking?Behavior
put(msg)YesBlocks if mailbox is full (bounded)
get(msg)YesBlocks if mailbox is empty
try_put(msg)NoReturns 0 immediately if full instead of blocking
try_get(msg)NoReturns 0 immediately if empty instead of blocking
peek(msg)YesReads without removing from the mailbox
num()N/AReturns number of items currently queued
📘 UVM Connection

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).

SystemVerilog
// Semaphore acting as a MUTEX (1 key = exclusive access) semaphore bus_lock; bus_lock = new(1); // initialize with 1 key task automatic access_shared_bus(); bus_lock.get(1); // acquire (blocks if no key available) // ... critical section: drive the shared bus ... bus_lock.put(1); // release the key endtask // Semaphore as a resource pool (e.g., N available DMA channels) semaphore dma_channels; dma_channels = new(4); // 4 channels available task automatic use_dma_channel(); dma_channels.get(1); // take one channel // ... perform DMA transfer ... dma_channels.put(1); // return the channel endtask // Non-blocking try_get if (dma_channels.try_get(1)) $display("got a channel immediately"); else $display("all channels busy");
FeatureMailboxSemaphore
PurposePass data/messages between processesControl access to a shared resource
Carries Data?Yes (typed messages)No — just manages a count of keys
Typical UseGenerator→Driver, Monitor→ScoreboardMutex on a shared bus, limiting concurrent DMA/agents
Core Methodsput(), get(), try_get(), peek()get(), put(), try_get()
💡 Interview Tip

"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_latch make design intent explicit and let the tool catch coding mistakes (unintended latches, blocking/non-blocking misuse) at compile time.
  • fork...join / join_any / join_none give 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 @.
  • disable terminates a named block; disable fork kills all still-running children of the current process.
  • events provide lightweight, data-less synchronization between processes.
  • mailbox moves typed data safely between concurrent producer/consumer processes — the backbone of generator/driver/monitor/scoreboard communication.
  • semaphore guards 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

Popular posts from this blog

Module 1 — SystemVerilog Basics

VLSI Verification  •  SystemVerilog Series Module 1 — SystemVerilog Basics A complete, example-driven walkthrough of SystemVerilog fundamentals: data types, variables vs nets, arrays of every flavor, strings, enums, structs, unions, and typedef — the building blocks every verification engineer needs before UVM. 🧩 Beginner → Intermediate ⏱ ~25 min read 💻 Code Examples Included 📑 Table of Contents Introduction to SystemVerilog Verilog vs SystemVerilog Data Types: 2-State vs 4-State Logic Variables and Nets Arrays: Packed vs Unpacked Dynamic Arrays Queues Associative Arrays Strings Enumerations Structures Unions Typedef 1 Introduction to SystemVerilog SystemVerilog (IEEE 1800) is a unified hardware description and verification language (HDVL). It extends Verilog (IEEE 1364) with powerful constructs for design (interfaces, always_comb/always_ff, enhanced d...

GPU Memory System & Interconnect: How Data Travels Inside a Modern GPU

GPU Memory System & Interconnect: How Data Travels Inside a Modern GPU Modern GPUs deliver incredible computing power by processing thousands of operations simultaneously. While compute cores often receive the spotlight, the real performance of a GPU depends heavily on how efficiently data moves between memory and compute units. A GPU memory system is designed to provide extremely high bandwidth while keeping latency as low as possible. This is achieved through a collection of specialized hardware components working together, including the Memory Controller, HBM/GDDR memory, Crossbar, Network-on-Chip (NoC), DMA Engine, PCIe Interface, and NVLink. In this article, we'll explore each component in detail and understand how they collectively enable high-performance parallel computing. Why Does GPU Memory Matter? Every GPU application—from gaming and graphics rendering to AI model training and scientific simulations—requires continuous movement of enormous amounts of data. For examp...

Clock Domain Crossing (CDC): A Complete Beginner's Guide to Metastability, Synchronizers & Safe Data Transfer

Clock Domain Crossing (CDC): A Complete Beginner's Guide to Metastability, Synchronizers & Safe Data Transfer Modern System-on-Chip (SoC) designs rarely operate with a single clock. Instead, different modules such as CPUs, GPUs, memory controllers, communication interfaces, and peripherals often run at different clock frequencies or even use completely independent clock sources . When data moves from one clock domain to another, timing uncertainty can cause serious problems such as metastability , data corruption , and unpredictable system behaviour . This challenge is known as Clock Domain Crossing (CDC) . CDC is one of the most important topics in ASIC Design , RTL Design , ASIC Verification , FPGA Design , and SoC Architecture . Every modern chip—from smartphones to automotive processors—uses CDC techniques to ensure reliable communication between multiple clock domains. In this article, we'll explore CDC fundamentals, metastability, synchronization techniques, asynchro...

AXI Verification Interview Questions and Real-World Debugging

AMBA Protocol Series · Part 10 of 10 AXI Verification Interview Questions and Real-World Debugging Common protocol violations, how to read them off a waveform, and a curated set of interview questions with concise, correct answers — the capstone of the core 10-day series. AMBA Protocol Series: From APB & AHB to AXI — A Complete Guide for VLSI Engineers This wraps up the core 10-day series. Everything here draws directly on Days 1–9 — if any answer below feels unfamiliar, that's a good signal for which earlier post to revisit. (If you want to go further, see the 20-day extended list at the end of this post.) In This Article Common Protocol Violations Debugging From a Waveform Interview Questions and Answers Series Recap Extending the Series 1. Common Protocol Violations VALID deasserted before handshake — a source drops VALID before READY ever went high (violates the Day 8 stabi...

Cache Basics and Cache Mapping Techniques

  Cache Basics and Cache Mapping Techniques  Modern processors execute billions of instructions every second. However, processor speed has increased much faster than main memory (RAM) speed. This difference, known as the memory wall , creates a performance bottleneck because the CPU often has to wait for data from slower memory. To solve this problem, computer systems use cache memory —a small, high-speed memory located between the CPU and the main memory. In this article, we will understand the fundamentals of cache memory, the principle of locality, cache organization, and the three major cache mapping techniques. What is Cache Memory? Cache memory is a small, high-speed memory that stores copies of frequently accessed data from the main memory. Since accessing cache is much faster than accessing RAM, the CPU first checks whether the required data is available in the cache. If the data is found in the cache, it is called a cache hit . Otherwise, the CPU fetches the dat...

Timing Basics in Static Timing Analysis (STA): Delay Models, Sequential Timing, and Wire Delays

Timing Basics in Static Timing Analysis (STA) Static Timing Analysis (STA) is one of the most important verification techniques used in modern VLSI design. Before a chip is manufactured, engineers must verify that every signal reaches its destination within the required clock period. Even if a design passes functional simulation, it may still fail after fabrication because of timing violations. Unlike simulation, Static Timing Analysis mathematically analyzes every timing path without applying input test vectors. This makes STA significantly faster and capable of analyzing millions of timing paths efficiently. Table of Contents What is Static Timing Analysis? Why STA is Required Propagation Delay Contamination Delay Cell Delay Models Combinational Gate Delay Sequential Timing Setup Time Hold Time Wire Delay RC Delay Model PVT Corners Worked Examples Interview Questions Key Takeaways What is Static Timing Analysis? Static Ti...

Computer Architecture Interview Question Part 1

Skip to content Computer Architecture Interview Series Part 1 · CPU Basics · RISC vs CISC · ISA 50 Most Asked Computer Architecture Interview Questions (Part 1) The foundational round every VLSI, ASIC, and CPU design interview opens with: instruction set architecture, RISC vs CISC trade-offs, Von Neumann vs Harvard design, registers, endianness, and the fetch-decode-execute cycle — explained the way interviewers actually expect you to answer.  40 Questions (Q1–Q40) ⌛ 22 min read  VLSI & Computer Architecture Interviews On this page Foundations of Computer Architecture RISC vs CISC — Core Differences Von Neumann, Harvard & the Semantic Gap Architecture Styles, Endianness & Registers ILP, VLIW, Superscalar & Amdahl's Law Register Files, Microcontrollers & the Fetch Cycle Tip...