Day 3 · Procedural Combinational Logic

if/else and case

Video 2 of 4 · ~14 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

always @(*)if/else & caseLatch ProblemBlock vs Nonblock

🌍 Where This Lives

In Industry

The if/else vs case decision shows up every time you write a priority encoder vs a mux. CPU instruction decoders use case. Interrupt controllers use if/else (priority order matters). Bus arbiters often use casez with don't-cares. Each synthesizes to physically different topologies.

In This Course

Day 3 ALU: case on opcode. Day 7 FSM: case on state. Day 9 memory: if/else for address decoding. Day 11 UART framing: case on state. The pattern recurs across nearly every lab.

⚠️ Same Logic, Different Hardware

❌ Wrong Model

if/else and case are interchangeable. I'll pick whichever reads nicer.”

✓ Right Model

if/else if/else is priority-encoded: earlier conditions have shorter combinational paths — first match wins. case is parallel: all branches compared simultaneously, no priority. Different topologies, different timing, different LUT counts on wide cases.

Consequence: If priority matters (interrupt sources, error masking), use if/else if. If alternatives are mutually exclusive (opcode decode, state decode), use case — the parallel topology is usually faster.

👁️ I Do — if/else if = Priority Chain

always @(*) begin
    if      (a) y = 4'd1;   // highest priority
    else if (b) y = 4'd2;
    else if (c) y = 4'd3;
    else        y = 4'd0;   // final else prevents latch
end

Synthesized structure:

   a → MUX ─┬─────────────── y
            │
   b → MUX ─┘
            │
   c → MUX ─┘
My thinking: Each else if adds a mux to the chain. Signal a reaches y through 1 mux; signal c through 3. On wide chains (8+ priorities), the deepest path becomes a timing bottleneck.

👁️ I Do — case = Parallel Mux

always @(*) begin
    case (opcode)
        2'b00: result = a + b;      // ADD
        2'b01: result = a - b;      // SUB
        2'b10: result = a & b;      // AND
        2'b11: result = a | b;      // OR
        default: result = 4'b0;     // always include default
    endcase
end

Synthesized structure:

      a+b ─┐
      a-b ─┤
      a&b ─┤ → 4:1 MUX → result
      a|b ─┘     ↑
              opcode
My thinking: Every branch is equally accessible. All four results compute in parallel, then the opcode picks one. Fixed delay regardless of which branch is selected.

🤝 We Do — casez for Don't-Cares

// Priority encoder for a 4-bit request bus
always @(*) begin
    casez (i_req)
        4'b1???: o_grant = 3'd4;   // MSB wins
        4'b01??: o_grant = 3'd3;
        4'b001?: o_grant = 3'd2;
        4'b0001: o_grant = 3'd1;
        default: o_grant = 3'd0;
    endcase
end

What if i_req = 4'b1011? Which grant wins?

Answer: 3'd4. The MSB is 1, so the first pattern matches — the ?s in bits 2:0 are don't-care. Even though bit 0 is also 1, it never gets inspected. This is exactly priority-encoder semantics.
Use casez, never casex. casex also treats X as wildcard, which can hide simulation bugs where X propagation should trigger a failure. Almost every modern lint rule bans casex.

🧪 You Do — Predict the Hardware

An 8-way decoder implemented two ways. Which is faster on iCE40?

// Version A: case
case (sel)
  3'd0: y = i0;
  3'd1: y = i1;
  ...
  3'd7: y = i7;
endcase
// Version B: if/else if
if      (sel==0) y = i0;
else if (sel==1) y = i1;
...
else if (sel==7) y = i7;

Predict: LUT count? Critical path depth?

Answer: Version A (case) synthesizes to a balanced mux tree, ~3 LUT levels deep. Version B (if/else) can synthesize to a 7-deep priority chain — slower by ~2× on wide decoders. Yosys sometimes optimizes B into a tree when it proves conditions are mutually exclusive, but don't rely on it — use case when you mean “parallel.”
▶ LIVE DEMO

4-bit ALU with case

~5 minutes

▸ COMMANDS

cd labs/week1_day03/ex3_alu/
make sim      # exhaustive per opcode
make wave
make stat

▸ EXPECTED STDOUT

PASS: ADD  5+3  = 8
PASS: SUB  5-3  = 2
PASS: AND  F&A  = A
PASS: OR   5|A  = F
=== 16 passed, 0 failed ===

▸ GTKWAVE

Signals: opcode · a · b · result. Set opcode to enum display (2'b00=ADD, 2'b01=SUB, 2'b10=AND, 2'b11=OR). Watch result switch instantly as opcode changes — parallel mux in action.

🔧 What Did the Tool Build?

$ yosys -p "read_verilog alu_4bit.v; synth_ice40 -top alu; stat" -q

=== alu ===
   Number of wires:                 14
   Number of cells:                 20
     SB_CARRY                        4    ← adder carry chain (for ADD/SUB)
     SB_DFF                          0
     SB_LUT4                        16    ← 4 per operation × 4 operations
The math: Each of the 4 operations produces 4 output bits = 16 LUTs. The adder/subtractor share 4 carry cells. Total: 20 cells for a 4-bit, 4-op ALU. Scales linearly with operand width and operation count.
Scaling check: 32-bit 8-op ALU ≈ 32×8 = 256 LUTs + 32 carry. Still fits the iCE40 HX1K easily — until you try to add a multiplier.

🤖 Check the Machine

Ask AI: “Explain the timing difference between an 8-way if/else-if and an 8-way case in Verilog after synthesis.”

TASK

Ask about timing difference, 8-way.

BEFORE

Predict: case is O(log N) depth, if/else is O(N). Difference grows with width.

AFTER

AI should mention “priority chain” and “mux tree.” If it says they're equivalent, model is weak.

TAKEAWAY

Verify: synth both, compare depth reports from opt_expr -fine.

Key Takeaways

if/else = priority chain. case = parallel mux.

 Use case when alternatives are equal; if/else when priority matters.

casez for don't-cares (never casex).

 Always include default (habit) and a final else (safety).

Write your intent. The synthesizer can only optimize what you told it you wanted.

🔗 Transfer

The Latch Problem

Video 3 of 4 · ~12 minutes

▸ WHY THIS MATTERS NEXT

You've noticed I keep saying “always include a final else” and “always include default.” Video 3 is the why. The consequence of skipping either is an inferred latch — the single most dangerous silent bug in combinational RTL. You'll learn to spot it, prevent it three different ways, and hunt for it in existing code.