Skip to content

if-else & Unique/Priority Modifiers

Hardware inferred, simulation checks, coverage and synthesis safety.

Module 5 · Page 5.2

Basic if-else — The Foundation

if-else inside always_comb infers a priority mux chain — the first condition that is true wins. Every subsequent condition is checked only if all previous ones were false. This is the correct mental model for both simulation and synthesis.

SystemVerilog — if-else: all forms
// ── Form 1: simple if ─────────────────────────────────────────────
always_comb begin
out = data;                // default
if (enable) out = data_in; // override when enable=1
end
 
// ── Form 2: if-else ───────────────────────────────────────────────
always_comb begin
if (sel)
y = a;
else
y = b;      // 2-to-1 mux — every path assigns y
end
 
// ── Form 3: if-else if-else chain (priority mux) ─────────────────
always_comb begin
out = '0;               // default: prevents latch
if      (req[0]) out = data_a;  // highest priority
else if (req[1]) out = data_b;
else if (req[2]) out = data_c;
else             out = data_d;  // lowest priority
end
// If req[0] and req[2] are both 1, req[0] wins — implicit priority
 
// ── Form 4: nested if ─────────────────────────────────────────────
always_comb begin
result = '0;
if (valid) begin
if (write)  result = write_data;
else        result = read_data;
end
end

Figure 1 — if-else if-else Infers a Priority Mux ChainMUX2:1data_anext…req[0]MUX2:1data_breq[1]MUX2:1data_creq[2]data_doutreq[0] has highest priority.If multiple reqs are high,the leftmost wins — always.if-else if-else chain → series of 2:1 muxes. req[0] mux is evaluated first, its output feeds into the next. Figure 1 — A 3-level if-else if chain infers 3 chained 2:1 muxes. The select line of each mux is one condition. req[0] has the highest priority — if it's true, none of the later conditions matter.

🏗 Synthesis Concern: if-else Chain = Critical Path Problem

Every level of if-else if adds one more mux in series. A 4-level chain produces 4 cascaded 2:1 muxes — the signal must propagate through all of them before settling. In a 500MHz design with a 2ns clock budget, each mux adds ~150–250ps. A 4-level chain can burn 600–1000ps of your timing budget just in the mux tree. When conditions are mutually exclusive, unique if collapses this to a single-level parallel mux — recovering that timing immediately.

🧠 How the Simulator Evaluates if-else in always_comb

When any input to an always_comb block changes, the entire block re-evaluates in the Active region. The simulator processes the if-else if chain top to bottom — evaluating each condition expression left-to-right — and takes the first branch whose expression is true. Subsequent conditions are short-circuited: they are never evaluated. This exactly mirrors how a hardware priority mux works, where the first-asserted select overrides all others. The simulation model is bit-accurate with the synthesized hardware — as long as conditions do not overlap.

The Problem Plain if Cannot Solve

Plain if has two limitations that matter in real hardware design:

  • Overlapping Conditions — If two conditions can both be true simultaneously, the first one silently wins. There is no simulation warning. This can mask a design bug — you think conditions are mutually exclusive, but they are not.
  • Missing Condition Coverage — If no condition in a chain is ever true, and you forgot the final else, the output is unchanged — a latch is inferred. Again, no warning. The tool creates hardware you did not intend.
  • Synthesis Adds Priority Gates — Even if your conditions are truly mutually exclusive, synthesis still generates a priority chain (extra logic). You lose area and timing performance for no reason.

SystemVerilog solves all three with two keywords: unique if and priority if.

unique if — Mutually Exclusive Conditions

unique if makes two guarantees explicit: (1) at most one condition can be true at any given time (mutually exclusive), and (2) the simulator checks this during simulation and issues a warning if two conditions are simultaneously true. The synthesis tool uses the guarantee to optimise — it can generate a parallel mux instead of a priority chain, reducing logic depth. Figure 2 — unique if Enables Parallel Mux (No Priority Chain)Plain if → priority chain (more gates, slower)MUXMUXMUX3 levels of logic in seriesCritical path is long — worse timingunique if → parallel mux (fewer gates, faster)abcone-hot selMUXparallel1 level of logic — parallel evaluationFaster timing. Tool can assume only one sel is high.BONUS: simulator warns if two conditions are true simultaneously Figure 2 — unique if tells the tool conditions are mutually exclusive: synthesis generates a parallel mux (1 logic level) instead of a priority chain (N levels). Simulation warns if two conditions are simultaneously true.

SystemVerilog — unique if: syntax, behaviour, simulation checks
// ── unique if: conditions are mutually exclusive ──────────────────
always_comb begin
out = '0;
unique if (mode == 2'b00)  out = a + b;
else if  (mode == 2'b01)  out = a - b;
else if  (mode == 2'b10)  out = a & b;
else                       out = a | b;
end
// Tool knows: only one condition is ever true at a time
// → Generates parallel mux, not priority chain
// → Simulator: warns if mode matches more than one branch (impossible here, but checked)
 
// ── unique if: simulation warning demonstration ───────────────────
always_comb begin
out = '0;
unique if (a && !b)  out = 8'hAA;
else if  (!a && b)  out = 8'hBB;
else if  (!a && !b) out = 8'h00;
// What if a=1 and b=1? No branch matches!
// → Simulator issues a WARNING: "unique if: no condition is true"
// This is a design bug that plain if would SILENTLY ignore
end
 
// ── unique if is also valid inside always_ff ──────────────────────
always_ff @(posedge clk) begin
unique if (!rst_n)  q <= '0;
else if  (en)       q <= d;
end

Waveform — unique if: overlap warning vs no-match warningScenarioNormal op Overlap hit No-match hitmode2'b01 2'bXX(two true) 2'b11(no case)outa-b (correct) WARNING fired WARNING fired out='0 (held)Sim log(silent) "unique if: mult conditions true" "unique if: no condition"────────────────────────────────────────────────────────────────────── With plain if: BOTH cases above are SILENT — bugs reach silicon undetected. With unique if: warnings surface in regression — caught in simulation.

⚠ When NOT to Use unique if — The X-State Trap

If any signal driving a unique if condition is X (unknown) — which happens at simulation start before reset, or during X-propagation — the simulator cannot determine which branch is true and may fire spurious overlap warnings. During the reset phase of simulation, X values on condition signals will trigger unique if warnings even though the design is functionally correct. Two solutions: (1) use priority if instead if overlap is acceptable, or (2) add $assertoff around unique if checks during reset, or (3) ensure all signals driving conditions are reset to valid values before simulation samples them.

priority if — Explicit Priority, No Overlap Warning

priority if declares that conditions may overlap, and the first-matching branch intentionally wins. The simulator checks that at least one condition is always true (warns if none match), but does NOT warn if multiple conditions are true — overlap is expected. The synthesis tool generates an optimised priority encoder rather than a plain mux chain.

SystemVerilog — priority if: overlapping conditions are intentional
// ── priority if: overlapping conditions OK — first wins ───────────
always_comb begin
irq_id = 3'b000;
priority if (irq[0]) irq_id = 3'd0;  // IRQ0 has highest priority
else if     (irq[1]) irq_id = 3'd1;
else if     (irq[2]) irq_id = 3'd2;
else if     (irq[3]) irq_id = 3'd3;
else if     (irq[4]) irq_id = 3'd4;
end
// irq[0] and irq[3] both asserted → irq_id = 0 (intentional, no warning)
// No interrupts asserted         → simulator WARNS: "no priority if branch taken"
 
// ── priority if vs plain if — what changes? ───────────────────────
// Plain if:    • No simulation checks. Tool generates priority chain anyway.
// priority if: • Simulator warns if no branch matches (helps catch unhandled states)
//              • Tool KNOWS priority is intentional → may optimise encode logic
// unique if:   • Simulator warns if overlap OR no-match (strictest checking)
//              • Tool generates parallel mux (no priority chain needed)

🚀 RTL Design Insight: priority if Is the Correct Tool for Interrupt Controllers

Every real SoC has an interrupt controller. Multiple interrupt sources can assert simultaneously — that is not a bug, it is the normal operating mode. Using priority if explicitly declares this to the tool: "multiple conditions can be true simultaneously, and I want the highest-priority (first-listed) branch to win." The synthesis tool generates an optimised priority encoder — not a simple mux chain — which is both area-efficient and timing-clean. Using plain if achieves the same hardware but loses the simulation safety net (no warning when no IRQ is asserted). Using unique if is actively wrong here — it fires spurious simulation warnings on every clock cycle when multiple IRQs are pending.

Choosing: plain, unique, or priority?

Figure 3 — Which if Modifier Should You Use?Writing an if statementCan two conditionsbe true at once?YESpriority ifoverlap OK, warns onno-matchNOAlways ONE conditionmust be true?YESunique ifwarns overlap +no-match. Parallel mux.NOifplain, no checks Figure 3 — Decision tree for choosing between plain if, unique if, and priority if. The choice affects both simulation checking and the hardware the tool generates.

KeywordConditions can overlap?Sim warns on overlap?Sim warns on no-match?Hardware inferred
ifYes (silent)NoNoPriority mux chain
unique ifNo — must be exclusiveYes ✅Yes ✅Parallel mux (optimised)
priority ifYes — intentionalNoYes ✅Optimised priority encoder

Real-World Examples

SystemVerilog — Three Real Patterns: plain, unique, priority
// ════ Example 1: plain if — for simple sequential decisions ═══════
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else if (start)
state <= BUSY;
else if (done)
state <= DONE;
else
state <= state;
end
// Plain if is fine here: rst_n, start, done are designed to not overlap
 
// ════ Example 2: unique if — ALU opcode decode (one-hot) ══════════
always_comb begin
alu_out = '0;
unique if (op == 4'h0) alu_out = a + b;     // ADD
else if  (op == 4'h1) alu_out = a - b;     // SUB
else if  (op == 4'h2) alu_out = a & b;     // AND
else if  (op == 4'h3) alu_out = a | b;     // OR
else if  (op == 4'h4) alu_out = a ^ b;     // XOR
else if  (op == 4'h5) alu_out = ~a;       // NOT
else if  (op == 4'h6) alu_out = a << 1;   // SHL
else if  (op == 4'h7) alu_out = a >> 1;   // SHR
end
// op is a 4-bit field — only one opcode is ever encoded at a time
// unique if: warns if somehow two ops match, warns if op=8..15 (unhandled)
// synthesis: generates parallel logic, not a chain of 8 muxes
 
// ════ Example 3: priority if — interrupt controller ════════════════
always_comb begin
irq_vec = 8'h00;
irq_num = 3'd0;
priority if (irq[7]) begin irq_vec[7] = 1; irq_num = 7; end
else if     (irq[6]) begin irq_vec[6] = 1; irq_num = 6; end
else if     (irq[5]) begin irq_vec[5] = 1; irq_num = 5; end
else if     (irq[4]) begin irq_vec[4] = 1; irq_num = 4; end
else if     (irq[3]) begin irq_vec[3] = 1; irq_num = 3; end
else if     (irq[2]) begin irq_vec[2] = 1; irq_num = 2; end
else if     (irq[1]) begin irq_vec[1] = 1; irq_num = 1; end
else if     (irq[0]) begin irq_vec[0] = 1; irq_num = 0; end
end
// Multiple IRQs pending simultaneously is EXPECTED — priority if is correct
// Simulator: no overlap warning (intentional), warns if irq == 0 (nothing pending)

if inside always_ff — Reset and Enable Patterns

Inside always_ff, the if statement controls which register action fires — reset, enable, or hold. The pattern is standardised across the industry. Mastering it is essential for all RTL design.

SystemVerilog — if patterns inside always_ff (register templates)
// ── Template 1: async reset only ─────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= '0;
else        q <= d;
end
 
// ── Template 2: async reset + synchronous enable ──────────────────
always_ff @(posedge clk or negedge rst_n) begin
if      (!rst_n) q <= '0;    // async reset — highest priority
else if (en)     q <= d;     // sync load when enabled
// else: q holds its value (no assignment = hold)
end
 
// ── Template 3: async reset + sync load + sync clear ─────────────
always_ff @(posedge clk or negedge rst_n) begin
if      (!rst_n) q <= '0;    // async reset
else if (clr)    q <= '0;    // sync clear (higher priority than load)
else if (load)   q <= d;     // sync load
// else: hold
end
 
// ── Template 4: shift register with if ───────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
shreg <= 8'h00;
else
shreg <= {shreg[6:0], serial_in};  // shift left, new bit enters from right
end

🔍 Debugging Insight: Priority of if Branches inside always_ff Is Absolute

In an always_ff block, the first if branch always wins — this is not a style choice, it is the hardware reality. The classic template is: reset first (async), then synchronous clear, then synchronous load, then hold. If you accidentally write else if (!rst_n) instead of if (!rst_n) as the first branch, the reset becomes gated by the previous condition — a functional bug that only appears during specific timing scenarios where reset is asserted while the earlier condition is also true. Always verify reset is the first, unconditional if branch inside always_ff with async reset.

Common Mistakes

SystemVerilog — if-else mistakes and fixes
// ════ MISTAKE 1: Missing default in always_comb — infers latch ═══
always_comb begin
if (sel == 2'b00) out = a;   // ❌ what about sel = 2'b01, 10, 11?
else if (sel == 2'b01) out = b;  // tool errors: latch inferred for 'out'
end
// ✅ FIX: add default or final else
always_comb begin
out = '0;                        // default
if      (sel == 2'b00) out = a;
else if (sel == 2'b01) out = b;
end
 
// ════ MISTAKE 2: Confusing unique if and priority if ══════════════
// IRQ arbiter — conditions CAN overlap (multiple IRQs at once)
always_comb begin
unique if (irq[0]) id = 0;   // ❌ unique if for IRQs that can overlap!
else if   (irq[1]) id = 1;   // Simulator will warn every clock cycle
end
// ✅ FIX: use priority if when overlap is intentional
always_comb begin
priority if (irq[0]) id = 0;  // overlap OK — no warning
else if     (irq[1]) id = 1;
end
 
// ════ MISTAKE 3: Using = instead of <= in always_ff ══════════════
always_ff @(posedge clk) begin
if (en) q = d;   // ❌ blocking in always_ff → covered in 5.6
end
// ✅ FIX: always use non-blocking inside always_ff
always_ff @(posedge clk) begin
if (en) q <= d;  // ✅ correct
end

Quick Reference — if-else Cheat Sheet

SystemVerilog — if-else Quick Reference
// ── Combinational if (always_comb) ────────────────────────────
always_comb begin
out = '0;               // ALWAYS add default first!
if      (cond_a) out = a;
else if (cond_b) out = b;
else             out = c;
end
 
// ── unique if: mutually exclusive, both overlap and no-match warned
always_comb begin
out = '0;
unique if  (op==2'b00) out = a;  // one-hot / enum decode
else if (op==2'b01) out = b;
else if (op==2'b10) out = c;
else                out = d;
end
 
// ── priority if: overlap OK, warns on no-match ─────────────────
always_comb begin
id = '0;
priority if (req[0]) id = 0;   // interrupt / priority encoder
else if     (req[1]) id = 1;
else if     (req[2]) id = 2;
end
 
// ── Sequential if (always_ff) ─────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
if      (!rst_n) q <= '0;   // async reset
else if (en)     q <= d;    // sync load
// else: hold (no assignment = correct register hold)
end
 
// ── Decision guide ────────────────────────────────────────────
// Conditions mutually exclusive + must match?  → unique if
// Conditions can overlap, priority needed?     → priority if
// No special checking needed?                  → plain if
// Always add default assignment in always_comb!

🧠 Waveform & Simulation — How if-else Actually Evaluates

Understanding what actually happens inside the simulator when an if-else if chain evaluates is the foundation of debugging priority-logic bugs. The simulator is deterministic — same inputs always give same output — but the priority chain means inputs evaluated earlier in the chain completely suppress later branches. Waveform — Priority if-else chain: req[0..3] → outTime →T0 T1 T2 T3 T4 T5 T6 T7req[0]0 1 1 1 0 0 0 0req[1]0 0 1 0 1 1 0 0req[2]0 0 0 1 0 1 1 0req[3]0 0 0 0 0 0 1 1outdflt d_a d_a d_a d_b d_b d_b d_c ↑ ↑ ↑ ↑ ↑ ↑ ↑ req0 req0 req0 req1 req1 req1 req2 wins wins wins wins wins wins wins (req1 (req2 (req2 (req2 (req3 &req0 &req0 &req1 &req1 lost) losss) loses) loses) loses) At T2 and T3: multiple req bits are high simultaneously. With plain if, this is completely silent — the first true condition wins, all others are ignored. With unique if, the simulator fires a warning at T2 and T3 alerting you to the overlap. If your spec says only one request can be active at a time, those warnings identify a real upstream bug.

SystemVerilog — Simulation-Level Verification of if Modifier Behavior
// ── Testbench: proves unique if catches overlap, priority if doesn't ─
module tb_modifier_check;
logic [3:0] req;
logic [7:0] out_unique, out_priority;
 
// DUT A: unique if — should warn when req[0] && req[1] both high
always_comb begin
out_unique = 8'h00;
unique if (req[0]) out_unique = 8'hAA;
else if  (req[1]) out_unique = 8'hBB;
else if  (req[2]) out_unique = 8'hCC;
else if  (req[3]) out_unique = 8'hDD;
end
 
// DUT B: priority if — no warning on overlap, intentional
always_comb begin
out_priority = 8'h00;
priority if (req[0]) out_priority = 8'hAA;
else if     (req[1]) out_priority = 8'hBB;
else if     (req[2]) out_priority = 8'hCC;
else if     (req[3]) out_priority = 8'hDD;
end
 
initial begin
req = 4'b0000; #10;   // no requests — unique if warns (no match)
req = 4'b0001; #10;   // req[0] only — clean, no warning
req = 4'b0011; #10;   // req[0] AND req[1] — unique if WARNS, priority if silent
req = 4'b0110; #10;   // req[1] AND req[2] — unique if WARNS, priority gives BB
req = 4'b1000; #10;   // req[3] only — clean
$display("out_unique=%h out_priority=%h", out_unique, out_priority);
$finish;
end
endmodule
 
// ── Expected Simulation Output ─────────────────────────────────────
// WARNING: unique if violation — all conditions are false (at req=0000)
// WARNING: unique if violation — multiple conditions are true (at req=0011)
// WARNING: unique if violation — multiple conditions are true (at req=0110)
// out_unique=dd out_priority=dd  (final state: req=1000, no overlap)

🏗 Synthesis Deep Dive — What Hardware Each Form Actually Generates

The choice between plain if, unique if, and priority if is not just a simulation checking decision — it directly impacts the logic depth, area, and timing of your synthesized netlist.

FormSynthesized StructureLogic LevelsTiming ImpactArea Impact
if-else if (plain, N conditions)N cascaded 2:1 muxes in series (priority chain)N levelsWorst — critical path grows linearly with NN × mux cells
unique if (N mutually exclusive conditions)Parallel N:1 mux or one-hot decoder + mux1–2 levelsBest — all inputs evaluated simultaneouslySimilar or less than priority chain
priority if (N overlapping conditions)Priority encoder + mux (optimised, not naive chain)log₂(N) levelsBetter than plain if — encoder is optimisedPriority encoder + single mux
SystemVerilog — Same Logic, Three Forms, Different Netlists
// ── Scenario: 4-input mux, one-hot select ─────────────────────────
// The spec says only one of {s0,s1,s2,s3} is ever high at a time.
 
// Form A: plain if — synthesis generates 3 chained muxes
always_comb begin
out = '0;
if      (s0) out = a;   // mux1: sel=s0
else if (s1) out = b;   // mux2: sel=s1 (output of mux1 is one input)
else if (s2) out = c;   // mux3: sel=s2 (output of mux2 is one input)
else if (s3) out = d;   // mux4: sel=s3 (output of mux3 is one input)
end
// Synthesized: 3-level mux chain. s0 must propagate through ALL 3 muxes.
// Critical path: ~3 × mux_delay ≈ 750ps in 28nm
 
// Form B: unique if — synthesis generates parallel mux
always_comb begin
out = '0;
unique if (s0) out = a;   // Tool knows: only one can be true
else if  (s1) out = b;   // → generates 4:1 one-hot mux
else if  (s2) out = c;   // → ALL inputs evaluated in parallel
else if  (s3) out = d;   // → 1-level mux
end
// Synthesized: 1-level parallel mux. ~250ps. 3× timing improvement.
// Bonus: simulator warns if two selects are high (spec violation caught)
 
// Form C: for truly overlapping signals, use priority if
always_comb begin
out = '0;
priority if (req[3]) out = d;  // highest priority
else if     (req[2]) out = c;
else if     (req[1]) out = b;
else if     (req[0]) out = a;  // lowest priority
end
// Synthesized: priority encoder (4→2 binary) driving mux select
// ~log2(4) = 2 levels. Better than plain if chain.

🚀 RTL Design Insight: Use unique if for All One-Hot and Enum Decodes

The most common use case for unique if in production RTL is decoding FSM state registers, opcode fields, and one-hot encoded control signals. All of these are architecturally mutually exclusive — only one state/opcode is active at a time. Replacing plain if-else if with unique if in these cases gives you: (1) synthesis QoR improvement from parallel mux generation, (2) simulation safety net catching FSM encoding bugs, and (3) lint tool sign-off improvement since most lint rules require unique if or unique case for one-hot decodes.

🔍 Latch Inference from if — Step-by-Step Debugging

Latch inference from an incomplete if in always_comb is one of the most common RTL bugs. With always_comb, the tool catches it immediately (error at compile time). With legacy always @(*), the latch is silently created — often not discovered until post-synthesis simulation fails.

  1. **** — See the tool error: "always_comb block infers latch on signal 'out'". With always @(*), this may be a WARNING only — treat it as an error.
  2. **** — Find every path through the block. Draw a tree: for each if branch and each else if, list what out is assigned to. Include the implicit "no branch taken" path.
  3. **** — Identify the missing path. The latch is inferred on the path where out has no assignment. This is always: a missing else, a missing default, or a signal omitted from a branch.
  4. **** — Fix with default assignment at top: out = '0; as the very first line of the always_comb block. This single line covers ALL paths — latch disappears.
  5. **** — Run regression. The default changes functional behavior for the previously-unhandled path. Any test that relied on the old hold behavior will now fail — which is the correct outcome (the test was hiding the bug). ❌ Three ways to accidentally infer a latch// Bug 1: if without else always_comb begin if (en) out = data; // en=0 → out not assigned → LATCH end // Bug 2: case without default always_comb begin if (s==2'b00) out=a; else if (s==2'b01) out=b; // s=10 or 11 → LATCH end // Bug 3: nested if misses one signal always_comb begin if (mode) begin x = a; if (en) y = b; // mode=1, en=0 → y not assigned → LATCH on y end else begin x = '0; y = '0; end end✅ All three fixed with top-level defaults// Fix 1: default covers all paths always_comb begin out = '0; // ← covers en=0 if (en) out = data; end // Fix 2: default covers s=10,11 always_comb begin out = '0; // ← covers all if (s==2'b00) out=a; else if (s==2'b01) out=b; end // Fix 3: defaults for ALL outputs always_comb begin x = '0; y = '0; // ← covers everything if (mode) begin x = a; if (en) y = b; // en=0: y stays '0 (from default) end end

⚙ Advanced Code Examples — Industry-Grade RTL Patterns

Example A — AXI-Lite Address Decoder (unique if)

SystemVerilog — AXI-Lite Address Decoder using unique if
// ── AXI-Lite slave address decoder ───────────────────────────────
// Address regions are mutually exclusive — unique if is correct
module axi_lite_decoder #(
parameter logic [31:0] CTRL_BASE  = 32'h0000_0000,
parameter logic [31:0] DATA_BASE  = 32'h0001_0000,
parameter logic [31:0] STAT_BASE  = 32'h0002_0000,
parameter logic [31:0] REGION_MSK = 32'hFFFF_0000
) (
input  logic [31:0] awaddr,
output logic        ctrl_sel, data_sel, stat_sel, err_sel
);
always_comb begin
// Default: error — address hits no valid region
{ctrl_sel, data_sel, stat_sel, err_sel} = 4'b0001;
 
unique if ((awaddr & REGION_MSK) == CTRL_BASE) begin
ctrl_sel = 1; err_sel = 0;
end else if ((awaddr & REGION_MSK) == DATA_BASE) begin
data_sel = 1; err_sel = 0;
end else if ((awaddr & REGION_MSK) == STAT_BASE) begin
stat_sel = 1; err_sel = 0;
end
// No else needed — default covers error case
// unique if: simulation warns if two regions decode simultaneously
// (would indicate a parametrization error)
end
endmodule
 
// ── Verification: check decoder with directed test ─────────────────
module tb_decoder;
logic [31:0] awaddr;
logic        ctrl_sel, data_sel, stat_sel, err_sel;
 
axi_lite_decoder u_dut (.awaddr,.ctrl_sel,.data_sel,.stat_sel,.err_sel);
 
initial begin
awaddr = 32'h0000_0004; #1;
assert(ctrl_sel && !err_sel) else $error("CTRL decode fail");
 
awaddr = 32'h0001_0020; #1;
assert(data_sel && !err_sel) else $error("DATA decode fail");
 
awaddr = 32'h0003_0000; #1;  // unknown region
assert(err_sel)             else $error("ERR decode fail");
$finish;
end
endmodule

Example B — Power Management FSM (priority if for overlapping power events)

SystemVerilog — Power Management Controller with priority if
// ── Power state arbiter: multiple power events can occur simultaneously
// Critical system event (thermal) must always win over user request
typedef enum logic [1:0] {
PWR_FULL   = 2'b00,
PWR_REDUCE = 2'b01,
PWR_SLEEP  = 2'b10,
PWR_OFF    = 2'b11
} pwr_state_t;
 
module pwr_ctrl (
input  logic thermal_alert,   // hardware thermal sensor — HIGHEST priority
input  logic battery_low,     // battery monitor
input  logic user_sleep_req,  // software request
input  logic user_wake_req,   // software request
output pwr_state_t pwr_cmd
);
always_comb begin
pwr_cmd = PWR_FULL;         // default: full power
 
priority if (thermal_alert) begin
pwr_cmd = PWR_SLEEP;   // thermal: always overrides everything
end else if (battery_low) begin
pwr_cmd = PWR_REDUCE;  // battery: overrides user, not thermal
end else if (user_sleep_req && !user_wake_req) begin
pwr_cmd = PWR_SLEEP;
end else if (user_wake_req) begin
pwr_cmd = PWR_FULL;
end
// thermal_alert + battery_low + user_sleep all asserted simultaneously:
// priority if → thermal wins, pwr_cmd = SLEEP. No simulation warning.
// This is intentional design — priority if is the correct modifier.
end
endmodule

Example C — Verification Scoreboard using if-else (RTL correctness check)

SystemVerilog — Testbench Scoreboard using if-else for ALU Checking
// ── ALU scoreboard: uses if-else to compute reference model output
module alu_scoreboard;
logic [7:0] a, b, dut_result;
logic [3:0] op;
logic        valid;
int          pass_cnt = 0, fail_cnt = 0;
 
// Reference model: uses unique if — op values are mutually exclusive
function automatic logic [8:0] alu_ref(
input logic [7:0] a, b,
input logic [3:0] op
);
unique if  (op == 4'h0) return {1'b0, a} + {1'b0, b};   // ADD
else if    (op == 4'h1) return {1'b0, a} - {1'b0, b};   // SUB
else if    (op == 4'h2) return {1'b0, a & b};           // AND
else if    (op == 4'h3) return {1'b0, a | b};           // OR
else if    (op == 4'h4) return {1'b0, a ^ b};           // XOR
else if    (op == 4'h5) return {1'b0, ~a};             // NOT
else                    return 9'h000;                  // unhandled
endfunction
 
// Scoreboard: sample DUT output, compare with reference
always @(posedge valid) begin
automatic logic [8:0] expected = alu_ref(a, b, op);
if (dut_result === expected[7:0]) begin
pass_cnt++;
end else begin
$error("FAIL: op=%0h a=%0h b=%0h got=%0h exp=%0h",
op, a, b, dut_result, expected[7:0]);
fail_cnt++;
end
end
endmodule

🔬 Debugging Academy — 8 Real if-else Bugs from the Field

Every one of these bugs has appeared in real RTL projects or code reviews. The symptoms look confusing until you understand exactly how the simulator and synthesis tool interpret the code. 1unique if Flooding Simulation Log with Overlap WarningsSim Warning StormBuggy Code

Bug 1 — Wrong Modifier for Overlapping Conditions
// ❌ BUG: IRQ lines can be simultaneously asserted — unique if is wrong here
always_comb begin
irq_id = 3'b000;
unique if (irq[0]) irq_id = 3'd0;
else if   (irq[1]) irq_id = 3'd1;
else if   (irq[2]) irq_id = 3'd2;
end
// Simulation: 50,000 lines of "unique if overlap violation" in log
// Every clock cycle where 2+ IRQs are pending → warning
// Engineers suppress all warnings → real bugs start getting missed
 
// ✅ FIX: use priority if — overlap is intentional for IRQ arbiters
always_comb begin
irq_id = 3'b000;
priority if (irq[0]) irq_id = 3'd0;  // highest priority
else if     (irq[1]) irq_id = 3'd1;
else if     (irq[2]) irq_id = 3'd2;
end

1Root Cause / Impact / FixRoot Causeunique if asserts that conditions are mutually exclusive. IRQ signals by definition can overlap — multiple interrupt sources fire simultaneously. Using unique if means the simulator issues a warning on every cycle where two or more IRQ bits are set.Real Project ImpactWarning logs fill with thousands of lines per simulation run. Engineers add -suppress flags to silence the warnings. This also suppresses real unique if violations elsewhere — the warning mechanism becomes useless. This is called "warning fatigue" and it is a critical verification quality issue.FixReplace unique if with priority if. The hardware generated is equivalent — first branch still wins. But the simulator no longer warns on overlap (expected behavior). It does still warn if no IRQ is asserted when at least one was expected — the useful check is preserved.2Latch Inferred — Output Holds Stale Value, Passes Directed TestLatch InferenceBuggy Code

Bug 2 — Latch from Incomplete if in always_comb
// ❌ BUG: FIFO read data path — output not assigned when not reading
always_comb begin
if (rd_en && !empty) begin
rd_data = fifo_mem[rd_ptr];
rd_valid = 1'b1;
end
// ❌ rd_data, rd_valid not assigned when !rd_en or empty
// → LATCH inferred on rd_data AND rd_valid
// Directed test: only reads when fifo is ready → passes
// Random test: reads empty fifo → stale rd_data from previous read
end
 
// ✅ FIX: default assignment covers all paths
always_comb begin
rd_data  = '0;         // safe default: don't expose stale data
rd_valid = 1'b0;       // valid=0 when not reading
if (rd_en && !empty) begin
rd_data  = fifo_mem[rd_ptr];
rd_valid = 1'b1;
end
end

2Waveform Symptom / Root CauseWaveform Symptomrd_data holds its last value when rd_en deasserts. In the waveform, rd_data appears to be a register — it holds steady between reads. But it was declared as logic (combinational). The latch is making it appear to have memory. The downstream module reads stale rd_data and processes incorrect data.Why Directed Test PassesThe directed test only ever reads valid data from a non-empty FIFO. It never tests the case where rd_en is deasserted mid-stream. The latch "holds" the correct value in that case, so the test sees the right answer. A constrained-random test that deasserts rd_en unexpectedly — or reads an empty FIFO — exposes the bug immediately.3Priority Wrong — Critical IRQ Silently Missed Due to if OrderWrong PriorityBuggy Code

Bug 3 — Wrong Branch Order Inverts Priority
// ❌ BUG: thermal alert is highest priority, but listed LAST
always_comb begin
pwr_state = PWR_FULL;
priority if (user_req)      pwr_state = PWR_SLEEP;  // low priority, listed first
else if     (battery_low)   pwr_state = PWR_REDUCE;
else if     (thermal_alert) pwr_state = PWR_OFF;    // ❌ critical — listed LAST
end
// When thermal_alert AND user_req are both asserted:
// → user_req (first branch) wins → PWR_SLEEP
// → Chip overheats because thermal_alert is silently ignored
// This is a SAFETY BUG — silicon may be damaged
 
// ✅ FIX: highest priority first
always_comb begin
pwr_state = PWR_FULL;
priority if (thermal_alert) pwr_state = PWR_OFF;    // ✅ critical first
else if     (battery_low)   pwr_state = PWR_REDUCE;
else if     (user_req)      pwr_state = PWR_SLEEP;  // lowest priority last
end

3Debugging ProcessWhy This Is Hard to FindThe code reads logically correct — all conditions are handled. The bug is purely in the ordering. In directed tests that never assert multiple conditions simultaneously, every test passes. Only a test that asserts both user_req and thermal_alert at the same time would reveal the bug. This is a classic case where constrained-random verification — which naturally generates simultaneous assertions — catches what directed tests miss entirely.RuleIn any priority if or if-else if chain: list the highest-priority condition FIRST. Document the intended priority order in a comment at the top of the block. Code review should always verify that priority ordering matches the design specification.4= Used Instead of == in if Condition — Assignment in ConditionSyntax/Logic BugBuggy Code

Bug 4 — Assignment (=) Instead of Comparison (==) in Condition
// ❌ BUG: = instead of == in if condition
always_comb begin
out = '0;
if (state = IDLE)   // ❌ ASSIGNMENT, not comparison!
out = idle_data;  // state is now ALWAYS set to IDLE (1)
else if (state == BUSY)
out = busy_data;  // this branch is NEVER taken (state=IDLE always)
end
// In always_comb: = is a blocking assignment inside the block
// Condition evaluates as the VALUE assigned (IDLE = non-zero = true)
// state gets overwritten to IDLE on every evaluation
// Synthesis: state is driven from multiple sources → multi-driver error
 
// ✅ FIX: use == for comparison
always_comb begin
out = '0;
if (state == IDLE)    // ✅ comparison — state is not modified here
out = idle_data;
else if (state == BUSY)
out = busy_data;
end

5Reset Not First in always_ff — Reset Gated by Other ConditionReset Priority BugBuggy Code

Bug 5 — Reset Buried Inside always_ff — Not Always Effective
// ❌ BUG: reset is NOT the first branch — gated by enable
always_ff @(posedge clk or negedge rst_n) begin
if      (en)     q <= d;      // ❌ en checked first!
else if (!rst_n) q <= '0;   // reset only fires when !en!
end
// If en=1 and rst_n=0 simultaneously: en branch wins → q is NOT reset
// This is a timing-sensitive functional bug
// Synthesis: reset path is conditional — STA may not apply reset timing
 
// ✅ FIX: reset ALWAYS first — unconditional
always_ff @(posedge clk or negedge rst_n) begin
if      (!rst_n) q <= '0;   // ✅ reset first — fires regardless of en
else if (en)     q <= d;
end
 
// Rule: In always_ff with async reset:
// if (!rst_n) is ALWAYS the first, unconditional branch. No exceptions.

6Nested if Creates Unexpected Latch on Inner Output OnlyNested LatchBuggy Code

Bug 6 — Nested if: Outer Default Does Not Cover Inner Paths
// ❌ BUG: partial inner coverage — flag_b gets a latch
always_comb begin
flag_a = 1'b0;        // outer default — covers flag_a everywhere
flag_b = 1'b0;        // outer default — but does it cover inner paths?
if (mode == WRITE) begin
flag_a = 1'b1;
if (burst) flag_b = 1'b1;  // ✅ flag_b assigned when burst=1
// ← flag_b when mode=WRITE, burst=0: NOT re-assigned here
// ← BUT outer default already set flag_b=0 before entering if!
// ← So: flag_b=0 via DEFAULT, not via latch — NO LATCH in this case
end
end
// Actually this is CORRECT — outer default covers inner paths.
// The trap: if you remove the outer default for flag_b:
 
always_comb begin
flag_a = 1'b0;         // ❌ no default for flag_b
if (mode == WRITE) begin
flag_a = 1'b1;
if (burst) flag_b = 1'b1;  // flag_b only assigned in one path
// mode=WRITE, burst=0: flag_b never assigned → LATCH!
// mode=READ: flag_b never assigned → LATCH!
end
end
// FIX: add flag_b = 0 at the very top of the block (see above correct version)

7unique if on X-State Input — Spurious Warnings During ResetX-State / Reset PhaseBuggy Code

Bug 7 — unique if During Reset Phase Produces Spurious X Warnings
// ❌ BUG: state register is X during reset → unique if fires warnings
typedef enum logic [1:0] {IDLE=2'b00, BUSY=2'b01, DONE=2'b10} st_t;
st_t state;
 
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= IDLE; else state <= next_st;
 
always_comb begin
next_st = state;
unique if (state == IDLE) next_st = start ? BUSY : IDLE;
else if  (state == BUSY) next_st = done  ? DONE : BUSY;
else if  (state == DONE) next_st = IDLE;
end
// At T=0 (before reset): state=2'bXX
// unique if evaluates: (XX==00)=X, (XX==01)=X, (XX==10)=X
// None are true → "unique if: no condition true" warning fires
// Log shows hundreds of warnings before simulation even starts
 
// ✅ FIX: add explicit X/default handling
always_comb begin
next_st = IDLE;              // default: safe state if X
unique if (state == IDLE) next_st = start ? BUSY : IDLE;
else if  (state == BUSY) next_st = done  ? DONE : BUSY;
else if  (state == DONE) next_st = IDLE;
else                     next_st = IDLE;  // covers X state
end
// The else branch prevents "no condition true" warning when state=X

8if in always_comb Reading always_ff Output — Wrong Delta Cycle ValueDelta Cycle OrderingBuggy Code

Bug 8 — $display in always_comb Sees Wrong Value Due to Delta Cycles
// ❌ BUG: monitoring combinational logic with $display — wrong values
always_ff @(posedge clk) q <= d;
 
always_comb begin
if (q == 8'hA5) comb_flag = 1'b1;
else             comb_flag = 1'b0;
end
 
// ❌ Bug: monitoring at posedge clk — sees BEFORE or AFTER NBA update?
always @(posedge clk) begin
$display("T=%0t q=%h flag=%b", $time, q, comb_flag);
// PROBLEM: $display fires in Active region BEFORE non-blocking (NBA) updates
// Shows OLD q, OLD comb_flag — looks like a 1-cycle lag in monitoring
end
 
// ✅ FIX: use $strobe — fires in Postponed region AFTER all NBA updates
always @(posedge clk) begin
$strobe("T=%0t q=%h flag=%b", $time, q, comb_flag);
// $strobe: fires after Active→NBA→Delta convergence → shows correct values
end

8The Real LessonKey TakeawayThis bug causes engineers to believe their combinational logic is broken — the waveform shows comb_flag changing one cycle "after" q changes, which looks like a pipeline register was accidentally inserted. In reality, the simulation is correct — the display is just showing the wrong moment. $display fires before NBA updates; $strobe fires after. Always use $strobe for monitoring signals that depend on registered (non-blocking) outputs.

💡 Senior Verification Engineer Tip: Enable Unique/Priority Checks Globally

In VCS, add +define+SV_UNIQUE_PRIORITY_CHECK to ensure unique if and priority if runtime checks are always active. In Questa, checks are enabled by default but can be controlled with -sv_unique and -sv_priority. Many teams accidentally disable these checks with overly broad warning suppress flags — then wonder why their simulations aren't catching conditions the modifiers were meant to catch. Audit your simulation command line: if you see broad warning suppression flags, that is a red flag for your verification quality.

🎯 Interview Q&A — From Fresher to Senior RTL Engineer

Beginner Level

BeginnerWhat hardware does an if-else if chain infer in synthesis?An if-else if chain inside always_comb infers a priority mux chain — a series of cascaded 2:1 muxes. Each condition becomes the select line of one mux. The first true condition's output passes through all subsequent muxes unchanged. The chain is evaluated serially, so a 4-level chain produces 4 mux delays in the critical path. This is why long if-else if chains can hurt timing — each added condition extends the critical path by one mux delay.BeginnerWhat is the difference between unique if and priority if?unique if declares that conditions are mutually exclusive — at most one can be true at any time. The simulator warns if two conditions are simultaneously true (overlap violation) OR if no condition is true (no-match violation). Synthesis generates a parallel mux.priority if declares that conditions may overlap, and the first-matching branch intentionally wins. The simulator only warns if no condition is ever true (no-match violation) — overlap is expected and not warned on. Synthesis generates an optimised priority encoder.Rule of thumb: use unique if for one-hot/enum decodes; use priority if for interrupt arbiters and power management where multiple sources can fire simultaneously. BeginnerWhy must you add a default assignment before if in always_comb?Because always_comb requires every output signal to be assigned on every possible path through the block. If any path through the if-else if chain does not assign the output, the tool infers a latch — memory that holds the last value when no condition matches. With always_comb, the tool errors on this. With legacy always @(*), it silently creates the latch. A default assignment at the very top of the block — out = '0; — covers all paths including any case combination not explicitly handled, eliminating the latch entirely.

Intermediate Level

IntermediateWhy does unique if improve synthesis QoR compared to plain if for one-hot decodes?With plain if-else if, the synthesis tool does not know that only one condition can be true — it must generate a priority chain to handle the (impossible) case of multiple conditions being true simultaneously. This produces N cascaded 2:1 muxes in series, which is the worst-case timing structure. With unique if, the tool receives a guarantee: conditions are mutually exclusive. It can now generate a one-hot parallel mux — all inputs are evaluated simultaneously, and the single active select line routes the output directly. This collapses N mux delays to 1 mux delay — a dramatic timing improvement. For a 4-input decode in 28nm, this can recover 400–600ps of timing budget. IntermediateA simulation log shows thousands of unique if overlap warnings. What is the most likely cause and fix?The most likely cause is unique if used where conditions can legitimately overlap — most commonly an interrupt controller, arbiter, or power management block where multiple request signals can simultaneously assert. The fix is to change unique if to priority if. The hardware behavior is identical (first matching branch wins), but priority if does not warn on overlap — it only warns when no branch is taken, which is the genuinely useful check. Do NOT suppress the warnings without understanding them. Suppressing unique if warnings globally hides real bugs in other parts of the design where unique if is correctly used. IntermediateCan you use unique if inside always_ff? What is the hardware implication?Yes — unique if is valid inside always_ff. The simulation checking behavior (warning on overlap, warning on no-match) still applies. However, the synthesis optimization implication is different: inside always_ff, the output is a register, so the mux generated by unique if drives the D input of the flip-flop. The synthesis tool can still optimize the select logic to be parallel instead of prioritized, reducing the logic depth on the D-input path and improving setup timing. This is particularly useful when decoding one-hot FSM states inside always_ff blocks.

Debugging / Advanced Level

AdvancedYour RTL passes lint but shows unexpected output in post-synthesis simulation. The RTL uses plain if for a one-hot decode. What is the likely failure mode?The most likely failure mode is a simulation-synthesis mismatch from priority chain vs parallel mux optimization. In the RTL simulation, the if-else if chain evaluates serially — if two conditions are simultaneously true (which shouldn't happen with one-hot, but glitches in real timing can cause it), the first one wins. In synthesis, the tool may have optimized the logic using the implicit one-hot assumption (which it infers from don't-care analysis), generating a different structure. The fix: use unique if explicitly for one-hot decodes. This communicates the constraint to both the simulator (which now warns if the one-hot assumption is violated) and the synthesis tool (which now has formal permission to optimize). Lint tools also explicitly check for unique if usage on one-hot signals as a best practice rule.AdvancedExplain how the priority if no-match warning is useful for safety-critical designs.In a safety-critical design — power management, safety controller, interrupt arbiter — there is often a requirement that at least one condition must always be asserted. For example, an interrupt controller should always have at least one pending IRQ when the arbiter is active, or a power management block must always have a valid power command. The priority if no-match warning fires when no branch is taken — meaning none of the conditions are true. In safety-critical RTL, this indicates: (1) an upstream module failed to assert any request, (2) a reset/initialization issue left all signals de-asserted, or (3) a bug in the condition logic. Catching this in simulation prevents the design from entering an undefined state in silicon. For maximum safety, pair priority if with an SVA assertion: assert property (@(posedge clk) !(irq == '0)) to catch the no-request condition formally.SynthesisWhat happens if you use plain if for a 16-level one-hot decode at 1GHz? Walk through the timing impact.A 16-level plain if-else if chain generates 15 cascaded 2:1 muxes (each level adds one 2:1 mux to the chain). In a 28nm technology, a 2:1 mux typically has a propagation delay of 150–200ps. The critical path through 15 muxes is approximately 15 × 175ps = 2,625ps. At 1GHz, the clock period is 1,000ps. Setup time is typically 100ps, leaving approximately 900ps of combinational budget. The 16-way priority chain exceeds this budget by nearly 3× — the design cannot close timing. With unique if, the synthesis tool generates a parallel 16:1 mux. The critical path is approximately 1 mux + decoder delay ≈ 300–400ps — well within the 900ps budget. This is not a hypothetical: wide opcode decodes (16+ opcodes) in processor datapaths and bus decoders fail timing regularly when engineers use plain if-else if. Switching to unique if fixes them with no RTL logic change.