Skip to content

Relational & Equality Operators

== vs === with X/Z values, the scoreboard false-pass bug, signed vs unsigned.

Module 4 · Page 4.2

The Operator That Causes Silent Failures

If you could only learn one thing from this entire operators chapter, it would be this: do not use != in your scoreboard's mismatch check. Use !== instead. The difference is one character. The consequence of getting it wrong is a false pass — a test that reports "PASS" while the DUT is actually outputting X on every cycle.

Here is exactly why. The != operator is a 4-state operator. When either operand contains X or Z, it returns X — not 1 and not 0. In a SystemVerilog if statement, X is treated as false. So if (expected != got) where got is X evaluates as false — the mismatch branch never executes — and your test reports clean.

The !== operator (case inequality) is 2-state. It performs a bit-exact comparison including X and Z bits, and always returns 0 or 1 — never X. If got is X and expected is a clean value, expected !== got returns 1 — the mismatch branch fires.

Two Families: Relational and Equality

SystemVerilog comparison operators split into two groups. Relational operators establish order — is A less than B, greater than or equal to B? They produce a result that depends on the numerical relationship. Equality operators check identity — are A and B the same? The equality group has two sub-families: logical equality (4-state, can return X) and case equality (2-state, always returns 0 or 1).

OperatorNameReturns X?SynthesizableUse in
<Less thanYes — if operand has X/Z✅ YesRTL, testbench ordered comparisons
>Greater thanYes — if operand has X/Z✅ YesRTL, testbench ordered comparisons
<=Less than or equalYes — if operand has X/Z✅ YesRTL, testbench ordered comparisons
>=Greater than or equalYes — if operand has X/Z✅ YesRTL, testbench ordered comparisons
==Logical equalityYes — returns X if either operand has X/Z✅ YesRTL; testbench only with clean values
!=Logical inequalityYes — returns X if either operand has X/Z✅ YesRTL; avoid in scoreboard mismatch checks
===Case equalityNever — always returns 0 or 1❌ NoTestbench X/Z detection, assertions
!==Case inequalityNever — always returns 0 or 1❌ NoScoreboard mismatch checks — use this

How == Handles X and Z — The Core Concept

The == operator models real hardware behavior. In hardware, comparing a known value against an unknown signal genuinely cannot produce a definitive answer — the result is unknown. So X on either input propagates to the output as X.

The === operator is a simulator construct with no hardware equivalent. It asks: "are these two bit patterns identical, including any X or Z bits?" It treats X as a specific distinguishable bit value rather than "unknown." This is useful for testbench checking but has no meaning in synthesis.

Syntax & Truth Tables

SystemVerilog — All Comparison Operators
// ── Relational operators ─────────────────────────────────────────
result = (a <  b);    // less than
result = (a >  b);    // greater than
result = (a <= b);    // less than or equal
result = (a >= b);    // greater than or equal
 
// ── Logical equality (4-state — can return X) ────────────────────
result = (a == b);    // logical equal     — returns X if a or b has X/Z
result = (a != b);    // logical not-equal  — returns X if a or b has X/Z
 
// ── Case equality (2-state — always returns 0 or 1) ─────────────
result = (a === b);   // case equal     — exact bit match, X matches X, Z matches Z
result = (a !== b);   // case not-equal — any bit difference, including X vs 0
 
// ── Common patterns ──────────────────────────────────────────────
if (dut_out !== expected)   // scoreboard check — catches X in dut_out
$error("Mismatch!");
 
if (signal === 1'bx)        // explicit X detection
$warning("X on signal");
 
if ($isunknown(bus))         // system function — any bit X or Z
$error("X/Z on bus");

Truth Table — Logical Equality (==) per Bit

aba == ba != b
0010
0101
1001
1110
0XXX
1XXX
XXXX
0ZXX
1ZXX

Truth Table — Case Equality (===) per Bit

aba === ba !== b
0010
0101
1110
XX1 ← X matches X0
ZZ1 ← Z matches Z0
0X01
1X01
XZ01
0Z01

Step-by-Step Visual Evaluation

The Scoreboard Scenario — What the Simulator Actually Does

Walk through exactly what happens when a DUT output is X and your scoreboard uses != vs !==:

Scenarioexpectedgot (DUT)expected != gotif fires?expected !== gotif fires?
Clean match8'hA58'hA50No0No
Clean mismatch8'hA58'h001Yes ✅1Yes ✅
DUT output is X8'hA58'hxxXNo ❌ FALSE PASS1Yes ✅ CORRECT
DUT output is Z8'hA58'hzzXNo ❌ FALSE PASS1Yes ✅ CORRECT
Partial X (some bits)8'hA58'hxAXNo ❌ FALSE PASS1Yes ✅ CORRECT

Signed vs Unsigned Comparison — The Width and Sign Rules

When comparing two values, the simulator first determines the expression's sign context and bit width. The rules (IEEE 1800-2017 §11.6.1):

  • If either operand is unsigned, the entire comparison is unsigned
  • Both operands are zero-extended (unsigned) or sign-extended (signed) to the width of the wider operand
  • logic [N:0] is unsigned by default
  • int, byte, shortint are signed by default
  • logic signed [N:0] explicitly marks a vector as signed
ComparisonTypesEffective operationResult
8'hF0 > 8'h0FBoth unsigned 8-bit240 > 151 (TRUE)
8'shF0 > 8'sh0FBoth signed 8-bit-16 > 150 (FALSE)
logic[7:0] vs intUnsigned vs signed — result is unsignedint is treated as unsignedNegative int becomes huge unsigned
8'hFF >= 8'hFFBoth unsigned255 >= 2551 (TRUE)

Code Examples — From Basics to Production

Example 1 — Beginner: All Operators with Clean Values

Example 1 — Basic Comparison Operators
module tb_comparison_basic;
 
int a = 10, b = 20, c = 10;
 
initial begin
 
// ── Relational operators ──────────────────────────────────────
$display("a < b   : %0b", a <  b);    // 1
$display("a > b   : %0b", a >  b);    // 0
$display("a <= c  : %0b", a <= c);    // 1  (equal counts)
$display("a >= b  : %0b", a >= b);    // 0
 
// ── Logical equality — clean values, == and === agree ─────────
$display("a == c  : %0b", a == c);    // 1
$display("a != b  : %0b", a != b);    // 1
 
// ── Case equality — same result here, but NEVER returns X ─────
$display("a === c : %0b", a === c);   // 1
$display("a !== b : %0b", a !== b);   // 1
 
$finish;
end
 
endmodule

Expected output:

Simulation Output
a < b   : 1
a > b   : 0
a <= c  : 1
a >= b  : 0
a == c  : 1
a != b  : 1
a === c : 1
a !== b : 1

Example 2 — Intermediate: == vs === with X Values

This is the most important example in this entire section. Run it yourself to see the exact X behavior — particularly how == returns x and === returns a deterministic 0 or 1.

Example 2 — == vs === with X/Z Values
module tb_equality_x;
 
logic [7:0] dut_out;
logic [7:0] expected = 8'hA5;
 
initial begin
 
// ── Case 1: clean, matching ───────────────────────────────────
dut_out = 8'hA5;
$display("[CLEAN MATCH]    == %b   !== %b",
dut_out == expected, dut_out !== expected);
// == 1,  !== 0  — both agree: it's a match
 
// ── Case 2: clean, NOT matching ──────────────────────────────
dut_out = 8'h00;
$display("[CLEAN MISMATCH] == %b   !== %b",
dut_out == expected, dut_out !== expected);
// == 0,  !== 1  — both agree: it's a mismatch
 
// ── Case 3: DUT output is all X ──────────────────────────────
dut_out = 8'hxx;
$display("[DUT IS X]       == %b   !== %b",
dut_out == expected, dut_out !== expected);
// == x,  !== 1  ← CRITICAL: == returns x (false in if)
//                            !== returns 1 (mismatch detected)
 
// ── Case 4: DUT output is Z (tristate/undriven) ──────────────
dut_out = 8'hzz;
$display("[DUT IS Z]       == %b   !== %b",
dut_out == expected, dut_out !== expected);
// == x,  !== 1  — same behavior as X
 
// ── Case 5: both same X pattern ──────────────────────────────
dut_out  = 8'hxx;
expected = 8'hxx;
$display("[BOTH X]         == %b   === %b",
dut_out == expected, dut_out === expected);
// == x,  === 1  — case equality: X matches X exactly
 
$finish;
end
 
endmodule

Expected output:

Simulation Output
[CLEAN MATCH]    == 1   !== 0
[CLEAN MISMATCH] == 0   !== 1
[DUT IS X]       == x   !== 1
[DUT IS Z]       == x   !== 1
[BOTH X]         == x   === 1

Example 3 — Verification-Oriented: Production Scoreboard

This is the safe, production pattern. It uses !== for mismatch detection and $isunknown() to distinguish X/Z failures from value mismatches — two different root causes that require different debug actions.

Example 3 — Scoreboard with Correct !== Mismatch Check
class DataScoreboard;
 
int pass_cnt = 0;
int fail_cnt = 0;
int xz_cnt   = 0;
 
// Use !== so that X/Z in dut_out is always caught
function void check(
input logic [31:0] expected,
input logic [31:0] dut_out,
input string        tag = ""
);
if (expected !== dut_out) begin
if ($isunknown(dut_out)) begin
// X or Z on DUT output — different root cause than a value error
xz_cnt++;
$error("[SB] X/Z DETECTED %s | exp=0x%08h got=%b",
tag, expected, dut_out);
end else begin
fail_cnt++;
$error("[SB] MISMATCH     %s | exp=0x%08h got=0x%08h",
tag, expected, dut_out);
end
end else begin
pass_cnt++;
end
endfunction
 
function void report();
$display("[SB] Results: PASS=%0d  FAIL=%0d  X/Z=%0d",
pass_cnt, fail_cnt, xz_cnt);
if (fail_cnt + xz_cnt == 0)
$display("[SB] ALL CHECKS PASSED");
endfunction
 
endclass
 
module tb_scoreboard;
 
DataScoreboard sb;
 
initial begin
sb = new();
 
sb.check(32'hA5A5_A5A5, 32'hA5A5_A5A5, "txn_0");   // PASS
sb.check(32'hA5A5_A5A5, 32'h0000_0000, "txn_1");   // FAIL
sb.check(32'hA5A5_A5A5, 32'hxxxx_xxxx, "txn_2");   // X detected
 
sb.report();
$finish;
end
 
endmodule

Expected output:

Simulation Output
[SB] MISMATCH     txn_1 | exp=0xa5a5a5a5 got=0x00000000
[SB] X/Z DETECTED txn_2 | exp=0xa5a5a5a5 got=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[SB] Results: PASS=1  FAIL=1  X/Z=1

Example 4 — Tricky Corner Case: Signed vs Unsigned Comparison

Example 4 — Signed vs Unsigned Comparison
module tb_signed_comparison;
 
logic        [7:0] u_val = 8'hF0;   // unsigned: 240
logic signed [7:0] s_val = 8'hF0;   // signed:   -16  (same bit pattern!)
 
initial begin
// Same bits — different numeric meaning
$display("u_val = %0d  (0x%0h)", u_val, u_val);   // 240
$display("s_val = %0d  (0x%0h)", s_val, s_val);   // -16
 
// Unsigned comparison: 240 > 127 → TRUE
if (u_val > 8'h7F)
$display("u_val > 0x7F: TRUE");
 
// Signed comparison: -16 > 127 → FALSE
if (s_val > 8'sh7F)
$display("s_val > 0x7F: TRUE");
else
$display("s_val > 0x7F: FALSE  (signed: -16 is not > 127)");
 
// Mixed comparison — unsigned wins, signed treated as unsigned
// u_val (unsigned) vs integer (signed) → comparison is unsigned
int threshold = -1;
if (u_val < threshold)
// threshold (-1) as unsigned 32-bit = 4294967295
// 240 < 4294967295 → TRUE — unexpected!
$display("u_val < -1: TRUE  ← mixed sign trap!");
 
$finish;
end
 
endmodule

Expected output:

Simulation Output
u_val = 240  (0xf0)
s_val = -16  (0xf0)
u_val > 0x7F: TRUE
s_val > 0x7F: FALSE  (signed: -16 is not > 127)
u_val < -1: TRUE  ← mixed sign trap!

Waveform & Simulation Thinking

X Propagation in Comparisons

Logical equality (==, !=) follows the same X-propagation rules as all 4-state arithmetic. A single X bit in either operand is enough to produce X on the output. In waveforms, you will see the comparison signal go to X rather than 0 or 1.

Case equality (===, !==) performs a bitwise exact match including X and Z. The result is always a clean 0 or 1. In waveforms, the signal never goes to X — making it far more useful for driving if conditions in testbenches.

The $isunknown() System Function

For production scoreboard code, $isunknown(expr) is the most readable way to check whether any bit of a signal is X or Z. It returns 1 if any bit is X or Z, 0 otherwise. It is equivalent to (expr === 'x || expr === 'z) on a multi-bit level but handles partial X patterns (e.g., 8'hxA) correctly.

ExpressionReturnsUse when
$isunknown(sig)1 if any bit is X or ZChecking any unknown bit — most common X-check
sig === 'x1 only if ALL bits are XChecking if fully undriven — stricter
sig === 'z1 only if ALL bits are ZChecking tristate/bus release
sig !== expected1 if any bit differs (including X/Z vs 0/1)Scoreboard mismatch — catches everything

Synthesis Implications

OperatorSynthesizableWhat synthesis tools do
==, !=✅ YesMaps to comparator gates. X/Z behavior does not exist in silicon
<, >, <=, >=✅ YesMaps to subtractor/comparator logic
===, !==❌ NoSynthesis tools either error or silently drop these. RTL with === will not build correctly

Where You'll Use These in Real Projects

Real Verification Usage Patterns
// ── 1. Scoreboard mismatch check — always use !== ─────────────────
function void compare(logic [31:0] exp, logic [31:0] got);
if (exp !== got)   // !== catches X/Z, != does not
$error("MISMATCH exp=%0h got=%0h", exp, got);
endfunction
 
// ── 2. X-check assertion — fires if DUT output goes X after reset ─
assert property (@(posedge clk) disable iff (!rst_n)
!$isunknown(dut_data_out))
else $error("X detected on data_out at time %0t", $time);
 
// ── 3. Reset check — all registers should be 0 after reset ───────
task automatic check_reset();
@(negedge rst_n);
@(posedge rst_n);
if (dut_status_reg !== 32'h0)
$error("Reset check FAIL: status_reg = 0x%08h", dut_status_reg);
else
$display("Reset check PASS");
endtask
 
// ── 4. Bus arbitration — check for tristate release ───────────────
task automatic wait_bus_release();
int timeout = 100;
while (bus_data !== 32'hzzzz_zzzz && timeout > 0) begin
@(posedge clk);
timeout--;
end
if (timeout == 0)
$error("Timeout: bus not released (still driving)");
endtask
 
// ── 5. Coverage guard — skip sample if signal has unknowns ────────
function void sample(logic [7:0] op_code);
if (!$isunknown(op_code))   // don't sample X into coverage bins
cov_op.sample();
endfunction

Common Bugs & How to Debug Them

Bug 1 — The False-Pass Scoreboard: != Instead of !==

This is the most consequential bug in this chapter. The test appears to pass while the DUT is actively broken. No error is reported. The regression log is clean. The bug ships.

Bug 1 — Buggy: False Pass When DUT Output is X
// BUGGY scoreboard — copied from C++ habit
function void check_buggy(logic [31:0] exp, logic [31:0] got);
if (exp != got)                // != returns X when got is X
$error("Mismatch!");           // X in if-condition → FALSE → never fires
// Result: DUT can output X on every transaction and this never reports.
endfunction
 
// Demonstration
logic [31:0] expected = 32'hA5A5_A5A5;
logic [31:0] dut_out  = 32'hxxxx_xxxx;  // DUT is completely broken
 
check_buggy(expected, dut_out);
// Output: (nothing) — $error never fires — test passes — BUG SHIPS
Bug 1 — Fixed: Use !== for Scoreboard Checks
// CORRECT scoreboard — !== always returns 0 or 1
function void check_correct(logic [31:0] exp, logic [31:0] got);
if (exp !== got)               // !== returns 1 when got is X → fires
$error("Mismatch! exp=0x%08h got=%b", exp, got);
endfunction
 
check_correct(expected, dut_out);
// Output: ERROR: Mismatch! exp=0xa5a5a5a5 got=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// Bug is caught. Test fails correctly.

Bug 2 — === in RTL: Non-Synthesizable Check

Bug 2 — === Accidentally Left in RTL
// BUGGY RTL — === is a simulation-only operator
always_ff @(posedge clk) begin
if (data_in === 8'hFF)    // === left here from debug session
state <= DONE;
end
// Synthesis may warn, replace with ==, or behave unexpectedly
// Simulation passes. Silicon may not match simulation behavior.
Bug 2 — Fixed: Use == in RTL
// CORRECT RTL — == is synthesizable
always_ff @(posedge clk) begin
if (data_in == 8'hFF)     // == is synthesizable, maps to comparator
state <= DONE;
end

Bug 3 — Signed vs Unsigned Comparison: Silent Wrong Behavior

Bug 3 — Mixed Sign Comparison: Negative Treated as Huge Unsigned
// BUGGY — mixed sign comparison
logic [7:0] addr_offset;   // unsigned — DUT address output
int          limit = -1;    // programmer uses -1 as sentinel: "no limit"
 
// Bug: addr_offset is unsigned, so comparison becomes unsigned
// limit (-1 as int) = 0xFFFFFFFF unsigned = 4294967295
// addr_offset (any 8-bit value) < 4294967295 → ALWAYS TRUE
if (addr_offset < limit)
$display("offset within limit");   // Always fires — guard is useless
Bug 3 — Fixed: Match Types or Use Explicit Guard
// FIX 1 — Use int for offset so both are signed
int addr_offset;   // signed — comparison context is now signed
int limit = -1;
if (limit < 0 || addr_offset < limit)  // explicit sentinel guard
$display("offset within limit");
 
// FIX 2 — Explicit cast to force signed context
if ($signed(addr_offset) < limit)
$display("offset within limit");

Interview Questions

Beginner Level

Q1: What is the difference between == and === in SystemVerilog?== is logical equality — a 4-state operator that returns X if either operand contains X or Z. === is case equality — a 2-state operator that always returns 0 or 1 by comparing bits exactly, treating X as a specific bit value that matches another X. In hardware, == is synthesizable and models real gates. === is simulation-only with no hardware equivalent. Q2: What does 4'bxxxx == 4'bxxxx evaluate to? It evaluates to X. With ==, any X or Z in either operand causes the result to be X — even if both operands are identical X patterns. However, 4'bxxxx === 4'bxxxx evaluates to 1 — case equality matches X bits exactly.

Intermediate Level

Q3: Your scoreboard always reports PASS even when the DUT is outputting X. What is the bug and how do you fix it? The mismatch check uses != instead of !==. When the DUT outputs X, expected != dut_out returns X. In a SystemVerilog if statement, X evaluates as false, so the $error call never executes and the test appears to pass. Fix: replace != with !== in all scoreboard comparison checks. !== always returns 0 or 1 and correctly returns 1 when the DUT output is X. Q4: Is === synthesizable? What happens if you use it in an always_ff block? No. === is not synthesizable — it has no hardware equivalent because silicon cannot distinguish between X (unknown) and a specific bit value. If used in an always_ff block, different synthesis tools behave differently: some replace it with == with a warning, others error out, and some silently produce incorrect gate-level netlists. The result is a simulation-synthesis mismatch — the most dangerous class of bug in ASIC design.

Experienced Engineer Level

Q5: A logic [7:0] signal is compared against an int variable containing -1. Describe the comparison behavior.logic [7:0] is unsigned. int is signed 32-bit. When these are compared, since one operand is unsigned, the entire comparison is performed as unsigned. The int value -1 (0xFFFFFFFF) is treated as an unsigned 32-bit value = 4,294,967,295. The logic [7:0] value is zero-extended to 32 bits. Any 8-bit unsigned value (0–255) will be less than 4,294,967,295, so the comparison logic[7:0] < int(-1) is always true. Fix by using $signed() or matching types explicitly. Q6: In an SVA assertion, when would you use == vs ===? Use == for protocol checks where the DUT signal is expected to always be a valid 0 or 1 — a valid-data comparison. Use === when you specifically want to detect or verify X/Z states — for example, asserting that a tristate bus is in the high-Z state (bus === 32'hzzzz_zzzz) or that a signal has been driven X (signal === 1'bx after a specific sequence). The most common pattern is using !$isunknown(sig) as a disabling condition so assertions don't fire during X states in simulation.

Best Practices & Coding Guidelines

  • Scoreboard: Always !== — Replace every != in a scoreboard comparison with !==. No exceptions. One character change eliminates an entire class of false-pass bugs.
  • RTL: Never === — Keep === and !== entirely out of synthesizable RTL files. If synthesis gives a warning about them, treat it as an error.
  • X Detection: $isunknown() — Prefer $isunknown(sig) over sig === 'x for X checking. It catches partial X (individual bits being X) which === 'x does not.
  • Match Types in Comparisons — When comparing logic[N:0] signals against integer variables, ensure both are the same signedness. Mixed comparisons silently produce unsigned behavior.

Operator Selection Reference

ContextUseAvoidReason
RTL equality check==, !====, !==Only ==/!= are synthesizable
Scoreboard mismatch!==!=!= produces false pass when DUT has X
Detecting X on a signal$isunknown()== 'x== 'x itself returns X — never true
Detecting tristate (Z)sig === 32'hzzzz_zzzzsig == 32'hzzzz_zzzz== returns X when comparing against Z
Assertion guard (disable iff)!$isunknown(sig)Nothing — always add thisPrevents false assertion failures during X states in sim
Loop/counter comparisoni < N with intMixed signed/unsignedUnsigned loop variables with signed limits produce wrong results

Summary

Relational operators are straightforward once you know the signed/unsigned context rules. The complexity in this section lives entirely in the equality operators — specifically the behavioral gap between == and === when X or Z values are present.

The three rules to internalize:

  • In your scoreboard, use !==, not !=. != returns X when the DUT output has X, which evaluates as false in an if and silently masks failures.
  • In RTL, never use === or !==. They are simulation-only. Using them in synthesizable code creates simulation-synthesis mismatches — among the hardest bugs to trace.
  • Mixed signed/unsigned comparisons are silent bugs. If either operand is unsigned, the whole comparison is unsigned. A negative signed value becomes a large unsigned value, and your guard conditions stop working.

These are not academic rules. They are patterns that appear in real production testbench code, produce real false passes, and cause real silicon respins. Getting them right is part of what separates a professional verification engineer from someone who just writes code that looks correct.