Verilog Case Statement: casez, casex and Latch Traps
The Verilog case statement is how you describe multi-way selection hardware: multiplexers, decoders, ALU opcode selection, and FSM next-state logic. It looks like a C switch, but it behaves very differently — there is no fall-through, matching is done on all four logic values (0, 1, x, z), and a badly written case statement silently turns your combinational logic into a latch. Interviewers love it for exactly these reasons. This chapter covers plain case, the wildcard forms casez and casex, the default branch, latch inference, full vs parallel case, and when to prefer case over if-else.
How a case statement works
A case statement lives inside a procedural block (always or initial). The general shape:
case (expression)
item_1: statement_1;
item_2: statement_2;
item_3,
item_4: statement_34; // one branch for several items
default: statement_d; // runs when nothing above matched
endcaseThree rules decide which branch runs:
- 4-state exact comparison. The case expression is compared with each item bit by bit, including
xandzbits. The comparison behaves like the case-equality operator===, not==. So2'b1xmatches an item written as2'b1x— and nothing else. - First match wins. Items are evaluated top to bottom and evaluation stops at the first match. There is no fall-through and no
breakkeyword. - At most one branch executes. If nothing matches and there is no
default, no branch executes at all — the targets keep their old values. That single fact is the root of the latch-inference problem discussed below.
Here is a complete, simulated example — a tiny 2-bit-opcode ALU, the most common place you will write a case statement in real RTL:
// alu_case.v : 2-bit opcode ALU built with a case statement
module mini_alu (
input wire [1:0] op,
input wire [7:0] a,
input wire [7:0] b,
output reg [7:0] y
);
always @(*) begin
case (op)
2'b00: y = a + b; // ADD
2'b01: y = a - b; // SUB
2'b10: y = a & b; // AND
default: y = a | b; // OR (covers 2'b11 and any x/z op)
endcase
end
endmodule
module tb_mini_alu;
reg [1:0] op;
reg [7:0] a, b;
wire [7:0] y;
integer i;
mini_alu dut (.op(op), .a(a), .b(b), .y(y));
initial begin
a = 8'd12;
b = 8'd10;
for (i = 0; i < 4; i = i + 1) begin
op = i[1:0];
#5;
$display("op=%b a=%0d b=%0d -> y=%0d", op, a, b, y);
end
$finish;
end
endmoduleop=00 a=12 b=10 -> y=22 op=01 a=12 b=10 -> y=2 op=10 a=12 b=10 -> y=8 op=11 a=12 b=10 -> y=14 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpuv4c__qj/alu_case.v:34: $finish called at 20 (1s)
Note the default handles the last opcode. Writing 2'b11 explicitly and then adding default: y = 8'h00; would also be fine — the point is that every possible value of op assigns y something.
case vs if-else: which one and when
Both describe selection, but they say different things to the synthesis tool:
| Aspect | case | if / else if |
|---|---|---|
| Natural hardware | Parallel multiplexer (when items don’t overlap) | Priority chain of 2:1 muxes |
| Best for | Decoding one variable against many values: opcodes, FSM states, address decode | Conditions on different signals, or when priority is genuinely intended |
| Timing | Roughly equal delay to every input of the selector | Later conditions sit deeper in the logic chain |
| Readability | One expression, many values — very scannable | Gets noisy past 3–4 branches |
Practical rule: if every branch tests the same expression against constants, use case. If the branches test unrelated conditions (if (flush) ... else if (stall) ...), or you deliberately want priority, use if-else. Modern synthesis tools will often produce identical gates for simple examples either way, but the intent you communicate to reviewers — and to the tool in harder cases — differs.
casez: wildcard matching done right
casez treats z bits (written z or ?) in the case items as don’t-care positions. This is the clean way to write priority encoders and decoders where only a few bits of the expression matter in each branch. Use ? rather than z in your patterns — it signals “don’t care” to the reader instead of “high impedance”.
// grant_casez.v : fixed-priority grant logic using casez wildcards
module prio_grant (
input wire [3:0] req, // req[3] has highest priority
output reg [3:0] grant
);
always @(*) begin
casez (req)
4'b1???: grant = 4'b1000;
4'b01??: grant = 4'b0100;
4'b001?: grant = 4'b0010;
4'b0001: grant = 4'b0001;
default: grant = 4'b0000;
endcase
end
endmodule
module tb_prio_grant;
reg [3:0] req;
wire [3:0] grant;
integer i;
prio_grant dut (.req(req), .grant(grant));
initial begin
for (i = 0; i < 16; i = i + 1) begin
req = i[3:0];
#5;
$display("req=%b -> grant=%b", req, grant);
end
$finish;
end
endmodulereq=0000 -> grant=0000 req=0001 -> grant=0001 req=0010 -> grant=0010 req=0011 -> grant=0010 req=0100 -> grant=0100 req=0101 -> grant=0100 req=0110 -> grant=0100 req=0111 -> grant=0100 req=1000 -> grant=1000 req=1001 -> grant=1000 req=1010 -> grant=1000 req=1011 -> grant=1000 req=1100 -> grant=1000 req=1101 -> grant=1000 req=1110 -> grant=1000 req=1111 -> grant=1000 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpi_9do8rz/grant_casez.v:30: $finish called at 80 (1s)
Look at the log: req=0111 grants bit 2, req=1011 grants bit 3 — the wildcards plus top-to-bottom matching give you priority behaviour in five readable lines. Writing the same thing with plain case would need all 16 patterns.
One caution: because the wildcard applies to both sides of the comparison, a z on the req bus itself would also match a ? position. In synthesis that is irrelevant (real gates carry 0/1), but in simulation it can mask a floating input, so keep testbench checks for x/z on interfaces.
casex: why most coding guidelines ban it
casex extends the wildcard to x bits as well — in both the case items and the case expression. That second part is the trap: in simulation an x usually means “uninitialised or contended signal — a bug”. With casex, an unknown selector happily matches your first branch and the bug disappears from view. Compare the two side by side:
// casex_trap.v : how casez and casex react to an unknown (x) input
module dec_both (
input wire [1:0] sel,
output reg [3:0] yz, // decoded with casez
output reg [3:0] yx // decoded with casex
);
always @(*) begin
casez (sel)
2'b00: yz = 4'b0001;
2'b01: yz = 4'b0010;
2'b1?: yz = 4'b0100;
default: yz = 4'b0000;
endcase
casex (sel)
2'b00: yx = 4'b0001;
2'b01: yx = 4'b0010;
2'b1?: yx = 4'b0100;
default: yx = 4'b0000;
endcase
end
endmodule
module tb_dec_both;
reg [1:0] sel;
wire [3:0] yz, yx;
dec_both dut (.sel(sel), .yz(yz), .yx(yx));
initial begin
sel = 2'b00; #5 $display("sel=%b casez->%b casex->%b", sel, yz, yx);
sel = 2'b10; #5 $display("sel=%b casez->%b casex->%b", sel, yz, yx);
sel = 2'b0x; #5 $display("sel=%b casez->%b casex->%b <-- casex matched 00 anyway", sel, yz, yx);
sel = 2'bxx; #5 $display("sel=%b casez->%b casex->%b <-- casex hides a fully unknown bus", sel, yz, yx);
$finish;
end
endmodulesel=00 casez->0001 casex->0001 sel=10 casez->0100 casex->0100 sel=0x casez->0000 casex->0001 <-- casex matched 00 anyway sel=xx casez->0000 casex->0001 <-- casex hides a fully unknown bus /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpkwi9bgx1/casex_trap.v:34: $finish called at 20 (1s)
With sel = 2'bxx the casez decoder falls to default and outputs a visibly “wrong” value you can catch in verification. The casex decoder outputs 0001 — a perfectly legal-looking result driven by garbage. Gate-level simulation and silicon will not reproduce it. The standard guidance, echoed in most company RTL coding standards: use casez for wildcard matching, avoid casex in synthesisable code.
Latch inference: the classic trap
For a combinational always @(*) block, synthesis demands that every output is assigned on every path through the block. A case statement with missing arms and no default breaks that rule: for the unlisted values the output must “remember” its previous value, and the only combinational element that remembers is a level-sensitive latch. The tool infers one, prints a warning most people scroll past, and your timing closure, testability and glitch behaviour all suffer.
You can watch the stale-value behaviour directly in simulation:
// latch_case.v : same selector written twice - one infers a latch, one does not
module sel_bad (
input wire [1:0] sel,
input wire [3:0] d0,
input wire [3:0] d1,
output reg [3:0] y
);
always @(*) begin
case (sel)
2'b00: y = d0;
2'b01: y = d1;
endcase // no default: y keeps its old value for sel = 10 or 11
end
endmodule
module sel_good (
input wire [1:0] sel,
input wire [3:0] d0,
input wire [3:0] d1,
output reg [3:0] y
);
always @(*) begin
case (sel)
2'b00: y = d0;
2'b01: y = d1;
default: y = 4'b0000; // every path assigns y: pure combinational
endcase
end
endmodule
module tb_latch_demo;
reg [1:0] sel;
reg [3:0] d0, d1;
wire [3:0] y_bad, y_good;
sel_bad u_bad (.sel(sel), .d0(d0), .d1(d1), .y(y_bad));
sel_good u_good (.sel(sel), .d0(d0), .d1(d1), .y(y_good));
initial begin
d0 = 4'b1010;
d1 = 4'b0101;
sel = 2'b00; #5 $display("sel=%b y_bad=%b y_good=%b", sel, y_bad, y_good);
sel = 2'b01; #5 $display("sel=%b y_bad=%b y_good=%b", sel, y_bad, y_good);
sel = 2'b10; #5 $display("sel=%b y_bad=%b y_good=%b <-- y_bad holds stale d1", sel, y_bad, y_good);
sel = 2'b11; #5 $display("sel=%b y_bad=%b y_good=%b <-- still stuck", sel, y_bad, y_good);
$finish;
end
endmodulesel=00 y_bad=1010 y_good=1010 sel=01 y_bad=0101 y_good=0101 sel=10 y_bad=0101 y_good=0000 <-- y_bad holds stale d1 sel=11 y_bad=0101 y_good=0000 <-- still stuck /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp4o_griig/latch_case.v:46: $finish called at 20 (1s)
Two reliable ways to stay latch-free in combinational case logic:
- Always write a
defaultbranch that assigns every output the block drives, or - Assign safe default values to all outputs at the top of the block, before the case statement. Later branches simply override them. This scales better when one case statement drives many signals.
Latch inference applies only to combinational intent. Inside a clocked block (always @(posedge clk)), a case statement with missing arms is fine — “keep the old value” just means the flip-flop holds, which is normal sequential behaviour.
Full case vs parallel case
Two properties of a case statement matter to synthesis, and interviewers frequently ask you to define them:
- Full case: every value the expression can actually take is covered by some item (or by
default). A case that is not full and assigns combinational outputs → latch inference, as above. - Parallel case: no input value can match more than one item. Non-overlapping items synthesise to a flat parallel mux; overlapping items (common with
casezwildcards, like our priority grant) force priority logic, because the language says the first match wins.
Older flows encouraged pragmas such as // industry-standard EDA tools full_case parallel_case to force these properties. Treat them as red flags in modern code: they tell the synthesis tool to assume something the simulator does not enforce. If the assumption is false — say two casez arms really can match together — synthesis builds hardware that disagrees with your verified RTL simulation. That class of bug is found in gate-level sim at best, in silicon at worst. Make the case genuinely full with a default, and make priority explicit with item ordering, instead of asserting it with pragmas.
Rules you can carry into an interview
- Case comparison is 4-state and exact (
===semantics); items evaluate top to bottom; first match wins; no fall-through. - Combinational case logic: add
defaultor pre-assign all outputs, otherwise a latch is inferred. - Use
casewhen one expression is compared against constants; use if-else for unrelated conditions or deliberate priority. - Use
casezwith?wildcards for decoders and priority encoders; avoidcasexbecause it lets unknown (x) inputs silently match a branch. - Avoid
full_case/parallel_casepragmas; write code that is genuinely full and has explicit priority. - In clocked blocks, missing arms are legal and simply mean “hold the register”.
Quick check
A combinational case statement covers 6 of the 8 possible selector values and has no default. What hardware does synthesis produce for the outputs?
A level-sensitive latch on each assigned output. For the 2 uncovered values no branch assigns the output, so it must hold its previous value, and the only combinational “memory” is a latch. Fix it with a default branch or by assigning every output a safe value at the top of the block.
What is the difference between casez and casex?
casez treats only z (and the ? shorthand) as a don’t-care in the comparison; casex treats both z and x as don’t-cares. Because casex also wildcards x bits in the case expression itself, an uninitialised or contended selector can silently match a branch, hiding real bugs — which is why coding guidelines prefer casez.
When would you choose if-else over a case statement?
When the branches test different, unrelated conditions rather than one expression against constants, or when you explicitly want priority between conditions. A case statement is the better fit when a single expression (opcode, state, address) is decoded against multiple constant values.
Why are the full_case and parallel_case synthesis pragmas considered risky?
They make the synthesis tool assume coverage or non-overlap that the simulator never checks. If the assumption is wrong, the gates no longer match the simulated RTL behaviour, producing a simulation-vs-synthesis mismatch that typically surfaces only in gate-level simulation or in silicon. Writing a genuine default and ordering arms for explicit priority achieves the same result safely.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses