SystemVerilog · Module 5
case, casex, casez, unique, priority
Hardware inference, don't-care matching, modifier safety.
Module 5 · Page 5.3
Plain case — The Parallel Decoder
The fundamental difference between case and if-else is what hardware they imply. A case statement tells the tool: every branch is checked in parallel — it infers a decoder (or a one-hot mux), not a priority chain. Synthesis tools exploit this to generate flatter, faster logic when conditions are known to be mutually exclusive.
In simulation, case evaluates branches top-to-bottom and picks the first match — but the hardware intent is parallel. Keep that distinction in mind.
// ── Form 1: case with 2-bit opcode ─────────────────────────────
always_comb begin
result = '0; // default prevents latch
case (opcode)
2'b00: result = a + b;
2'b01: result = a - b;
2'b10: result = a & b;
2'b11: result = a | b;
endcase
end
// ── Form 2: case with default clause ────────────────────────────
always_comb begin
case (state)
IDLE : next = FETCH;
FETCH : next = DECODE;
DECODE: next = EXECUTE;
EXECUTE: next = IDLE;
default: next = IDLE; // covers unreachable encodings safely
endcase
end
// ── Form 3: multiple values per branch ──────────────────────────
always_comb begin
case (irq)
4'h1, 4'h2: priority_out = HIGH;
4'h4, 4'h8: priority_out = MED;
default: priority_out = LOW;
endcase
endFigure 1 — case Infers a Parallel Decoder, Not a Priority Chain
Figure 1 — A plain case with four branches infers a 4-to-1 decoder. All four conditions are checked simultaneously — there is no "earlier branch wins" in hardware.
🧠 The Critical Distinction: case in Simulation vs Synthesis
This is one of the most misunderstood facts about case. In simulation, the SystemVerilog simulator evaluates case branches top-to-bottom and takes the first match — exactly like an if-else if chain. If two branches could match (which plain case allows), the first one wins silently. In synthesis, the tool treats case as a declaration of parallel decode intent — it generates a flat decoder where all branches are checked simultaneously with no implied priority. This disconnect is why unique case exists: it aligns simulation behavior with synthesis intent by issuing a warning when two branches match (which should never happen if the RTL is correct).
🏗 Synthesis Concern: case vs if-else — Area and Timing
When you use case for a fully-decoded enumeration (all patterns listed, no overlap possible), the synthesis tool generates a flat N:1 decoder — essentially an AND gate per branch feeding one output mux. Critical path: one decoder gate + one mux = ~300ps in 28nm for a 4-bit opcode. When you use if-else if for the same logic, the tool generates a cascaded mux chain — each level adds ~150–200ps. For a 4-opcode decode, case is roughly 2× faster in synthesis. This is why style guides universally require case for state machine next-state decode and opcode decode.
casex & casez — Don't-Care Matching
Plain case uses 4-state exact matching: every bit in the expression must match every bit in the pattern, including X and Z. casex and casez relax this by treating certain bit values as wildcards — don't cares.
casex
Treats X and Z in both the expression and the pattern as don't cares. Powerful, but dangerous: X values in simulation (propagated unknowns) silently match patterns. Generally avoid in RTL.
casez
Treats Z and ? in the pattern (or expression) as don't cares. X values are not treated as don't cares. Safer than casex. Preferred for opcode/instruction decoding.
Plain case
Exact 4-state match. No don't cares. Use when every bit matters and you want X to propagate normally in simulation.
// ── casez: decode a 4-bit opcode with partial patterns ──────────
// '?' in patterns = don't care (matches 0 or 1)
always_comb begin
ctrl = '0;
casez (instr[7:4])
4'b1???: ctrl = BRANCH; // MSB=1, lower 3 bits = don't care
4'b01??: ctrl = ALU_OP; // bits 7:6 = 01
4'b001?: ctrl = MEM_OP; // bits 7:5 = 001
4'b0000: ctrl = NOP;
default: ctrl = ILLEGAL;
endcase
end
// ── casex (avoid in RTL — shown for reference only) ─────────────
always_comb begin
casex (sel)
4'b1xxx: out = d3; // x = don't care in pattern
4'b01xx: out = d2;
4'b001x: out = d1;
4'b0000: out = d0;
default: out = '0;
endcase
end
// ⚠ If sel contains X bits at simulation time they silently match.
// This hides X-propagation bugs. Prefer casez in synthesisable RTL.🔍 Debugging Insight: Why casex Is Banned in Most RTL Style Guides
Here is the exact failure mode, and it needs one precision most write-ups skip.
Suppose sel[3:0] has an X on bit 1 — a bit your patterns actually care about. With casex and pattern 4'b1x00, X in the expression is a wildcard at every position, so bit 1 matches the pattern's 0 and the branch fires. Simulation looks correct; the hardware is in an undefined state; the root cause is masked. With casez and pattern 4'b1?00, bit 1 is a care position holding 0, and an X in the expression does not match it. No branch matches, the output goes X, and the bug becomes visible.
Now the part that matters and is usually stated wrongly. casez protects you only where the pattern cares. If the X had landed on bit 2 — the position written ? — that bit is a don't-care and is excluded from the comparison entirely, so the branch fires under casez exactly as it does under casex. The X is equally masked. A ? does not "reject X"; it removes the bit from the match.
So the accurate rule is: casex masks X everywhere; casez masks X only where you wildcarded. That is still a decisive advantage — it confines the blind spot to bits you explicitly declared irrelevant, instead of every bit — and it is why ARM, Intel, Qualcomm, and virtually every ASIC methodology ban casex from synthesisable RTL. But it also means a casez decoder that wildcards a bit it genuinely depends on is no safer than casex on that bit, which §"Proving it" below demonstrates in about forty lines.
| Bit value in pattern | case match? | casez match? | casex match? |
|---|---|---|---|
0 | Only if expression bit = 0 | Only if expression bit = 0 | Only if expression bit = 0 |
1 | Only if expression bit = 1 | Only if expression bit = 1 | Only if expression bit = 1 |
Z or ? | Only if expression bit = Z | Always (wildcard) | Always (wildcard) |
X | Only if expression bit = X | Only if expression bit = X | Always (wildcard) |
unique case & priority case
These are the case equivalents of unique if and priority if. They add simulation checks without changing the synthesised hardware — they are assertions built into the language.
unique case
unique case asserts two things to both the simulator and the synthesis tool:
- Mutually exclusive: at most one branch condition can be true at any given time.
- Complete: at least one branch condition is always true (no unhandled case).
If either assertion fails during simulation, the tool issues a violation warning. The synthesis tool uses the mutual-exclusivity guarantee to generate a parallel decoder (no priority gates), potentially reducing area and critical path.
// unique case: patterns are mutually exclusive AND complete
always_comb begin
unique case (grant) // one-hot: exactly one bit set
4'b0001: bus_out = data_a; // if two bits set → sim warning
4'b0010: bus_out = data_b; // if no bits set → sim warning
4'b0100: bus_out = data_c;
4'b1000: bus_out = data_d;
endcase // no default needed — unique covers it
end
// ⚠ Warning: unique case without default still warns if no branch matches.
// For synthesis safety add a default that drives a known value.priority case
priority case asserts that at least one branch is always true (completeness), but allows multiple branches to be true simultaneously — the first match wins. It does not assert mutual exclusivity. The simulator warns if no branch matches at all.
// priority case: first match wins, multiple matches are OK
always_comb begin
priority case (1'b1) // case(1) idiom: test which bit is set
req[0]: grant = 4'b0001; // highest priority
req[1]: grant = 4'b0010;
req[2]: grant = 4'b0100;
req[3]: grant = 4'b1000; // lowest priority
endcase // sim warns if all req[] = 0
end
// case(1'b1) — evaluates each item as a condition.
// Equivalent to priority if chain, but reads cleanly.| Keyword | Mutual-exclusivity check | Completeness check | Multiple matches | Hardware inferred |
|---|---|---|---|---|
case | None | None | First match wins (silent) | Priority encoder |
unique case | Yes — sim warning if violated | Yes — sim warning if no match | Violation — sim warning | Parallel decoder (optimised) |
priority case | None | Yes — sim warning if no match | First match wins (intentional) | Priority encoder |
🚀 RTL Design Insight: unique case Is the Correct Choice for All One-Hot and FSM Decodes
In production RTL, unique case is the mandatory choice for: (1) FSM next-state decode where each state encoding is unique, (2) one-hot bus grant decode, (3) ALU opcode decode where opcodes are guaranteed mutually exclusive. The simulation safety net catches encoding bugs immediately — if you accidentally assign two states the same encoding, the unique case violation fires instantly on the first simulation cycle. Without unique case, these bugs survive simulation and are only caught post-synthesis, or in silicon. The synthesis QoR improvement (parallel decoder instead of priority chain) is a bonus; the real value is the simulation-time bug detection.
💡 Senior Verification Engineer Tip: The case(1'b1) Idiom Explained
The priority case (1'b1) idiom is one of the most confusing patterns for engineers who haven't seen it before. Here's what it does: case evaluates the expression (1'b1) and compares it against each item. Each item is a signal — e.g., req[0]. The comparison is: "does req[0] equal 1'b1?" — which is true when req[0] is high. So the case fires the branch whose item equals 1. Combined with priority, this is a clean, readable priority encoder. It's exactly equivalent to a priority if chain but reads as a table — much cleaner for 8+ priority levels. Many style guides require this idiom for priority arbiters.
What each form actually synthesises to
The three decision modifiers read almost identically and infer different hardware. That difference is invisible in simulation — all three evaluate top-to-bottom and take the first match — so it is worth seeing once as structure.
The row that costs real money is the top one. A missing default does not produce a warning or an X — it produces storage, and a latch in a path the timing tool expected to be combinational is a closure problem discovered late. The other two rows are a genuine engineering choice: a decoder is faster, a priority chain is correct when the conditions really do overlap, and claiming unique for arms that are not exclusive gives the tool permission to build hardware that does not match your simulation.
The default Clause — Always Use It
The default branch in a case statement handles every pattern not listed explicitly. It is the hardware equivalent of the final else in an if-else chain. Without it, an unmatched case expression leaves assigned signals unchanged — inferring a latch.
Even when you believe your patterns are exhaustive (e.g., a 2-bit opcode with all four patterns listed), adding a default is good practice: it documents your intent, prevents lint warnings, and guards against future code changes that add new encodings.
// ── Pattern A: assign default before case (preferred for RTL) ──
always_comb begin
out = '0; // default first — safe, clean
case (opcode)
ADD: out = a + b;
SUB: out = a - b;
// any unlisted opcode → out stays '0 from default above
endcase
end
// ── Pattern B: explicit default branch ─────────────────────────
always_comb begin
case (opcode)
ADD: out = a + b;
SUB: out = a - b;
default: out = '0; // explicit — synthesis generates safe mux
endcase
end
// ── Pattern C: latch — DO NOT write this ───────────────────────
always_comb begin
case (opcode)
ADD: out = a + b;
SUB: out = a - b;
// ⚠ missing default: out holds previous value → LATCH
endcase
end⚠ Common Industry Mistake: Trusting "All Patterns Listed" Without a Default
Engineers often write a 2-bit case with all four patterns (00, 01, 10, 11) and skip the default, reasoning "all cases are covered." This is incorrect in two ways. First, a 4-state simulator also has X and Z states — if the expression ever contains X (during reset, startup, or from an upstream bug), none of the binary patterns match, and the output becomes a latch-held value. Second, if someone later adds a new enum value and forgets to update the case, the missing branch silently produces the held (latch) value. A default: out = '0; line costs zero logic gates when all patterns are listed — but it eliminates both failure modes permanently.
Quick Reference
| Construct | X in pattern | Z / ? in pattern | Best used for |
|---|---|---|---|
case | Exact match only | Exact match only | Enumerations, state machines, opcodes |
casez | Exact match only | Wildcard (don't care) | Instruction decoding with partial patterns |
casex | Wildcard | Wildcard | Avoid in RTL (use casez instead) |
unique case | Exact match | Exact match | One-hot decode, mutually exclusive conditions |
priority case | Exact match | Exact match | Priority arbiter (case(1) idiom) |
🏗 FSM Design with case — The Industry Standard Pattern
The state encodings below are enums, and the transition logic is the always_comb half of the two-block FSM style; the sequential half uses non-blocking assignment.
The case statement is the backbone of finite state machine implementation in RTL. Every production SoC has dozens to hundreds of FSMs — controllers, arbiters, protocol handlers, power managers. The 3-block FSM pattern using always_ff + always_comb with case is the universal industry standard.
// ── AXI-Lite Slave Controller FSM — Real Project Style ───────────
typedef enum logic [2:0] {
IDLE = 3'b000,
RD_ADDR = 3'b001,
RD_DATA = 3'b010,
WR_ADDR = 3'b011,
WR_DATA = 3'b100,
WR_RESP = 3'b101
} axi_st_t;
module axi_lite_slave_ctrl (
input logic clk, rst_n,
input logic awvalid, wvalid, bready, arvalid, rready,
output logic awready, wready, bvalid, arready, rvalid
);
axi_st_t state, next;
// ── Block 1: State register ───────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) state <= IDLE;
else state <= next;
end
// ── Block 2: Next-state logic (combinational) ────────────────
always_comb begin
next = state; // default: hold state
unique case (state)
IDLE: begin
if (arvalid) next = RD_ADDR;
else if (awvalid) next = WR_ADDR;
end
RD_ADDR: next = RD_DATA;
RD_DATA: if (rready) next = IDLE;
WR_ADDR: if (wvalid) next = WR_DATA;
WR_DATA: next = WR_RESP;
WR_RESP: if (bready) next = IDLE;
default: next = IDLE; // safety: unreachable encodings → IDLE
endcase
end
// ── Block 3: Output logic (Moore — combinational from state) ─
always_comb begin
{awready, wready, bvalid, arready, rvalid} = 5'b00000;
unique case (state)
IDLE: begin arready = arvalid; awready = awvalid; end
RD_ADDR: arready = 1'b1;
RD_DATA: rvalid = 1'b1;
WR_ADDR: awready = 1'b1;
WR_DATA: wready = 1'b1;
WR_RESP: bvalid = 1'b1;
default: {awready, wready, bvalid, arready, rvalid} = 5'b00000;
endcase
end
endmoduleWaveform — AXI Write Transaction FSM State Traceclk_‾‾‾‾‾_‾awvalid0 1 1 0 0 0wvalid0 0 1 1 0 0bready0 0 0 0 1 0stateIDLE IDLE WR_ADDR WR_DATA WR_RESP IDLEawready0 1 0 0 0 0wready0 0 0 1 0 0bvalid0 0 0 0 1 0 ↑ ↑ ↑ ↑ ↑ ↑ idle aw wr wr wr idle V A D R
| FSM Encoding Style | State Bits | Next-State Logic | Best For | Modifier to Use |
|---|---|---|---|---|
| Binary | ⌈log₂N⌉ bits | Dense decoder logic | Small FSMs (≤8 states), area-critical | unique case |
| One-hot | N bits (1 per state) | One FF per state; simpler logic | High-speed FSMs, FPGA-friendly | unique case (parallel) |
| Gray | ⌈log₂N⌉ bits | Only 1 bit changes per transition | Async domain crossing state sync | Plain case |
| Sequential | ⌈log₂N⌉ bits | Counter-style | Linear pipelines | Plain case |
⚙ Instruction Decoder with casez — Partial Opcode Matching
This is the pattern the Bug 2 Debug Lab dissects — worth reading together, because the wildcard that makes partial matching convenient is the same wildcard that creates the X blind spot.
Every processor has an instruction decoder. Modern ISAs (RISC-V, ARM) use hierarchical opcode fields — the top bits identify the instruction class, and lower bits qualify it. casez with ? wildcards is the natural implementation: match the bits that matter, ignore the rest.
// ── Simplified RISC-V 32-bit instruction decoder ─────────────────
// Opcode field: instr[6:0] — identifies instruction type
// Funct3 field: instr[14:12] — qualifies within the type
typedef enum logic [4:0] {
OP_ADD, OP_SUB, OP_AND, OP_OR, OP_XOR, OP_SLL, OP_SRL,
OP_LW, OP_SW, OP_BEQ, OP_BNE, OP_JAL, OP_LUI, OP_AUIPC,
OP_ECALL, OP_ILLEGAL
} alu_op_t;
module riscv_decoder (
input logic [31:0] instr,
output alu_op_t alu_op,
output logic reg_write, mem_read, mem_write, branch
);
always_comb begin
// Safe defaults — prevent latches and ensure known state
alu_op = OP_ILLEGAL;
reg_write = 1'b0;
mem_read = 1'b0;
mem_write = 1'b0;
branch = 1'b0;
casez ({instr[14:12], instr[6:0]}) // {funct3, opcode}
// ── R-type (opcode = 0110011) ─────────────────────────────
10'b000_0110011: begin alu_op = OP_ADD; reg_write = 1'b1; end
10'b000_0110011: begin alu_op = OP_SUB; reg_write = 1'b1; end
10'b111_0110011: begin alu_op = OP_AND; reg_write = 1'b1; end
10'b110_0110011: begin alu_op = OP_OR; reg_write = 1'b1; end
10'b100_0110011: begin alu_op = OP_XOR; reg_write = 1'b1; end
// ── I-type Load (opcode = 0000011, funct3 = 010 = LW) ────
10'b010_0000011: begin alu_op = OP_LW; mem_read = 1'b1; reg_write = 1'b1; end
// ── S-type Store (opcode = 0100011, funct3 = 010 = SW) ───
10'b010_0100011: begin alu_op = OP_SW; mem_write = 1'b1; end
// ── B-type Branch (opcode = 1100011) ─────────────────────
10'b000_1100011: begin alu_op = OP_BEQ; branch = 1'b1; end
10'b001_1100011: begin alu_op = OP_BNE; branch = 1'b1; end
// ── U-type (opcode only — funct3 is don't-care) ──────────
10'b???_0110111: begin alu_op = OP_LUI; reg_write = 1'b1; end
10'b???_0010111: begin alu_op = OP_AUIPC; reg_write = 1'b1; end
// ── SYSTEM (opcode = 1110011) ─────────────────────────────
10'b000_1110011: alu_op = OP_ECALL;
// ── All others → illegal ──────────────────────────────────
default: alu_op = OP_ILLEGAL;
endcase
end
endmodule
// ── Key: casez matches '?' against any bit (0 or 1)
// 10'b???_0110111 fires for ALL funct3 values with opcode=0110111
// Without casez, you'd need 8 separate case items (funct3=000..111)🚀 RTL Design Insight: casez ? vs Separate Bit Checks — Synthesis Impact
When you write casez with ? wildcards, the synthesis tool understands the don't-care semantics and applies it to logic minimization. For a pattern like 4'b1???, the synthesizer knows only bit[3] matters — it generates a single gate checking bit[3], not a 4-bit comparator. Without casez, you'd need to write all 8 combinations (1000, 1001, 1010, ..., 1111) as separate case items, and the synthesis tool would generate a wider comparator before optimizing it. The casez version communicates don't-care intent directly to the tool, enabling cleaner minimization and smaller logic cones.
🔬 Simulation vs Synthesis — What the Simulator and Tool Each See
Understanding the gap between how the simulator processes case and how synthesis interprets it is the key to writing correct, synthesizable RTL. The gap is small but critical.
| Aspect | Simulator (VCS/Questa/Xcelium) | Synthesis Tool (DC/Genus) |
|---|---|---|
| Branch evaluation order | Top to bottom — first match wins | All branches in parallel — no implied order |
| Multiple matching branches | First one fires (silent with plain case) | Undefined — tool may generate incorrect logic |
| X in expression (casex) | Matches any x/z in pattern (wildcard) | X never exists in gates — treated as 0 or 1 |
| X in expression (casez) | X does NOT match ? (Z does) | Z treated as don't-care (DC optimization) |
| No-match (no default) | Signal holds old value (latch behavior) | Latch cell inferred — STA treats as memory |
| unique case with overlap | Warning issued, first match taken | Tool assumes exclusive — parallel decoder |
| priority case ordering | First match wins (guaranteed) | Priority encoder inferred |
| ❌ Sim/Synth mismatch — overlapping case items// Two case items match when grant=4'b0011 always_comb begin bus_out = '0; case (grant) 4'b0001: bus_out = data_a; // ← sim: wins when grant=1 4'b0011: bus_out = data_b; // ← this can also match grant=1 if // casez? No, plain case is exact. // But if programmer made typo: 4'b00?1: bus_out = data_c; // ← casez: matches 0001 AND 0011! endcase end // Sim: first match (data_a) wins // Synth: may generate data_c for 0001✅ Use unique case to catch the overlap// unique case exposes the overlap in simulation always_comb begin bus_out = '0; unique case (grant) 4'b0001: bus_out = data_a; 4'b0010: bus_out = data_b; 4'b0100: bus_out = data_c; 4'b1000: bus_out = data_d; default: bus_out = '0; endcase end // Sim warns if any two branches match // Synth: clean parallel one-hot decoder |
📊 Waveform Analysis — case Evaluation in Action
Watching how a case statement behaves in a waveform viewer gives you the intuition to debug case-related issues instantly. The key insight: case inside always_comb re-evaluates every time any input changes — including X values at startup.
Waveform — 4-opcode ALU: case evaluation at each opcode transitionTime →T0 T1 T2 T3 T4 T5 T6opcodeXX 00 01 10 11 01 00a08 08 08 08 08 08 10b03 03 03 03 03 03 05resultXX 0B 05 08 0B 05 15 ↑ ↑ ↑ ↑ ↑ ↑ ↑ X at ADD SUB AND OR SUB ADD T=0 8+3 8-3 8&3 8|3 8-3 16+5 (rst) =0Bh =05h =08h =0Bh =05h =15h
At T0: opcode is X (undriven before reset). The case finds no exact match for XX — result becomes X. This is correct simulation behavior: X propagates to alert you to an uninitialized input. At T1 after reset: opcode=00, case fires the ADD branch instantly (combinational, no clock delay). Each subsequent opcode change immediately re-evaluates the case and updates result.
Waveform — FSM case: state transitions over clock edgesclk_‾‾‾‾‾rst_n0 1 1 1 1 1start0 0 1 0 0 0done0 0 0 0 1 0stateIDLE IDLE IDLE BUSY BUSY DONEnextIDLE IDLE BUSY BUSY DONE IDLE ↑ ↑ ↑ ↑ ↑ ↑ rst held comb start comb done IDLE sees low sees cond start done
🧠 How to Read an FSM Waveform — next vs state
In a 3-block FSM, next (combinational) and state (registered) are both visible in the waveform. next changes immediately when any input changes (combinational — always_comb re-evaluates). state changes only at the posedge clock (registered — always_ff captures next). The 1-cycle lag between next changing and state updating is correct flip-flop behavior. If they ever appear to change at the same time (without a clock edge), you have a blocking assignment in the always_ff — a race condition.
⚙ Advanced Code Examples — Industry Patterns
Example A — 8-bit One-Hot Bus Arbiter (unique case)
// ── 8-master bus arbiter: grant is always one-hot ─────────────────
module bus_arbiter (
input logic clk, rst_n,
input logic [7:0] req,
output logic [7:0] grant,
output logic [2:0] grant_id,
output logic bus_active
);
always_comb begin
grant = 8'h00;
grant_id = 3'd0;
bus_active = 1'b0;
// Fixed-priority arbiter: req[7] highest, req[0] lowest
priority case (1'b1)
req[7]: begin grant = 8'b1000_0000; grant_id = 3'd7; bus_active = 1'b1; end
req[6]: begin grant = 8'b0100_0000; grant_id = 3'd6; bus_active = 1'b1; end
req[5]: begin grant = 8'b0010_0000; grant_id = 3'd5; bus_active = 1'b1; end
req[4]: begin grant = 8'b0001_0000; grant_id = 3'd4; bus_active = 1'b1; end
req[3]: begin grant = 8'b0000_1000; grant_id = 3'd3; bus_active = 1'b1; end
req[2]: begin grant = 8'b0000_0100; grant_id = 3'd2; bus_active = 1'b1; end
req[1]: begin grant = 8'b0000_0010; grant_id = 3'd1; bus_active = 1'b1; end
req[0]: begin grant = 8'b0000_0001; grant_id = 3'd0; bus_active = 1'b1; end
endcase
// priority case(1): sim warns if no req active (bus idle unexpectedly)
end
// ── Verification: scoreboard checks grant is always one-hot ──────
always_comb begin
if (bus_active) begin
assert ($countones(grant) == 1)
else $error("GRANT NOT ONE-HOT: %b", grant);
end
end
endmoduleExample B — Gray Code Counter Decoder (casez for don't-care)
// ── 4-bit Gray code → binary decode via casez ─────────────────────
// Used in async FIFO pointers, rotary encoders, ADC thermometer codes
module gray_to_bin (
input logic [3:0] gray,
output logic [3:0] binary
);
always_comb begin
binary = 4'h0;
casez (gray)
4'b0000: binary = 4'd0;
4'b0001: binary = 4'd1;
4'b0011: binary = 4'd2;
4'b0010: binary = 4'd3;
4'b0110: binary = 4'd4;
4'b0111: binary = 4'd5;
4'b0101: binary = 4'd6;
4'b0100: binary = 4'd7;
4'b1100: binary = 4'd8;
4'b1101: binary = 4'd9;
4'b1111: binary = 4'd10;
4'b1110: binary = 4'd11;
4'b1010: binary = 4'd12;
4'b1011: binary = 4'd13;
4'b1001: binary = 4'd14;
4'b1000: binary = 4'd15;
default: binary = 4'hX; // impossible (all covered), but defensive
endcase
end
// Note: casez here is actually plain case — no ? wildcards are used.
// The important thing is the default: outputs X for unexpected inputs,
// making simulation bugs visible rather than silently outputting 0.
endmoduleExample C — Verification Scoreboard using case for Opcode Reference Model
// ── ALU reference model: used in scoreboard to generate expected output
module alu_scoreboard;
typedef enum logic [3:0] {
ADD=4'h0, SUB=4'h1, AND=4'h2, OR=4'h3,
XOR=4'h4, NOT=4'h5, SHL=4'h6, SHR=4'h7
} opcode_t;
function automatic logic [8:0] alu_ref(
input logic [7:0] a, b,
input opcode_t op
);
unique case (op)
ADD: return {1'b0, a} + {1'b0, b};
SUB: return {1'b0, a} - {1'b0, b};
AND: return {1'b0, a & b};
OR: return {1'b0, a | b};
XOR: return {1'b0, a ^ b};
NOT: return {1'b0, ~a};
SHL: return {a[7], a, 1'b0}; // MSB spills to carry
SHR: return {1'b0, 1'b0, a[7:1]}; // LSB lost
default: return 9'hX; // illegal op — propagate X
endcase
endfunction
// ── Coverage group: hit every opcode in simulation ────────────────
opcode_t cov_op;
covergroup opcode_cg @(posedge clk);
cp_op: coverpoint cov_op {
bins all_ops[] = {ADD, SUB, AND, OR, XOR, NOT, SHL, SHR};
}
endgroup
endmodule🔬 Debugging Academy — 8 Real case/casez Bugs from the Field
Every one of these bugs has appeared in production RTL reviews or verification campaigns. The symptoms look confusing until you understand exactly how case matching, X propagation, and latch inference interact.
Bug 1 — Missing default in an FSM case: unknown state after a reset glitch
Category: FSM / latch inference. Buggy code:
// ❌ BUG: 3-state FSM with 3-bit state register — 5 unreachable encodings
typedef enum logic [2:0] {IDLE=3'd0, FETCH=3'd1, EXEC=3'd2} st_t;
st_t state, next;
always_comb begin
next = state;
case (state) // ❌ no default!
IDLE: next = FETCH;
FETCH: next = EXEC;
EXEC: next = IDLE;
// state = 3'd3..7: no match → next holds → LATCH on next!
// If state glitches to 3'd5 (reset noise, power event):
// → next never leaves 3'd5 → FSM locked forever
endcase
end
// ✅ FIX: always include default → IDLE as safety net
always_comb begin
next = IDLE; // safe default at top
case (state)
IDLE: next = FETCH;
FETCH: next = EXEC;
EXEC: next = IDLE;
default: next = IDLE; // safety: illegal encodings → IDLE
endcase
endRoot cause / waveform symptom / prevention. Waveform symptom: the FSM appears stuck in an unrecognized state — the state register shows a binary value (e.g., 3'd5) that doesn't correspond to any declared state. The state never transitions. Outputs are all at their default (zero) values since no output case branch fires.How It Happens in SiliconPower-on glitch or reset noise can briefly corrupt the state register to an illegal encoding. Without a default: next = IDLE;, the FSM has no recovery path — it stays locked in the illegal state forever. In simulation, this scenario is often missed because testbenches apply clean resets. In silicon, power integrity issues create exactly this scenario.Industry RuleEvery FSM case statement must have a default that returns to a safe state. No exceptions. Most RTL style guides make this a P1 (blocker) lint rule. Tools like Spyglass flag missing FSM defaults as "FSM_NO_DEFAULT_STATE" — a must-fix before tape-out.2casex Hides X-Propagation — Reset Bug Survives SimulationX-Propagation / casexBuggy Code
Bug 2 — casex masks an X, and the "obvious" casez fix does not help
Swapping casex for casez leaves the X exactly as hidden, because the X landed on a wildcarded bit
CASEZ-WILDCARD-BLIND-SPOTAn instruction decoder produces plausible control signals at startup, but the design behaves randomly on the bench. Simulation is clean. opcode[3:2] is X out of reset — an upstream register is never initialised — and the decoder never notices.
A reviewer applies the standard rule, replaces casex with casez, and the simulation is still clean. The X is still invisible.
// The original. casex treats X in the EXPRESSION as a wildcard everywhere.
always_comb begin
ctrl = '0;
casex (opcode) // opcode = 4'bXX00
4'bxx00: ctrl = LOAD; // fires - X masked
4'bxx01: ctrl = STORE;
4'bxx10: ctrl = ALU;
4'bxx11: ctrl = BRANCH;
endcase
end
// The "fix" that changes nothing for THIS opcode.
always_comb begin
ctrl = '0;
casez (opcode) // opcode = 4'bXX00
4'b??00: ctrl = LOAD; // STILL fires - bits 3:2 are '?', so they are
4'b??01: ctrl = STORE; // excluded from the comparison entirely. The X
4'b??10: ctrl = ALU; // sits exactly where the pattern stopped caring.
4'b??11: ctrl = BRANCH;
default: ctrl = '0;
endcase
endExpected (per the usual rule): casez refuses to match an X, output goes X, bug visible.
Actual: bits 3:2 are ?. A ? does not reject an X — it removes that bit from the match. Both decoders return LOAD for an opcode nobody knows.
The question that resolves it is not "casex or casez" but where is the X relative to the pattern's care bits?
- Locate the X. Which bits of the expression are unknown? Here, 3:2.
- Read the pattern at those positions.
4'b??00— both are?. The comparison never looks at them, under either keyword. - Predict, then confirm. If every X bit sits on a wildcard, no
casevariant will expose it; if any X bit sits on a0/1,casezwill. That prediction is testable in seconds with the proof below, and it is faster than re-reading the decoder.
The tell that separates this from an ordinary decode bug: swapping casex→casez changed nothing. That is not the fix failing to apply — it is evidence the X is on a don't-care bit.
Two distinct faults, and the review caught only the second.
The real fault is upstream: opcode is X out of reset. The decoder is downstream of a missing initialisation.
The masking fault is that the decoder wildcards bits 3:2 while its four patterns are distinguished solely by bits 1:0 — so the decoder genuinely does not depend on 3:2, and an X there is invisible by construction. casex additionally masks X on bits 1:0; casez does not. Replacing one with the other fixed a blind spot the design did not have and left the one it did.
casez is still right — it removes the wildcard blind spot on the care bits — but it is not sufficient. Check the operand explicitly:
always_comb begin
ctrl = '0;
// The X check the case statement structurally cannot perform: a wildcarded
// bit is excluded from matching, so no case variant can flag it.
if ($isunknown(opcode))
ctrl = ILLEGAL; // or leave '0 and let an assertion fire
else
casez (opcode)
4'b??00: ctrl = LOAD;
4'b??01: ctrl = STORE;
4'b??10: ctrl = ALU;
4'b??11: ctrl = BRANCH;
default: ctrl = '0;
endcase
end
// Better still, make it a verification failure rather than a silent recode:
a_opcode_known: assert property (@(posedge clk) disable iff (!rst_n)
!$isunknown(opcode))
else $error("[%0t] opcode has X bits: %b", $time, opcode);Do not delegate X-detection to the pattern matcher. A case statement decides which branch, not whether the input is meaningful; a wildcard is a statement that a bit is irrelevant, and an irrelevant bit cannot also be a bit you want checked. Use $isunknown or an assertion for the second job.
Then treat the rule with its real scope: "use casez not casex" is about the care bits. It is correct and worth following, but quoting it as blanket X-protection is what let this reviewer close the ticket with the bug still in the design. Where a decoder truly must depend on a bit, do not wildcard that bit — a ? there converts a decode bug into silence.
Proving it — when casez helps and when it cannot
Forty lines settle the argument permanently. The same X is placed once on a care bit and once on a wildcard bit, and run through both keywords.
`timescale 1ns/1ps
module casez_x_proof;
typedef enum logic [1:0] {LOAD, STORE, ALU, BRANCH} ctrl_e;
// Decoder wildcarding bits 3:2, deciding on bits 1:0.
function automatic ctrl_e dec_casex(input logic [3:0] op);
casex (op)
4'bxx00: return LOAD; 4'bxx01: return STORE;
4'bxx10: return ALU; 4'bxx11: return BRANCH;
default: return LOAD;
endcase
endfunction
function automatic ctrl_e dec_casez(input logic [3:0] op);
casez (op)
4'b??00: return LOAD; 4'b??01: return STORE;
4'b??10: return ALU; 4'b??11: return BRANCH;
default: return LOAD;
endcase
endfunction
// A match is "exposed" when no specific arm fires and we fall to default.
// Encode that by returning a sentinel from default instead: simpler here to
// test the arms directly with a known-good reference.
initial begin
logic [3:0] x_on_wildcard = 4'bxx00; // X on bits 3:2 - the '?' positions
logic [3:0] x_on_care = 4'b00x0; // X on bit 1 - a '0'/'1' position
$display(" opcode casex casez");
$display(" ----------------------------------------");
$display(" 4'bxx00 -> %-12s %-12s", dec_casex(x_on_wildcard).name(),
dec_casez(x_on_wildcard).name());
$display(" 4'b00x0 -> %-12s %-12s", dec_casex(x_on_care).name(),
dec_casez(x_on_care).name());
$display("");
$display(" Row 1: X sits on the wildcarded bits. BOTH decoders return LOAD.");
$display(" casez did NOT help - the '?' excluded those bits.");
$display(" Row 2: X sits on a care bit. casex still matches an arm;");
$display(" casez matches none and falls through to default.");
$display("");
$display(" Conclusion: casez removes the blind spot on CARE bits only.");
$finish;
end
endmodule opcode casex casez
----------------------------------------
4'bxx00 -> LOAD LOAD
4'b00x0 -> LOAD LOAD
Row 1: X sits on the wildcarded bits. BOTH decoders return LOAD.
casez did NOT help - the '?' excluded those bits.
Row 2: X sits on a care bit. casex still matches an arm;
casez matches none and falls through to default.
Conclusion: casez removes the blind spot on CARE bits only.Read row 2 carefully, because it exposes a second trap. Both columns print LOAD — but for opposite reasons. casex matched the 4'bxx00 arm (X is a wildcard, so bit 1 matched 0). casez matched nothing and fell through to default: return LOAD. The decoder returns the same value either way, so the X is invisible again — this time because the default silently supplies a plausible answer.
That is the practical lesson the keyword debate obscures: casez moves an unmatched X into the default arm, and a default that returns a valid-looking value hides it just as effectively as casex did. Make the default distinguishable — ILLEGAL, an X, or an assertion — or the improvement exists only on paper.
Bug 3 — unique case fires spurious warnings: the one-hot bus has an idle state
Category: unique case misuse. Buggy code:
// ❌ BUG: grant can be 4'b0000 (bus idle) — unique case warns
always_comb begin
unique case (grant)
4'b0001: bus_out = data_a;
4'b0010: bus_out = data_b;
4'b0100: bus_out = data_c;
4'b1000: bus_out = data_d;
// grant=4'b0000 (idle): no branch → "unique case: no match" warning
// Simulation log: thousands of warnings during bus idle cycles
endcase
end
// ✅ FIX A: add default for the idle case
always_comb begin
unique case (grant)
4'b0001: bus_out = data_a;
4'b0010: bus_out = data_b;
4'b0100: bus_out = data_c;
4'b1000: bus_out = data_d;
default: bus_out = '0; // covers idle — no more warnings
endcase
end
// ✅ FIX B: add a zero assignment before case (alternative)
always_comb begin
bus_out = '0; // idle state covered here
unique case (grant)
4'b0001: bus_out = data_a;
4'b0010: bus_out = data_b;
4'b0100: bus_out = data_c;
4'b1000: bus_out = data_d;
endcase
endBug 4 — Overlapping casez patterns: the wrong branch fires for some inputs
Category: casez pattern overlap. Buggy code:
// ❌ BUG: overlapping casez patterns — opcode 4'b1100 matches BOTH
always_comb begin
ctrl = ILLEGAL;
casez (opcode)
4'b1???: ctrl = BRANCH; // matches 1000..1111 (8 patterns)
4'b1100: ctrl = JUMP; // ❌ also matches 1100 — but BRANCH already took it!
4'b0???: ctrl = ALU;
endcase
end
// opcode=4'b1100: BRANCH fires (first match in sim)
// JUMP branch is UNREACHABLE — dead code in simulation AND synthesis
// Lint: "unreachable case item" warning
// ✅ FIX: put more specific patterns BEFORE general wildcard patterns
always_comb begin
ctrl = ILLEGAL;
casez (opcode)
4'b1100: ctrl = JUMP; // ✅ specific first — catches 1100 before wildcard
4'b1???: ctrl = BRANCH; // wildcard last — catches remaining 1xxx
4'b0???: ctrl = ALU;
default: ctrl = ILLEGAL;
endcase
end
// Rule: in casez, order from MOST SPECIFIC to LEAST SPECIFIC pattern.Bug 5 — case inside always_ff with blocking assignment: the pipeline collapses
Category: blocking assignment in sequential logic. Buggy code:
// ❌ BUG: blocking = in always_ff case — 2-stage pipeline becomes 1-stage
always_ff @(posedge clk) begin
case (opcode)
ADD: begin
stage1 = a + b; // blocking: stage1 updates immediately
stage2 = stage1; // blocking: reads NEW stage1 — same cycle!
end
SUB: begin
stage1 = a - b;
stage2 = stage1; // stage2 = a-b in same cycle → 1-stage not 2
end
endcase
end
// Expected: stage2 = stage1 from PREVIOUS cycle (2-cycle latency)
// Actual: stage2 = stage1 from THIS cycle (1-cycle latency)
// ✅ FIX: use non-blocking <= — evaluates RHS before updating LHS
always_ff @(posedge clk) begin
case (opcode)
ADD: begin stage1 <= a + b; stage2 <= stage1; end // ✅ old stage1
SUB: begin stage1 <= a - b; stage2 <= stage1; end
endcase
endBug 6 — case expression width mismatch: synthesis generates the wrong comparison
Category: width mismatch. Buggy code:
// ❌ BUG: opcode is 4 bits, but patterns are only 2 bits
logic [3:0] opcode;
always_comb begin
ctrl = '0;
case (opcode)
2'b00: ctrl = ADD; // ❌ 2-bit literal vs 4-bit expression
2'b01: ctrl = SUB; // simulator: zero-extends to 4'b0000, 4'b0001
2'b10: ctrl = AND; // so opcode=4'b0100 also matches 2'b00 after zero-extend?
2'b11: ctrl = OR; // No — zero-extension means 2'b00→4'b0000 only
// But: opcode=4'b0100 → no match → latch!
endcase
end
// The patterns ONLY match opcode[3:0] = 0000, 0001, 0010, 0011
// opcode[3:0] = 0100..1111: no match → latch on ctrl
// Lint: "case item width mismatch" WARNING — treat as ERROR
// ✅ FIX: match expression width in all patterns
always_comb begin
ctrl = '0; // default covers missing patterns
case (opcode)
4'b0000: ctrl = ADD; // ✅ 4-bit literals match 4-bit expression
4'b0001: ctrl = SUB;
4'b0010: ctrl = AND;
4'b0011: ctrl = OR;
endcase
endBug 7 — Partial output assignment in case: some outputs latch, others do not
Category: partial latch inference. Buggy code:
// ❌ BUG: mem_write is only assigned in one branch, not all
always_comb begin
case (opcode)
LD: begin reg_write = 1'b1; mem_read = 1'b1; end
ST: begin reg_write = 1'b0; mem_write = 1'b1; end // mem_read not assigned!
ALU: begin reg_write = 1'b1; end // mem_read, mem_write not assigned!
default: begin reg_write = 1'b0; end // mem_read, mem_write not assigned!
endcase
end
// Result: reg_write has no latch (assigned in every branch via default)
// mem_read: LATCH inferred (only assigned in LD branch)
// mem_write: LATCH inferred (only assigned in ST branch)
// Tool ERROR: "Latch inferred on mem_read, mem_write"
// ✅ FIX: assign ALL outputs at top before case
always_comb begin
{reg_write, mem_read, mem_write} = 3'b000; // ← covers ALL paths
case (opcode)
LD: begin reg_write = 1'b1; mem_read = 1'b1; end
ST: begin mem_write = 1'b1; end
ALU: begin reg_write = 1'b1; end
default:; // all held at '0 from top assignment
endcase
endBug 8 — priority case (1'b1) with no active request: simulation warns, output undefined
Category: no-match warning. Buggy code:
// ❌ BUG: what if req = 4'b0000 (no interrupt pending)?
always_comb begin
priority case (1'b1)
req[3]: irq_id = 2'd3;
req[2]: irq_id = 2'd2;
req[1]: irq_id = 2'd1;
req[0]: irq_id = 2'd0;
// req=4'b0000 → no item equals 1'b1 → no match
// priority case: "no condition true" WARNING fires
// irq_id is NOT assigned → LATCH holds last value
endcase
end
// ✅ FIX: gate with irq_active, or add default
always_comb begin
irq_id = 2'd0; // default: irq 0 (or idle encoding)
irq_active = 1'b0;
priority case (1'b1)
req[3]: begin irq_id = 2'd3; irq_active = 1'b1; end
req[2]: begin irq_id = 2'd2; irq_active = 1'b1; end
req[1]: begin irq_id = 2'd1; irq_active = 1'b1; end
req[0]: begin irq_id = 2'd0; irq_active = 1'b1; end
endcase
// Default assignment at top covers req=0000 silently — no warning, no latch
end💡 Senior Verification Engineer Tip: Make casez Pattern Overlap Visible
When you use casez in RTL, always pair it with a lint rule that detects overlapping case items. Synopsys Spyglass and Cadence JasperGold have specific rules like STARC-2.1.4.5 (overlapping case items in casez). Many teams miss this because casez overlaps are legal SystemVerilog — the tool silently takes the first match. Discovering an unreachable case item in post-synthesis simulation is far more expensive than catching it during RTL lint review. Set the lint rule severity to ERROR for casez overlap detection.
Interview Q&A — From Fresher to Principal Engineer
case tells the synthesis tool the branches are mutually exclusive parallel conditions, so it infers a decoder — flat logic whose critical path is roughly one decode level plus one mux level. if / else if implies priority: each condition matters only when all earlier ones are false, so the tool infers a chain of cascaded 2:1 muxes whose depth grows linearly with the branch count.
In simulation both evaluate top-to-bottom and take the first match, which is why the difference is invisible in a testbench and shows up only in timing reports. That asymmetry — identical simulation, different hardware — is the reason this question is asked.
Where This Is Specified
case, casez, and casex are defined in IEEE Std 1800 (SystemVerilog), clause 12 — Procedural programming statements — the case statement and its wildcard variants in §12.5, and the unique/priority decision modifiers with their violation-reporting semantics in §12.4–12.5. The IEEE Standards Association listing is the primary source.
The sentence worth reading in the clause rather than taking from a summary is the definition of the wildcard behaviour: casez treats z (and its ? alias) as don't-care in either the expression or the case item, and casex extends that to x as well. Read precisely, that says a wildcard bit is excluded from the comparison — it does not "reject" anything. That single distinction is what makes the Bug 2 blind spot above possible, and it is why casez is a narrowing of the X hazard rather than a cure.
Equally worth noting is what the standard does not promise: unique and priority are decision modifiers whose violation reports are simulation behaviour. They convey intent to synthesis, but a unique case whose arms overlap in silicon is a lie the tool is entitled to believe — the standard does not make the tool check it for you.
Related lessons. The conditional counterpart is if-else with unique and priority; the enum types these decode are enums, and the procedural context is procedural blocks. For the X behaviour Bug 2 turns on, see 4-state vs 2-state types, and for what an X does once it reaches gates, where X comes from and X through muxes and flops.
Part of SystemVerilog Fundamentals·Procedural Statements·Lesson 31 of 53
View program