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 ChainopcodeDECODERall branchesevaluated inparallel2'b00 → a + b2'b01 → a - b2'b10 → a & b2'b11 → a | bUnlike if-else, there is no priority relationship between branches. The synthesis tool generates flat parallel logic. 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. Suppose sel[3:0] has an X on bit 2 due to a reset bug. With casex and pattern 4'b1x00, the X in the expression matches the x in the pattern — the branch fires. Your simulation output looks correct. But sel[2] is X — the hardware is in an undefined state. You've masked the root cause. With casez and pattern 4'b1?00, the X in the expression does not match ? (Z only matches Z/?, not X). No branch matches, output becomes X, X propagates through downstream logic, your simulation flags the problem. casez exposes bugs; casex buries them. This is why ARM, Intel, Qualcomm, and virtually every ASIC methodology bans casex from synthesizable RTL.
| 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.
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 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
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. 1Missing default in FSM case → Unknown State After Reset GlitchFSM / Latch BugBuggy 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
end1Root Cause / Waveform Symptom / PreventionWaveform SymptomFSM 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: opcode[3:2] is X due to a reset bug in the upstream register
// casex treats X in the expression as a wildcard — silently matches
always_comb begin
ctrl = '0;
casex (opcode) // ❌ casex: X bits are wildcards
4'bxx00: ctrl = LOAD; // matches ANY opcode where bits[1:0]=00
4'bxx01: ctrl = STORE; // including opcode = 4'bXX00!
4'bxx10: ctrl = ALU;
4'bxx11: ctrl = BRANCH;
endcase
end
// opcode = 4'bXX00: casex matches 4'bxx00 → ctrl = LOAD
// Simulation: looks correct! X masked.
// Reality: opcode is undefined — we don't know if it's LOAD.
// Bug reaches silicon: random behavior at startup.
// ✅ FIX: use casez — X in expression does NOT match ?
always_comb begin
ctrl = '0;
casez (opcode)
4'b??00: ctrl = LOAD; // ? matches Z, not X
4'b??01: ctrl = STORE; // opcode=XX00 → no match → ctrl=X → bug visible
4'b??10: ctrl = ALU;
4'b??11: ctrl = BRANCH;
default: ctrl = '0;
endcase
end3unique case Fires Spurious Warnings — One-Hot Bus Has Idle Stateunique case MisuseBuggy 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
end4Overlapping casez Patterns — Wrong Branch Fires for Some Inputscasez Pattern OverlapBuggy 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.5case Inside always_ff with Blocking Assignment — Pipeline CollapsesBlocking in FFBuggy 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
end6case Expression Width Mismatch — Synthesis Generates Wrong ComparisonWidth MismatchBuggy 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
end7Partial Output Assignment in case — Some Outputs Latch, Others Don'tPartial LatchBuggy 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
end8priority case(1) With No Active Request — Simulation Warns, Output UndefinedNo-Match WarningBuggy 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
Beginner Level
BeginnerWhat is the difference between case and if-else in terms of hardware inference?case tells the synthesis tool that branches are mutually exclusive parallel conditions — it infers a decoder (flat parallel logic). The critical path is typically 1 decoder level + 1 mux level.if-else if implies priority — the first condition is checked first, and later conditions are only relevant when earlier ones are false. Synthesis infers a series of cascaded 2:1 muxes. The critical path grows linearly with the number of branches. In simulation, both evaluate top-to-bottom and take the first match. The difference only appears in synthesis, which is why code review and tool checks matter.BeginnerWhat is the difference between casez and casex, and which should you use in RTL?casez: treats Z and ? in patterns as wildcards (don't cares). X values in the expression are NOT treated as wildcards — they must match X in the pattern. This preserves X-propagation, which is critical for debugging uninitialized signals.casex: treats both X and Z in the expression as wildcards. If any bit in the case expression is X, it matches any pattern bit. This silently hides X-propagation bugs.Always use casez in RTL, never casex. casex is banned by virtually all ASIC RTL style guides. Use casez with ? wildcards for partial opcode matching. casez lets X values propagate to outputs, making simulation bugs visible.BeginnerWhy must every case statement in always_comb have a default?Without a default, any case expression value not listed in the case items leaves all outputs unassigned — the synthesizer infers a latch to hold the last value. This creates unintended memory in what should be combinational logic. There are two reasons this is dangerous even when you think all patterns are covered: (1) In 4-state simulation, the expression may be X or Z — neither matches a binary pattern, so the latch holds whatever value existed before. (2) Future engineers adding new opcodes or enum values may miss updating the case, silently adding unhandled patterns. Best practice: assign all outputs to safe defaults before the case statement, then optionally add default: ; explicitly for documentation.
Intermediate Level
IntermediateWhat does unique case guarantee that plain case does not? When should you use each?unique case adds two simulation-time guarantees not present in plain case: 1. Mutual exclusivity: at most one branch can match at any time. If two branches match, the simulator issues a violation warning. 2. Completeness: at least one branch always matches. If no branch matches (and there is no default), the simulator warns. These translate to synthesis intent: the tool generates a parallel decoder (no priority gates) rather than a priority encoder. Use plain case when: you're not certain conditions are exclusive, or you have a default that safely handles the unexpected. Use unique case when: patterns are architecturally guaranteed to be mutually exclusive (FSM state decodes, one-hot encodes, exact opcode matching). The simulation warnings catch violations that would otherwise reach silicon silently.IntermediateExplain the case(1'b1) idiom. Why is it used?The case (1'b1) idiom evaluates each case item as a boolean expression and fires the branch where the expression equals 1'b1 (i.e., is true). It's equivalent to a series of if-else if branches but reads as a clean tabular format. Example: case (1'b1) req[3]: ... req[2]: ... endcase fires the branch for the highest-asserted req bit. It's used because: (1) for 8+ priority levels, a table of case items is far more readable than 8 nested else if statements, (2) it pairs naturally with priority case for explicit priority documentation, (3) it avoids the width-matching complexity of a regular case expression. The hardware generated by priority case (1'b1) is a priority encoder — identical to what priority if-else if produces.IntermediateIn casez, which order should you list patterns when some are more specific than others?In casez, always list more specific patterns before wildcard patterns. The simulator evaluates top-to-bottom and takes the first match. A general wildcard pattern (like 4'b1???) will match any 1xxx input — if it comes before a specific pattern (like 4'b1100), the specific pattern becomes unreachable dead code. Example: list 4'b1100 (specific) BEFORE 4'b1??? (general). The general pattern then catches all 1xxx inputs that don't match the specific patterns above it. Lint tools detect unreachable casez items. If you see a "unreachable case item" lint warning, it almost always means a wildcard pattern is shadowing a specific one above it — reverse the order.
Debugging / Advanced Level
AdvancedAn FSM stops responding after operating for several hours in silicon. Testbench simulations pass 100%. What is the most likely root cause involving the state machine case statement?The most likely root cause is missing default in the FSM case, combined with a rare event (power integrity glitch, EMI event, thermal noise) that corrupts the state register to an illegal encoding. Without default: next = IDLE;, an illegal state has no outgoing transitions — the FSM locks permanently. In simulation, clean resets and controlled stimulus never exercise illegal encodings, so the test passes. In silicon, billions of clock cycles create opportunities for rare events that never appear in simulation. The fix: add default: next = SAFE_STATE; to every FSM case. This is a P0 issue in most safety-critical design guidelines (ISO 26262, IEC 61508). The lint tool check is "FSM_NO_DEFAULT_STATE" — it should be a blocker-level rule. A second related cause: if the FSM uses binary encoding, a single-bit error in the state register shifts to a valid-but-wrong state, producing incorrect behavior rather than a lockup. One-hot encoding with error detection is safer for critical FSMs.AdvancedYour casez instruction decoder passes RTL simulation but fails gate-level simulation after synthesis. Walk through your debug process.Gate-level sim failure after RTL sim pass typically means a simulation-synthesis mismatch. For casez, the most common causes are:Step 1: Check for overlapping casez patterns. In RTL simulation, the first match wins. In synthesis, overlapping patterns can produce incorrect logic minimization — the tool may merge branches in a way that doesn't match the simulation priority. Look for "unreachable case item" lint warnings you may have ignored.Step 2: Check the pattern width. If case item widths don't match the expression width, zero-extension changes which patterns match. Run +lint=all in VCS or equivalent.Step 3: Check for missing default in RTL. The latch behavior in RTL sim (holds old value) may accidentally produce correct outputs in a directed test, while the synthesized latch cell behaves differently under timing (hold time violation).Step 4: Compare the RTL and gate-level netlists for the specific failing scenario. Use formal equivalence checking (Formality, Conformal) to mathematically prove RTL-to-netlist equivalence — this catches all functional mismatches at once.SynthesisWhy does unique case generate better QoR (Quality of Results) than plain case, and when does this improvement disappear?Why unique case generates better QoR: The synthesis tool receives a formal guarantee that exactly one branch fires at a time. It can generate a one-hot decoder or a parallel mux instead of a priority chain. For an N-branch decode, this reduces logic depth from N levels (priority chain) to 1–2 levels (decoder + mux). In 28nm at 1GHz, this can recover 400–800ps of timing budget for wide decoders.When the improvement disappears: 1. When the case has very few branches (2–3): the synthesis tool already optimizes away the priority chain through don't-care analysis. unique case adds no additional benefit. 2. When all patterns use wildcard bits (casez with many ?): the decoder must still handle pattern overlap, limiting parallelism. 3. When the tool's optimization engine (-map_effort high) is already at maximum — it may produce equivalent QoR regardless of the modifier. The simulation safety benefit of unique case (catching illegal encodings) exists regardless of timing improvement — use it even when QoR gain is minimal.