SystemVerilog · Module 5
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.
// ── 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
endFigure 1 — if-else if-else Infers a Priority Mux Chain
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 addresses all three with decision modifiers: unique if, unique0 if, and priority if. The same modifiers apply to case — see case, casex and casez for the statement form and its own set of traps.
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)
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.
// ── 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 inside always_ff: use unique0, or plain if ─────────────
// ✗ WRONG. Whenever rst_n is high and en is low - the normal idle
// condition of most flops - NO branch is taken, and unique if
// reports a no-match violation. Every cycle. In every test.
// unique if (!rst_n) q <= '0;
// else if (en) q <= d;
//
// ✓ unique0 checks exclusivity but does NOT require a branch to be
// taken, which is exactly the semantics an incomplete chain wants.
always_ff @(posedge clk) begin
unique0 if (!rst_n) q <= '0;
else if (en) q <= d;
end
// ✓ plain if is also perfectly correct here: the priority is
// intentional and there is nothing to check.unique if: the overlap violation and the no-match violation
6 cyclesWith plain if, both violation windows above are silent and the bugs reach silicon undetected. With unique if they surface in regression. Note that in the overlap window the hardware is identical either way — the first matching branch wins in both cases. The modifier buys visibility, not different behaviour.
⚠ When NOT to Use unique if — The X-State Trap
If any signal driving a unique if condition is X — which is the normal state at time zero before reset, and during X-propagation — no comparison evaluates to true, so the chain reports a no-match violation on every evaluation. The design is functionally correct; the reports describe the reset window.
Note what does not work here. These are violation reports on a procedural statement, not concurrent assertions, so there is no disable iff to attach and $assertoff is not defined by IEEE 1800 to control them — some tools extend their assertion controls to cover them and some do not, so anything built on that is tool-specific and quietly non-portable. The portable fixes are structural:
- Give the chain a final
else. No-match becomes unreachable, the uniqueness check is untouched, and the X case lands in a defined state instead of holding. - Use
unique0 if. It checks that at most one condition is true and drops the requirement that any branch be taken — the right modifier whenever the conditions are genuinely exclusive but deliberately incomplete. This is usually the cleanest answer. - Use
priority ifif the conditions can overlap anyway, which keeps the no-match check while dropping the overlap check.
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.
// ── 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)
// unique0 if: • Warns on overlap, but NOT on no-match
// • For conditions that are exclusive but deliberately
// incomplete — the usual fix for reset-window X warnings🚀 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 reports a violation on every clock cycle where multiple IRQs are pending, which is the controller's normal operating mode rather than a fault. The same reasoning drives arbiter design generally; see AXI arbitration for how fixed priority compares with round-robin when starvation matters.
Choosing: plain, unique, or priority?
Figure 3 — Which if Modifier Should You Use?
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.
| Keyword | Conditions can overlap? | Sim warns on overlap? | Sim warns on no-match? | Hardware inferred |
|---|---|---|---|---|
if | Yes (silent) | No | No | Priority mux chain |
unique if | No — must be exclusive | Yes ✅ | Yes ✅ | Parallel mux (optimised) |
unique0 if | No — must be exclusive | Yes ✅ | No | Parallel mux (optimised) |
priority if | Yes — intentional | No | Yes ✅ | Optimised priority encoder |
Real-World Examples
// ════ 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
// See /learnings/systemverilog/procedural-blocks for always_ff vs always_comb
// ════ 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.
// ── 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
// ════ 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
endQuick Reference — if-else Cheat Sheet
// ── 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.
Priority chain: the first-listed asserted request always wins
8 cyclesAt 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.
// ── 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.
| Form | Synthesized Structure | Logic Levels | Timing Impact | Area Impact |
|---|---|---|---|---|
if-else if (plain, N conditions) | N cascaded 2:1 muxes in series (priority chain) | N levels | Worst — critical path grows linearly with N | N × mux cells |
unique if (N mutually exclusive conditions) | Parallel N:1 mux or one-hot decoder + mux | 1–2 levels | Best — all inputs evaluated simultaneously | Similar or less than priority chain |
priority if (N overlapping conditions) | Priority encoder + mux (optimised, not naive chain) | log₂(N) levels | Better than plain if — encoder is optimised | Priority encoder + single mux |
// ── 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.
- **** — 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. - **** — Find every path through the block. Draw a tree: for each
ifbranch and eachelse if, list whatoutis assigned to. Include the implicit "no branch taken" path. - **** — Identify the missing path. The latch is inferred on the path where
outhas no assignment. This is always: a missingelse, a missingdefault, or a signal omitted from a branch. - **** — 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. - **** — 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)
// ── 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
endmoduleExample B — Power Management FSM (priority if for overlapping power events)
// ── 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
endmoduleExample C — Verification Scoreboard using if-else (RTL correctness check)
// ── 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.
unique if flooding the simulation log with overlap warnings
SIM WARNING STORM// ❌ 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;
endunique if asserts that the conditions are mutually exclusive. IRQ signals by
definition can overlap — multiple interrupt sources fire simultaneously, and that
is the controller's normal operating mode, not a fault. So the simulator issues a
violation report on every cycle where two or more IRQ bits are set.
Warning logs fill with thousands of lines per run. Engineers add suppression
flags to silence them, and those flags also suppress genuine unique if
violations elsewhere in the design — so the mechanism that was supposed to catch
one-hot bugs stops reporting them. This is warning fatigue, and it is a
verification-quality problem rather than a cosmetic one: the team ends up worse
off than if the modifier had never been used.
Replace unique if with priority if. The generated hardware is equivalent —
the first matching branch still wins — but the simulator no longer reports
overlap, which was expected behaviour all along. The genuinely useful check
survives: priority if still reports when no branch is taken, which for an
interrupt controller means the arbiter ran with no pending request.
Latch inferred — output holds a stale value and the directed test still passes
LATCH INFERENCE// ❌ 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
endrd_data holds its last value when rd_en deasserts. On the waveform it looks
like a register — steady between reads — even though it was declared logic and
lives in an always_comb block. The inferred latch is what gives it apparent
memory, and the downstream module consumes that stale data as if it were fresh.
The directed test only ever reads valid data from a non-empty FIFO. It never
deasserts rd_en mid-stream and never reads an empty FIFO, so the one value the
latch is holding happens to be the correct one on every cycle the test checks. A
constrained-random test that deasserts rd_en at an arbitrary point, or reads an
empty FIFO, exposes it on the first attempt.
Assign every output unconditionally at the top of the block, before any if.
A default of '0 for the data and 1'b0 for the valid flag both removes the
latch and makes the failure mode safe: a consumer that ignores rd_valid now
sees zeros rather than a plausible-looking stale payload.
Priority inverted — a critical condition silently loses to a lower-priority one
WRONG 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
endThe code reads as logically correct: every condition is handled and nothing is
missing. The defect is entirely in the ordering, which no lint rule and no
review checklist catches unless the reviewer knows the intended priority. In
directed tests that never assert two conditions at once, every test passes. Only
a stimulus that raises user_req and thermal_alert in the same cycle exposes
it — which is exactly the kind of coincidence constrained-random generates
naturally and directed tests almost never do.
In any priority if or if-else if chain, list the highest-priority condition
first, and state the intended order in a comment at the top of the block so a
reviewer can check the code against an explicit specification rather than against
their own assumption. For a safety-relevant chain, back it with an assertion:
assert property (@(posedge clk) thermal_alert |-> pwr_state == PWR_OFF) fails
the moment the ordering regresses, which a comment cannot do.
Assignment (=) written instead of comparison (==) in an if condition
SYNTAX/LOGIC BUG// ❌ 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;
endWriting = inside an if condition in an always_comb block is a blocking
assignment, not a comparison. The condition then evaluates to the value
assigned — IDLE, which is non-zero and therefore true — so the first branch is
taken unconditionally and state is overwritten on every evaluation of the
block. The later state == BUSY branch becomes unreachable. Synthesis usually
catches this as a multi-driver error on state, because the block is now driving
a signal that a sequential block also drives, but simulation runs happily and
produces a design that always reports IDLE.
Use ==. The broader defence is that this is one of the few bugs a linter
reliably catches — an assignment inside a condition is a standard rule in every
lint ruleset, so the real lesson is that the rule should be enabled and its
warnings treated as errors rather than triaged.
Reset is not the first branch, so an enable can gate the reset
RESET PRIORITY BUG// ❌ 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.With en tested first, the reset branch is only reachable when en is low. If
en is high while rst_n falls, the flop takes d instead of resetting. That is
a functional bug on its own, but the synthesis consequence is worse: the reset is
no longer an unconditional asynchronous path, so the tool may not recognise it as
a reset at all. It then infers ordinary logic on the asynchronous input, and
static timing analysis never applies reset recovery and removal checks to it.
In an always_ff with an asynchronous reset, if (!rst_n) is the first branch,
unconditionally, with no exceptions. Every synthesis tool pattern-matches this
shape to infer the reset flop; anything else is a gamble on the tool's inference.
A nested if leaves only the inner output latched
NESTED LATCH// ❌ 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)The trap is that the outer default is easy to read as covering the whole block
when it only covers the signals it actually names. In the first version both
flag_a and flag_b get defaults, so the inner if (burst) is safe and no latch
appears — the code is correct. Remove flag_b's default and only the inner path
assigns it, so mode == WRITE with burst low, and every value of mode other
than WRITE, leave it unassigned and a latch is inferred on flag_b alone.
flag_a remains clean, which is what makes the bug confusing: half the block
behaves and half does not.
Give every output a default at the very top of the block, before any conditional
— not inside the outermost if, and not per-branch. always_comb will report
the latch, but only for signals it can prove are incompletely assigned, so the
habit is worth more than the tool check.
unique if reporting violations during reset because the state is X
X-STATE / RESET PHASE// ❌ 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=XBefore reset takes effect, state is 2'bXX. Each comparison against an enum
value evaluates to X rather than to true or false, so no branch is selected and
unique if reports "no condition is true" on every evaluation. The reports are
technically accurate and completely useless — they describe the reset window, not
a design fault, and they arrive in the hundreds before the simulation reaches any
interesting behaviour.
Note that disable iff is not available here: this is a violation report on a
procedural statement, not a concurrent assertion, so there is no clocking or
disable construct to attach.
Give the chain a final else so that some branch is always taken. That resolves
the no-match report while leaving the uniqueness check intact for the cases that
matter, and it also makes the X behaviour explicit — an unknown state now lands
in a defined recovery state instead of holding whatever it held.
unique0 if is the other correct answer, and often the better one: it checks
that at most one condition is true and does not require that any branch be
taken, which is precisely the semantics wanted here. Reach for it whenever the
conditions are genuinely exclusive but incomplete.
A monitor in the wrong scheduling region prints a value that lags by a cycle
DELTA CYCLE ORDERING// ❌ 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$display executes in the Active region, and the non-blocking assignment to q
updates in the NBA region, which runs later in the same time step. So the
$display in an @(posedge clk) block prints the value q held before the
edge, along with the comb_flag derived from it. Nothing is wrong with the
design; the monitor is sampling at the wrong point in the time step.
This one costs disproportionate time because it makes correct logic look broken.
The printed trace shows comb_flag changing a cycle after q, which reads
exactly like an accidental pipeline register, so engineers start hunting for a
flop that does not exist.
$strobe schedules in the Postponed region, after all NBA updates for that time
step have settled, so it prints the values the design actually converged on. Use
it for any monitor that observes signals derived from non-blocking assignments.
A clocking block in the testbench solves the same problem structurally, which is
why UVM environments rarely hit this at all — see
race conditions and determinism for the
scheduling model this rests on.
💡 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
An if-else if chain inside always_comb infers a priority mux chain — a series of cascaded 2:1 muxes, with each condition becoming the select line of one mux. The first true condition's output then passes through all the later muxes unchanged. Because the chain is structurally serial, a four-level chain puts four mux delays in the critical path, which is why long if-else if chains hurt timing: every condition you add extends the path by roughly one mux delay. That cost is invisible in RTL simulation and shows up only in synthesis reports, so it tends to be discovered late.
unique if declares that the conditions are mutually exclusive — at most one can be true at a time. The simulator reports a violation if two conditions are simultaneously true (an overlap violation) or if no condition is true and there is no final else (a no-match violation). Synthesis is free to build a parallel mux.
priority if declares that conditions may overlap and that the first matching branch intentionally wins. The simulator reports only the no-match case; overlap is expected and goes unreported. Synthesis builds an optimised priority encoder.
The rule of thumb: unique if for one-hot and enum decodes, where overlap really would be a bug; priority if for interrupt arbiters, power-management chains and anything else where several sources can legitimately fire at once. Choosing unique for the second class is the single most common misuse, and it is the subject of Debug Lab 1.
Because always_comb requires every output to be assigned on every path through the block, and an if-else if chain without a final else leaves at least one path where nothing is assigned. On that path the signal must hold its previous value, which is memory — so the tool infers a latch.
A single default assignment at the very top of the block, before any conditional, covers every path including the combinations you did not think to handle. always_comb will report the incomplete assignment where it can prove one, but the default is worth writing regardless: it also decides what the safe value is, which the latch never does. Legacy always @(*) gives no error at all and simply creates the latch silently, which is why so much older RTL carries them. See procedural blocks for the inference rules in full.
With plain if-else if, the tool does not know that only one condition can be true, so it must generate a priority chain that would still behave correctly if several were true simultaneously. That is N cascaded 2:1 muxes in series — the worst-case timing structure available.
unique if supplies the missing guarantee. The tool can now build a one-hot parallel mux: all inputs are evaluated at once and the single active select routes the result directly, collapsing N mux delays to roughly one. For a wide decode this is a large recovery — wide opcode decodes and bus decoders regularly fail timing on the priority-chain form and close on the parallel form with no change to the logic itself.
Two caveats worth stating in an interview. The guarantee is a promise you are making, not something the tool verifies at elaboration — if the conditions can overlap in silicon, you have told the tool something false and the hardware may not match simulation. And modern tools often infer the parallelism anyway from don't-care analysis, so the more dependable value of unique is the simulation check rather than the QoR.
Almost certainly unique if used where the conditions legitimately overlap — an interrupt controller, an arbiter, or a power-management block where several request signals can assert at once. The fix is priority if: the hardware is identical, the first matching branch still wins, and overlap is no longer reported while the useful no-match check is retained.
The thing not to do is suppress the warnings. A global suppression also hides genuine unique if violations everywhere else in the design, converting a working check into a silent one — and the places where unique is used correctly are exactly the places where a violation would have been worth knowing about. Fix the modifier at the source rather than filtering its output.
Yes, it is legal, and the violation checks apply exactly as they do in always_comb. The synthesis effect is that the mux unique permits now drives the D input of the flop, so the benefit is reduced logic depth on the D path and better setup timing — useful when decoding one-hot FSM states.
The thing to watch for is the no-match check, and it catches people out constantly. Consider a reset-and-enable flop written as unique if (!rst_n) q <= '0; else if (en) q <= d;. Whenever rst_n is high and en is low — which is the normal idle condition of most flops in most designs — no branch is taken and the simulator reports a violation. The chain is not wrong as hardware; it is simply incomplete, and unique demands completeness as well as exclusivity.
There are three correct responses. Add a final else if a default action exists. Use unique0 if, which checks exclusivity but does not require a branch to be taken, and is the right modifier whenever the conditions are exclusive but deliberately incomplete. Or use plain if, which is entirely appropriate for a reset-enable flop where the priority is intentional and there is nothing to check.
A simulation-synthesis mismatch caused by the tool optimising on an assumption the RTL never stated. In RTL simulation the if-else if chain evaluates serially, so if two conditions are ever simultaneously true — which should not happen for a genuine one-hot, but can if the one-hot generator is buggy or if the signal is glitching through a real timing path — the first one wins deterministically. Synthesis, meanwhile, may have inferred the one-hot property from don't-care analysis and built a structure that does something different in that same situation.
The fix is to state the constraint explicitly with unique if, which addresses both halves at once: the simulator now reports if the one-hot assumption is ever violated, and the tool has explicit permission to optimise rather than an inference it made on its own. The deeper point for an interview is that the mismatch was never really about if — it was about an assumption held by the tool and not by the source, and making assumptions explicit is what these modifiers are for.
A 16-level if-else if chain generates fifteen cascaded 2:1 muxes. Taking a 2:1 mux at roughly 150–200 ps in a 28 nm library, the chain contributes on the order of 2.5 ns. At 1 GHz the period is 1 ns, and after setup and clock uncertainty there is perhaps 900 ps of combinational budget — so the priority chain overruns the budget by nearly three times and the path cannot close.
With unique if the tool builds a parallel 16:1 mux: one mux delay plus the decode, in the region of 300–400 ps, comfortably inside budget. This is a routine finding rather than a hypothetical — wide opcode decodes in processor datapaths and address decoders in bus fabrics fail timing on the chain form regularly, and switching the modifier fixes them without touching the logic.
The number to carry into an interview is the shape rather than the picoseconds: a priority chain is O(N) in logic depth and a parallel mux is O(1), so the gap widens with every condition you add. That is also why the fix is worth the most on exactly the decodes that are hardest to restructure by hand.
Where This Is Specified
- IEEE 1800-2023 (SystemVerilog) §12.4.2 —
unique-if,unique0-if, andpriority-if. The decision-modifier semantics, and the two classes of violation report: more than one condition true, and no condition true with no finalelse.unique0relaxes only the second. - IEEE 1800-2023 §12.4 — Conditional if-else statement. Evaluation order and the priority behaviour of an unmodified chain.
- IEEE 1800-2023 §9.2.2.2 —
always_comb. The complete-assignment requirement whose violation infers a latch. - IEEE 1800-2023 §4.9 and §10.4.2 — Scheduling semantics and nonblocking assignment. The Active/NBA/Postponed ordering that Debug Lab 8 rests on, and why
$strobeobserves the settled values that$displaycannot.
Part of SystemVerilog Fundamentals·Procedural Statements·Lesson 30 of 53
View program