SystemVerilog
One language covering three jobs: describing hardware that synthesizes into gates, specifying properties that formal and simulation tools check, and building the object-oriented testbenches that verify it all.
!!! info "Framing for this handbook"
SystemVerilog is often described as "Verilog plus verification features," which undersells it. It is a **superset of Verilog-2005** that merged three previously separate worlds — the Verilog HDL, the Vera/OpenVera verification language, and assertion languages like PSL — into a single IEEE standard (**IEEE 1800**).
For an ASIC designer, the important consequence is that only a well-defined **synthesizable subset** maps to hardware; everything else exists to test that hardware. Knowing exactly where that boundary sits is the single most valuable thing on this page.
1. What SystemVerilog Is
!!! abstract "Core idea"
SystemVerilog is a **hardware description and verification language (HDVL)**. The same file format that describes a flop can also declare a class, randomize a transaction, and assert a protocol rule. Tools decide what to do with each construct: a synthesis tool ignores or errors on non-synthesizable code; a simulator runs all of it.
Lineage
Verilog-1995 (IEEE 1364-1995)
│
Verilog-2001 (IEEE 1364-2001) ANSI ports, generate, signed types
│
Verilog-2005 (IEEE 1364-2005)
│
├── + Vera / OpenVera (classes, constrained random, coverage)
├── + assertion concepts (PSL / Sugar lineage → SVA)
├── + C-like data types (int, struct, enum, typedef)
▼
SystemVerilog 3.1a (Accellera, 2004)
│
IEEE 1800-2005 first IEEE SystemVerilog standard
IEEE 1800-2009 Verilog (1364) folded IN — one unified standard
IEEE 1800-2012 / -2017 / -2023 incremental refinement
Since 2009 there is no separate Verilog standard. IEEE 1800 is Verilog. When someone says "we write Verilog," in a modern ASIC flow they almost always mean the SystemVerilog RTL subset.
The problems it was built to solve
-
Intent was invisible
In Verilog,
always @(*)could accidentally infer a latch, andalways @(posedge clk)could accidentally infer combinational logic. Nothing in the code stated what you meant.Fix:
always_comb,always_ff,always_latch— tools now check intent against inference. -
Port lists didn't scale
A 200-signal bus between two blocks meant 200 port declarations in every module, repeated in every instantiation, edited in five places on every change.
Fix:
interfaceandmodportbundle signals and direction once. -
Testbenches were toys
Verilog had no classes, no randomization, no functional coverage. Real verification lived in a separate language (Vera, e) with a fragile inter-language bridge.
Fix: OOP, constrained-random stimulus, covergroups, mailboxes — all native. UVM is a library on top of this.
-
No way to state a rule
"Grant must follow request within 4 cycles" lived in a spec document, not in the code, and drifted from reality.
Fix: SystemVerilog Assertions — temporal properties that both simulation and formal tools consume.
2. The Language in Layers
┌──────────────────────────────────────────────────────────────────────┐
│ SystemVerilog (IEEE 1800) │
├───────────────────┬───────────────────┬──────────────────────────────┤
│ DESIGN LAYER │ ASSERTION LAYER │ VERIFICATION LAYER │
│ (synthesizable) │ (SVA) │ (simulation only) │
├───────────────────┼───────────────────┼──────────────────────────────┤
│ module │ assert property │ class / extends │
│ always_ff │ sequence │ rand / randc / constraint │
│ always_comb │ property │ covergroup / coverpoint │
│ logic / bit │ cover property │ mailbox / semaphore │
│ struct / enum │ assume property │ program / clocking block │
│ interface │ |-> |=> ##N │ fork...join_any/none │
│ generate / for │ $rose $stable │ virtual interface handle │
│ parameter │ disable iff │ DPI-C imports │
├───────────────────┴───────────────────┴──────────────────────────────┤
│ Consumed by: │
│ Synthesis ──► design layer only (errors/ignores the rest) │
│ Simulation ──► all three layers │
│ Formal ──► design layer + assertion layer │
│ Lint/CDC ──► design layer, plus assertions as extra intent │
└──────────────────────────────────────────────────────────────────────┘
!!! warning "The one rule that matters"
A construct being *legal SystemVerilog* tells you nothing about whether it becomes hardware. `#10ns`, `initial`, `class`, `real`, dynamic arrays, and `while` loops with data-dependent bounds all simulate fine and all fail (or silently mislead) in synthesis.
Keep design files and verification files in separate directories and separate filelists. It's the cheapest guardrail you can put in a flow.
3. Data Types
4-state vs. 2-state
A 4-state type holds 0, 1, X (unknown/conflict), and Z (high-impedance). A 2-state type holds only 0 and 1.
| Type | States | Width | Signed? | Use for |
|---|---|---|---|---|
logic |
4 | 1 bit (or packed) | unsigned | Default choice for all RTL signals. Replaces both reg and wire in most places. |
wire |
4 | 1 bit (or packed) | unsigned | Only when you actually need a net with multiple drivers / resolution (tri-state, bidirectional pads). |
bit |
2 | 1 bit (or packed) | unsigned | Testbench counters, models, anything where X-propagation would just be noise. |
byte |
2 | 8 | signed | TB scoreboards, DPI interop. |
shortint / int / longint |
2 | 16 / 32 / 64 | signed | Loop indices, TB math. int unsigned for unsigned. |
integer |
4 | 32 | signed | Legacy Verilog. Prefer int. |
real / shortreal |
— | float | signed | Analog models, timing math. Never synthesizable. |
string |
— | dynamic | — | Messages, TB only. |
!!! tip "Why RTL wants 4-state"
X-propagation is a debug feature, not a nuisance. An uninitialized flop or an unreached case arm shows up as `X` in waveforms and poisons downstream logic loudly. If you model RTL in `bit`, that same bug reads as a clean `0` and hides until silicon.
**Use `logic` in design, `bit` in testbenches.**
logic vs. reg vs. wire
This trips up everyone coming from Verilog. The old reg/wire split was never about storage — it was about how the value gets assigned.
VERILOG (two type systems, procedural vs continuous)
reg q; // assigned inside always/initial (procedural)
wire y; // assigned by assign or module output (continuous)
→ "reg" implies memory to newcomers. It doesn't. reg can be pure combo.
SYSTEMVERILOG
logic q; // assign it EITHER way — procedurally OR continuously.
// A single-driver variable. The compiler enforces
// single-driver, which catches real bugs.
Still need `wire` when:
- multiple drivers must be resolved (tri-state bus, inout pad)
- you need a net type with a strength/resolution function
Rule of thumb: logic everywhere; wire only for multi-driver nets.
Packed vs. unpacked arrays
The dimension position relative to the identifier decides everything.
// PACKED — dimensions BEFORE the name. A contiguous vector of bits.
logic [31:0] word; // one 32-bit value; can do arithmetic on it
logic [3:0][7:0] word_bytes; // same 32 bits, addressable as 4 bytes
word_bytes[2] // → bits [23:16]. Legal, free in hardware.
// UNPACKED — dimensions AFTER the name. A collection of separate objects.
logic [31:0] mem [0:1023]; // 1024 separate 32-bit words → infers a RAM
logic [31:0] mem [1024]; // same thing; [size] means [0:size-1]
// The distinction in one line:
// packed = one number you can slice, add, shift, compare
// unpacked = many numbers you can index, iterate, and infer memory from
// Combine them freely:
logic [3:0][7:0] fifo [16]; // 16 entries, each 4 bytes / 32 bits
!!! warning
You cannot do arithmetic on an unpacked array as a whole, and you cannot pass it through a port that expects a packed vector. When a design won't compile with a cryptic width error, a packed/unpacked mismatch is the usual culprit.
User-defined types
=== "enum"
Strongly-typed named constants. Synthesizable, and the single biggest readability win in FSM code.
```systemverilog
typedef enum logic [2:0] {
IDLE = 3'b001, // explicit encoding — one-hot here
FETCH = 3'b010,
EXEC = 3'b100
} state_e;
state_e state, next_state; // waveforms show IDLE/FETCH/EXEC, not 3'b001
// You get for free:
state.name() // → "FETCH" as a string, great for $display
state.next() // → next enum member
// And the compiler rejects: state = 3'b111; (not a valid member)
```
Leave encodings off (`{IDLE, FETCH, EXEC}`) and the synthesis tool picks binary / gray / one-hot for you based on your constraints — usually the right call.
=== "struct"
Bundle related signals into one object. `packed` structs are just a vector with named fields — fully synthesizable and portable through ports.
```systemverilog
typedef struct packed {
logic valid;
logic [3:0] id;
logic [31:0] addr;
logic [63:0] data;
} req_t; // exactly 101 bits, MSB = valid
req_t req;
req.addr = 32'hDEAD_BEEF; // field access
some_fifo_din = req; // or treat the whole thing as 101 bits
// UNPACKED struct — fields may be padded/reordered, NOT a bit vector.
// Fine in a testbench, avoid in synthesizable ports.
typedef struct { int a; string s; } tb_item_t;
```
!!! tip
Packed structs are how you make a wide bus survive refactoring. Add a field to `req_t` and every producer, consumer, and FIFO width follows automatically — no 40-file edit.
=== "typedef / package"
Put shared types, parameters, and functions in a `package` and import it. This is the standard way to keep type definitions consistent across a hierarchy.
```systemverilog
package soc_pkg;
parameter int ADDR_W = 32;
parameter int DATA_W = 64;
typedef enum logic [1:0] { OKAY, EXOKAY, SLVERR, DECERR } resp_e;
typedef struct packed {
logic [ADDR_W-1:0] addr;
logic [DATA_W-1:0] data;
} beat_t;
function automatic int clog2_ceil(int n);
return $clog2(n);
endfunction
endpackage
// In a module:
import soc_pkg::*; // bring everything in
// or, more disciplined:
import soc_pkg::beat_t;
import soc_pkg::resp_e;
module node import soc_pkg::*; ( // import in the header so ports can use the types
input beat_t in_beat,
output resp_e resp
);
```
4. Procedural Blocks & RTL Modeling
!!! abstract "The golden rules"
- Sequential logic → **`always_ff`** with nonblocking (`<=`)
- Combinational logic → **`always_comb`** with blocking (`=`)
- Never mix the two assignment styles in one block
- Never drive the same signal from two blocks
These rules eliminate most simulation/synthesis mismatches before they exist.
The three intent-declaring blocks
// ── SEQUENTIAL ────────────────────────────────────────────────
// Tool CHECKS that this infers flops. Nonblocking assignments.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= '0; // async active-low reset
else if (clear) count <= '0;
else if (incr) count <= count + 1'b1;
end
// No `else` needed — the flop simply holds. That's not a latch.
// ── COMBINATIONAL ─────────────────────────────────────────────
// Sensitivity list is inferred AND the tool errors if a latch is inferred.
always_comb begin
next_state = state; // ← default assignment: kills latch inference
grant = '0;
unique case (state)
IDLE : if (req) next_state = FETCH;
FETCH: begin grant = 1'b1; next_state = EXEC; end
EXEC : next_state = IDLE;
default: next_state = IDLE; // always have a default
endcase
end
// ── LATCH (rare, and you should mean it) ──────────────────────
always_latch begin
if (enable) q <= d; // no else → intentional latch
end
| Block | Sensitivity | Tool check | Assignment |
|---|---|---|---|
always_ff |
Explicit (@(posedge clk ...)) |
Must infer flip-flops | Nonblocking <= |
always_comb |
Inferred from all RHS reads | Must be latch-free, no incomplete assignment | Blocking = |
always_latch |
Inferred | Must infer latches | Nonblocking <= |
always (legacy) |
Whatever you wrote | None. Silently infers anything. | Whatever you wrote |
always @(*) |
Inferred | None — will happily create a latch | Blocking |
!!! danger "Ban plain always in new RTL"
The entire point of `always_comb` / `always_ff` is that the tool now verifies your intent. A plain `always @(*)` that misses a signal in the sensitivity list simulates one way and synthesizes another — the classic, expensive, silicon-respin-grade bug that these keywords exist to make impossible.
Blocking vs. nonblocking — why it actually matters
GOAL: a 2-stage shift register. d ──▶[FF a]──▶[FF b]──▶ q
CORRECT (nonblocking — both flops sample the OLD values):
always_ff @(posedge clk) begin
a <= d;
b <= a; // ← 'a' here is the value BEFORE this clock edge
end
Result: two flops in series. ✓
WRONG (blocking — executes like software, top to bottom):
always_ff @(posedge clk) begin
a = d;
b = a; // ← 'a' was ALREADY updated to d on the line above
end
Result: ONE flop. 'd' races straight through to b. ✗
WHY: nonblocking assignments evaluate all RHS first, then update all LHS
at the end of the time step. That is exactly what real flops do —
every flop in a design samples its input simultaneously.
!!! tip
Say it as a physical fact, not a coding style: **flops sample simultaneously, so model them with an assignment that updates simultaneously.** Combinational logic settles in order, so model it with an assignment that executes in order. The rule follows from the hardware.
unique, unique0, and priority
Qualifiers on case and if that state your intent to both the simulator (runtime violation reports) and synthesis (optimization freedom).
| Qualifier | You are asserting | Synthesis effect | Sim effect if violated |
|---|---|---|---|
unique case |
Exactly one arm matches, always | Parallel mux, no priority chain | Runtime warning if 0 or 2+ arms match |
unique0 case |
At most one arm matches (zero is OK) | Parallel mux | Warning only if 2+ match |
priority case |
At least one matches; first match wins | Priority encoder chain | Warning if no arm matches |
| (none) | Nothing | Priority chain (safe default) | Silent |
!!! warning "These are promises, not requests"
If you write `unique case` and two arms can genuinely overlap, synthesis builds a parallel mux that produces garbage for that input, while simulation may only print a warning you filtered out.
The old `// synopsys full_case parallel_case` pragmas were the same trap with no simulation check at all — `unique`/`priority` are strictly better because the simulator polices the claim. Still: only claim what you can prove.
5. Design Hierarchy & Reuse
Parameterization
module sync_fifo #(
parameter int DEPTH = 16,
parameter type T = logic [31:0], // type parameters! very useful
localparam int PTR_W = $clog2(DEPTH) // derived — caller cannot override
) (
input logic clk, rst_n,
input logic push, pop,
input T din,
output T dout,
output logic full, empty
);
T mem [DEPTH];
logic [PTR_W:0] wptr, rptr; // extra MSB for full/empty disambiguation
// ...
endmodule
// Instantiation — always use named ports and named parameters.
sync_fifo #(
.DEPTH (64),
.T (beat_t) // pass a struct type straight in
) u_req_fifo (
.clk (clk),
.rst_n (rst_n),
.push (req_valid && req_ready),
.pop (fifo_pop),
.din (req_beat),
.dout (fifo_beat),
.full (fifo_full),
.empty (fifo_empty)
);
!!! tip "localparam vs. parameter"
**`parameter`** = knob the instantiator turns. **`localparam`** = value derived from those knobs.
Making `PTR_W` a `parameter` invites someone to override it inconsistently with `DEPTH`; making it a `localparam` makes that impossible.
Generate — structural elaboration
generate constructs are evaluated at elaboration time, before simulation or synthesis begins. They build structure; they are not runtime loops.
// generate-for: replicate hardware
genvar i;
generate
for (i = 0; i < NUM_LANES; i++) begin : g_lane // ← ALWAYS name the block
lane_top #(.LANE_ID(i)) u_lane (
.clk (clk),
.din (din [i]),
.dout (dout[i])
);
end
endgenerate
// Hierarchy becomes: top.g_lane[0].u_lane, top.g_lane[1].u_lane, ...
// Unnamed blocks get tool-generated names → your SDC, UPF, and
// waveform scripts break on the next tool version. Name every block.
// generate-if: compile-time configuration
generate
if (USE_ECC) begin : g_ecc
ecc_encoder u_enc (.*);
end else begin : g_no_ecc
assign data_out = data_in;
end
endgenerate
!!! warning
A `for` loop inside `always_comb` is *also* unrolled at elaboration — it is not a runtime loop either. The bounds must be compile-time constant. If you find yourself wanting a data-dependent loop count in RTL, what you actually want is an FSM.
6. Interfaces
An interface is a named bundle of signals — plus, optionally, the tasks, functions, assertions, and coverage that go with the protocol. A modport declares which direction each signal takes from a given side's point of view.
interface axi_lite_if #(parameter int AW = 32, DW = 32) (input logic clk, rst_n);
// write address channel
logic [AW-1:0] awaddr;
logic awvalid, awready;
// write data channel
logic [DW-1:0] wdata;
logic [DW/8-1:0] wstrb;
logic wvalid, wready;
// write response
logic [1:0] bresp;
logic bvalid, bready;
modport master (
output awaddr, awvalid, wdata, wstrb, wvalid, bready,
input awready, wready, bresp, bvalid,
input clk, rst_n
);
modport slave ( // same signals, mirrored directions
input awaddr, awvalid, wdata, wstrb, wvalid, bready,
output awready, wready, bresp, bvalid,
input clk, rst_n
);
// Protocol checking lives WITH the protocol — every instance gets it free.
property p_awvalid_stable;
@(posedge clk) disable iff (!rst_n)
awvalid && !awready |=> awvalid && $stable(awaddr);
endproperty
a_awvalid_stable: assert property (p_awvalid_stable)
else $error("AW channel: valid dropped or addr changed before ready");
endinterface
// ── Usage ──────────────────────────────────────────────────────
module cpu (axi_lite_if.master bus, ...); // one port, whole protocol
module sram (axi_lite_if.slave bus, ...);
module top;
axi_lite_if #(.AW(32)) bus (.clk(clk), .rst_n(rst_n));
cpu u_cpu (.bus(bus));
sram u_sram (.bus(bus));
endmodule
-
What you gain
- Add a signal to the protocol → edit one file, not every module and instantiation
- Direction errors caught at compile time by the modport
- Assertions and coverage live next to the signals they describe
- Testbench gets a
virtual interfacehandle — the standard UVM driver/monitor connection
-
What to watch for
- Synthesis support for interfaces is good but not universal — check your tool and your IP vendors before committing
- Interfaces at the chip top-level pins are usually a bad idea; flatten to plain ports at the boundary
- Some flows (formal, some lint, some legacy IP) prefer flat ports — many teams use interfaces internally and flatten at block boundaries
7. The Synthesizable Subset
!!! abstract "Working definition"
Synthesizable code describes a **static, finite structure of gates and flops**. Anything that implies unbounded time, dynamic memory allocation, or a value that can't be resolved at elaboration is not synthesizable — regardless of whether it simulates beautifully.
| Category | Synthesizable | Not synthesizable |
|---|---|---|
| Types | logic, bit, wire, packed arrays/structs/unions, enum, int (as constants/loop vars) |
real, string, class, dynamic arrays, queues, associative arrays, ref args |
| Procedural | always_ff, always_comb, always_latch, assign, function, task (no timing) |
initial, final, fork/join, #delay, wait, @ inside a task |
| Control flow | if/else, case/casez, for with constant bounds, generate |
while/forever with data-dependent bounds, casex (legal but banned by most style guides) |
| Structure | module, interface (tool-dependent), package, parameter, hierarchical instantiation |
program, clocking blocks, covergroup, constraint, randomize() |
| System tasks | $clog2, $bits, $size, $signed/$unsigned, $countones, $onehot |
$display, $finish, $random, $time, $fopen (ignored or errored) |
| Assertions | Ignored by synthesis (harmless), consumed by formal | — |
!!! danger "casex is a trap"
It treats `X` in the *case expression* as a wildcard, so an `X` arriving on a real signal can match an arm it should never have matched — and simulation will happily proceed down the wrong branch while the gates do something else.
Use `casez` (only `?`/`Z` is a wildcard) or, better, `case ... inside` with explicit wildcards. Most ASIC style guides ban `casex` outright.
The inference cheat sheet
WHAT YOU WRITE WHAT YOU GET
─────────────────────────────────────────────────────────────────
always_comb + every output assigned
on every path ──► pure combinational cloud
always_comb + a path that DOESN'T
assign an output ──► LATCH (tool will error — good)
always_ff @(posedge clk) ──► D flip-flop, no reset
always_ff @(posedge clk)
if (!rst_n) q <= 0; ──► D flop with SYNCHRONOUS reset
always_ff @(posedge clk or
negedge rst_n)
if (!rst_n) q <= 0; ──► D flop with ASYNCHRONOUS reset
logic [31:0] mem [1024];
+ one write port, one read port ──► RAM macro (or flops, if small
and the tool can't map it)
assign y = sel ? a : b; ──► 2:1 mux
if / else if / else chain ──► priority mux chain
unique case ──► parallel mux
for (i=0;i<N;i++) sum += x[i];
inside always_comb ──► N-input adder tree, UNROLLED
(not a loop — N adders of area)
8. Assertions (SVA)
SystemVerilog Assertions let you write a temporal rule — something that must hold over time — as code. Simulation checks it on every run; formal tools try to prove it for all inputs. Both consume the same source.
Immediate vs. concurrent
// IMMEDIATE — a plain procedural statement, checked when execution reaches it.
always_comb begin
a_onehot: assert ($onehot0(grant))
else $error("Arbiter granted more than one requester");
end
// CONCURRENT — clocked, evaluated across time. This is the interesting one.
property p_req_gets_grant;
@(posedge clk) disable iff (!rst_n) // ← never omit disable iff
req |-> ##[1:4] grant;
endproperty
a_req_gets_grant: assert property (p_req_gets_grant)
else $error("req not granted within 4 cycles");
c_req_gets_grant: cover property (p_req_gets_grant); // did we ever exercise it?
| Operator | Name | Meaning |
|---|---|---|
|-> |
Overlapping implication | If LHS holds, RHS must hold starting the same cycle. |
|=> |
Non-overlapping implication | If LHS holds, RHS must hold starting the next cycle. |
##N |
Cycle delay | Exactly N clock cycles later. |
##[M:N] |
Delay range | Between M and N cycles later. ##[1:$] = eventually. |
$rose / $fell |
Edge | Signal transitioned 0→1 / 1→0 since the last clock. |
$stable / $changed |
Value sampling | Value did / did not change since the last clock. |
$past(sig, N) |
History | Value of sig N clocks ago. |
throughout, within |
Sequence composition | Constrain one sequence relative to another. |
disable iff (x) |
Reset guard | Abort evaluation while x is true. Always guard with reset. |
assert vs. assume vs. cover
-
assert
"This must be true." A failure is a bug in the DUT. Simulation reports it; formal tries to disprove it.
-
assume
"The environment guarantees this." Constrains the formal tool's input space. In simulation, typically treated as an assert on the stimulus.
Over-assuming is how formal proofs become meaningless — you proved the design correct in a world that can't happen.
-
cover
"Did we ever see this?" No pass/fail — it reports whether the scenario was reached.
Essential: an assertion that never fires because the scenario never occurred proves nothing.
!!! tip "Where to put them"
Inline in the RTL for internal invariants (one-hot state, FIFO never overflows, counter never wraps unexpectedly), and inside the `interface` for protocol rules — that way every instantiation of the protocol is checked, in every testbench, forever, with no extra work.
9. The Verification Half
You won't write this in RTL, but you'll read it in bug reports, review it in DV code, and use it when you write your own block-level testbench. Here's the working vocabulary.
class / extends
: Full OOP: inheritance, virtual methods, polymorphism, handles (references, not values). Objects are dynamically allocated with new() and garbage-collected. This is the foundation UVM is built on.
rand / randc / constraint
: Constrained-random stimulus. Declare a class member rand, write constraint c { addr inside {[0:1023]}; len dist {1:=70, [2:8]:=30}; }, then call obj.randomize(). A SAT-based solver picks legal values. This is how you cover a state space no directed test could reach.
covergroup / coverpoint / cross
: Functional coverage — the measurement of what your random tests actually hit. Distinct from code coverage (lines/toggles/branches). Coverage closure is the exit criterion for most verification plans.
mailbox / semaphore / event
: Inter-process communication between concurrent testbench threads (driver, monitor, scoreboard).
fork ... join / join_any / join_none
: Spawn concurrent threads. join waits for all, join_any for the first, join_none for none (fire and forget).
clocking block
: Declares sampling and driving skew relative to a clock edge, so the testbench can't race the DUT. Solves the classic "did my driver's value land before or after the flop sampled?" problem.
program block
: A testbench container that executes in the Reactive region — after all design events settle. Largely superseded by clocking blocks + UVM conventions, but you'll see it in older code.
virtual interface
: A handle to a physical interface instance, usable inside a class. This is the bridge between the static design world and the dynamic class-based TB world.
DPI-C
: Direct Programming Interface. Call C/C++ from SystemVerilog and vice versa. Used for reference models, ISS co-simulation, and reusing existing C models.
!!! info "UVM is not part of SystemVerilog"
The Universal Verification Methodology is a *class library* (IEEE 1800.2) written in SystemVerilog, providing a standard structure for agents, sequences, drivers, monitors, and scoreboards. It's convention on top of the language, not the language itself. Worth its own handbook page.
10. Scheduling & Race Avoidance
Every simulation time step is divided into ordered regions. Understanding the ordering is what turns "nonblocking assignments are a rule I follow" into "nonblocking assignments are obviously correct."
Within ONE simulation time step (simplified from the 17-region LRM model):
┌─ PREPONED ────────── sample values for concurrent assertions
│ (BEFORE anything changes — this is why SVA
│ never sees a race with the RTL it's checking)
│
├─ ACTIVE ──────────── blocking assignments (=), continuous assigns,
│ always_comb bodies, RHS of nonblocking evaluated
│
├─ INACTIVE ────────── #0 delays (avoid these — they're a smell)
│
├─ NBA ─────────────── nonblocking assignments (<=) UPDATE their LHS
│ ← all flops in the design land here, together
│
├─ OBSERVED ────────── concurrent assertions evaluate
│
├─ REACTIVE ────────── program blocks / testbench code runs
│ (so the TB sees settled design values)
│
└─ POSTPONED ───────── $strobe, $monitor, coverage sampling
THE RACE THAT NONBLOCKING KILLS:
always_ff @(posedge clk) a <= d; // both blocks trigger on the
always_ff @(posedge clk) b <= a; // same edge — but in WHAT ORDER?
The LRM does NOT define which always block runs first.
With BLOCKING assignments, the answer decides your circuit. Nondeterministic.
With NONBLOCKING: both RHS are read in ACTIVE (old values),
both LHS are written in NBA. Order of the blocks is irrelevant.
Deterministic, and it matches what the gates do. ✓
!!! tip
The PREPONED region is why you can drop assertions into RTL without worrying about them racing the logic they observe — they sample the values as they existed *before* any of this time step's activity. This is also why `$past()` is trustworthy.
11. Pitfalls & Lint Rules
-
Unintentional latch
An
always_combpath that doesn't assign an output on every branch infers a latch — a level-sensitive element with no defined timing arc, a nightmare for STA.Fix: assign a default to every output at the top of the block, then override.
always_combmakes the tool error on this; plainalways @(*)does not. That's the whole argument for the keyword. -
Blocking in sequential logic
Using
=insidealways_ffcreates order dependence between always blocks. May simulate correctly, may synthesize to something else, may differ between simulators.Fix:
<=inalways_ff,=inalways_comb, never mixed. Every lint tool checks this. Never waive it. -
Width mismatch / silent truncation
logic [7:0] a = b + c;whereb,care 8-bit silently drops the carry. Verilog's implicit width rules are permissive and will not warn you by default.Fix: turn width warnings into errors in your lint policy. Size literals explicitly (
8'd1, not1). Use'0/'1for context-sized fill. -
Incomplete sensitivity list
Legacy:
always @(a or b)when the block also readsc. Simulation misses the update; synthesis builds the full combinational cloud. Classic sim/synth mismatch.Fix:
always_comb. It derives the sensitivity list, and it includes signals read by functions the block calls — which@(*)does not. -
Reset style mixing
Some flops async-reset, some sync-reset, some not reset at all, with no documented policy. Reset release becomes a metastability hazard and STA becomes guesswork.
Fix: pick one house style (commonly: async assert, synchronous de-assert, via a reset synchronizer). Document it. Lint for deviations.
-
Unsynchronized clock crossings
SystemVerilog will not stop you from sampling a signal from another clock domain with a single flop. Silicon will.
Fix: a dedicated CDC tool — not lint, not simulation, since zero-delay models cannot see metastability. Use standard synchronizer cells and mark them.
-
Multiple drivers
Two
alwaysblocks assigning the same signal. Withlogicthis is a compile error (single-driver rule) — withwireit silently resolves toX.Fix: use
logic, notwire, so the compiler catches it. One signal, one driver, always. -
`includeand macro soup
Verilog macros (
`define) are global, textual, and have no scope. A macro collision across two IP blocks is a miserable debug.Fix: use
package+parameter+typedefinstead of macros wherever possible. Scoped, typed, and visible in the debugger.
A starter house style
- Types:
logicin design,bitin TB.wireonly for multi-driver nets. - Blocks:
always_ff/always_comb/always_latchonly. Plainalwaysis a lint error. - Assignments:
<=inalways_ff,=inalways_comb. Never mixed in one block. - Case: always a
default. Useunique/prioritywhen you can prove the claim.casexbanned. - Ports: named connection only (
.clk(clk)). Positional connection is a lint error..*is a team-by-team judgment call. - Names: active-low suffix
_n, clockclk_*, resetrst_n, types_t, enums_e, generate blocksg_*, instancesu_*. - Constants:
localparamfor derived values,parameteronly for real knobs. No bare magic numbers. - Files: one module per file, filename matches module name. Design and TB in separate trees.
12. Study Checklist
Foundations
- [ ] Explain what IEEE 1800 unified and why Verilog no longer has a separate standard
- [ ] Name the three layers (design / assertion / verification) and which tools consume each
- [ ] State the working definition of the synthesizable subset
Data types
- [ ] Explain 4-state vs. 2-state and why RTL should use
logic, notbit - [ ] Explain what
logicreplaced and when you still needwire - [ ] Distinguish packed vs. unpacked arrays and know which infers memory
- [ ] Write a packed struct and a typedef'd enum for an FSM state
- [ ] Use a
packageto share types and parameters across a hierarchy
RTL modeling
- [ ] State what each of
always_ff,always_comb,always_latchchecks - [ ] Explain blocking vs. nonblocking using the 2-stage shift register example
- [ ] Explain how a default assignment at the top of
always_combprevents a latch - [ ] Contrast
unique,unique0,priorityand their synthesis consequences - [ ] Explain why
casexis dangerous and what to use instead - [ ] Write a 3-block FSM (state reg / next-state comb / output comb)
Structure & reuse
- [ ] Explain
parametervs.localparamand when each is correct - [ ] Use a type parameter to make a generic FIFO
- [ ] Explain that
generateand RTLforloops are elaboration-time, not runtime - [ ] Explain why every generate block must be named
- [ ] Write an
interfacewith mirroredmodports and explain the tradeoffs of using them
Assertions & scheduling
- [ ] Distinguish immediate from concurrent assertions
- [ ] Explain
|->vs.|=>, and##Nvs.##[M:N] - [ ] Explain why
disable iff (!rst_n)belongs on essentially every property - [ ] Distinguish assert / assume / cover and explain how over-assuming invalidates a formal proof
- [ ] Name the event regions in order and explain why NBA makes flop order irrelevant
Pitfalls
- [ ] List the classic sim/synth mismatch sources and how the SV keywords eliminate each
- [ ] Explain why width truncation is silent by default and how to catch it
- [ ] Explain why simulation cannot find CDC bugs and what tool must
- [ ] Recite your team's house style from memory (or write one)
References
- IEEE 1800-2023 — SystemVerilog LRM
- IEEE 1800.2 — UVM
- Sutherland & Mills — Standard Gotchas: Subtleties in the Verilog and SystemVerilog Standards
- Cummings — nonblocking assignment papers (SNUG)