Skip to main content

Conditional and Case Statements

if/else and case are the two decision-making constructs used inside procedural (always/initial) blocks. They can often express the same logic, but they synthesize to subtly different hardware shapes — knowing which to reach for, and which pitfalls each one carries, is core to writing behavioral RTL that means what it looks like it means.

if/else: priority logic​

An if/else if/else chain synthesizes to priority logic — the first matching condition wins, and every condition after it is implicitly gated by all the earlier ones having failed:

always @(*) begin
if (req3) grant = 3'b100;
else if (req2) grant = 3'b010;
else if (req1) grant = 3'b001;
else grant = 3'b000;
end

This is a priority encoder — if req3 and req1 are both asserted, req3 wins outright, because the hardware this describes is a chain of muxes, each one only reached if every earlier condition was false:

req3 ──┐
▼
grant=100──[MUX]──┐
▲ │
req2 │
│ ▼
grant=010──[MUX]──[MUX]──► grant
▲ ▲
req1 (falls through
│ if req3 false)
grant=001──[MUX]
▲
(else: grant=000)

Each level of else if adds one more mux stage to the chain — a long if/else if chain is a long, and therefore slow, chain of priority logic. That's a real hardware cost, not just a style concern, and it's the main practical reason to prefer case once there are more than a handful of genuinely equal-priority branches.

case: parallel selection​

A case statement compares one expression against a list of values and synthesizes to parallel selection logic (a wide mux) instead of a priority chain, when the cases are mutually exclusive:

always @(*) begin
case (opcode)
2'b00: result = a + b;
2'b01: result = a - b;
2'b10: result = a & b;
2'b11: result = a | b;
endcase
end

Every branch here is reached directly by opcode's value, in parallel, with no chain of falling-through conditions — a 4-to-1 mux selecting between four ALU operations, structurally the same mux shape as Digital Design's Multiplexers, Decoders & Comparators page, just with opcode as the select signal.

The missing-default latch bug​

always @(*) begin
case (opcode)
2'b00: result = a + b;
2'b01: result = a - b;
// 2'b10 and 2'b11 not covered!
endcase
end
Always include a default in combinational case statements

This case doesn't say what result should be when opcode is 2'b10 or 2'b11 — and "don't change" is exactly what a real digital circuit does when nothing tells it otherwise, which means synthesis infers a latch to hold result's previous value across those uncovered cases. That's very likely not what was intended, and it silently turns an always-block meant to be pure combinational logic into a mix of combinational logic and inferred latches. A default: result = 0; (or any defined fallback) branch closes every path and guarantees purely combinational hardware. This exact failure mode — an always block that accidentally infers a latch — is covered in full in Combinational vs. Sequential always Blocks.

casez and casex: don't-care matching​

casez treats z (and, by convention, ?) in the case item as a don't-care bit position; casex additionally treats x as don't-care. These exist specifically for priority-encoded, overlapping patterns that plain case can't express, since plain case requires an exact bitwise match:

always @(*) begin
casez (req[3:0])
4'b1???: grant = 2'd3; // req[3] set — highest priority, ignore the rest
4'b01??: grant = 2'd2;
4'b001?: grant = 2'd1;
4'b0001: grant = 2'd0;
default: grant = 2'd0;
endcase
end

This is a 4-input priority encoder — each ? means "don't care what this bit is," so 4'b1??? matches any req value with bit 3 set, regardless of the other three bits. Because the branches are checked top-to-bottom and the first match wins (exactly like if/else if), casez here restores priority behavior inside a case statement, combining case's pattern-matching syntax with if/else's priority semantics.

casex is best avoided in synthesizable RTL

casex treats an x in the case expression itself (not just the case items) as a don't-care that can match anything — including in real simulation waveforms where an x usually means "genuinely uninitialized or contended," not "don't care." That mismatch has caused real, hard-to-find bugs where casex matched a branch it shouldn't have simply because a signal happened to be uninitialized. casez doesn't have this problem, since it only treats z specially. Prefer casez (or plain if/else if priority logic) over casex in RTL.

Full-case / parallel-case pragmas: a legacy trap​

Older code sometimes carries // synopsys full_case or // synopsys parallel_case comments — synthesis-tool-specific directives telling the synthesizer "trust me, every case is covered" or "treat these as non-overlapping" even when the code doesn't actually guarantee that. These pragmas can make a simulator and a synthesizer disagree about the exact same source code, because the simulator has no idea the pragma exists and just executes the case as written, while the synthesizer takes the pragma's claim at face value. Modern style guides near-universally recommend avoiding them — write a real default branch instead of asserting one exists via a comment.

What's next​

if/case decide what happens; the next page covers repetition — for, while, repeat, and forever loops — and, crucially, which of them synthesis tools can actually turn into hardware at all.