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).
| Operator | Name | Returns X? | Synthesizable | Use in |
|---|---|---|---|---|
< | Less than | Yes — if operand has X/Z | ✅ Yes | RTL, testbench ordered comparisons |
> | Greater than | Yes — if operand has X/Z | ✅ Yes | RTL, testbench ordered comparisons |
<= | Less than or equal | Yes — if operand has X/Z | ✅ Yes | RTL, testbench ordered comparisons |
>= | Greater than or equal | Yes — if operand has X/Z | ✅ Yes | RTL, testbench ordered comparisons |
== | Logical equality | Yes — returns X if either operand has X/Z | ✅ Yes | RTL; testbench only with clean values |
!= | Logical inequality | Yes — returns X if either operand has X/Z | ✅ Yes | RTL; avoid in scoreboard mismatch checks |
=== | Case equality | Never — always returns 0 or 1 | ❌ No | Testbench X/Z detection, assertions |
!== | Case inequality | Never — always returns 0 or 1 | ❌ No | Scoreboard 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
// ── 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
| a | b | a == b | a != b |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 0 |
| 0 | X | X | X |
| 1 | X | X | X |
| X | X | X | X |
| 0 | Z | X | X |
| 1 | Z | X | X |
Truth Table — Case Equality (===) per Bit
| a | b | a === b | a !== b |
|---|---|---|---|
| 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 |
| 1 | 1 | 1 | 0 |
| X | X | 1 ← X matches X | 0 |
| Z | Z | 1 ← Z matches Z | 0 |
| 0 | X | 0 | 1 |
| 1 | X | 0 | 1 |
| X | Z | 0 | 1 |
| 0 | Z | 0 | 1 |
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 !==:
| Scenario | expected | got (DUT) | expected != got | if fires? | expected !== got | if fires? |
|---|---|---|---|---|---|---|
| Clean match | 8'hA5 | 8'hA5 | 0 | No | 0 | No |
| Clean mismatch | 8'hA5 | 8'h00 | 1 | Yes ✅ | 1 | Yes ✅ |
| DUT output is X | 8'hA5 | 8'hxx | X | No ❌ FALSE PASS | 1 | Yes ✅ CORRECT |
| DUT output is Z | 8'hA5 | 8'hzz | X | No ❌ FALSE PASS | 1 | Yes ✅ CORRECT |
| Partial X (some bits) | 8'hA5 | 8'hxA | X | No ❌ FALSE PASS | 1 | Yes ✅ 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 defaultint,byte,shortintare signed by defaultlogic signed [N:0]explicitly marks a vector as signed
| Comparison | Types | Effective operation | Result |
|---|---|---|---|
8'hF0 > 8'h0F | Both unsigned 8-bit | 240 > 15 | 1 (TRUE) |
8'shF0 > 8'sh0F | Both signed 8-bit | -16 > 15 | 0 (FALSE) |
logic[7:0] vs int | Unsigned vs signed — result is unsigned | int is treated as unsigned | Negative int becomes huge unsigned |
8'hFF >= 8'hFF | Both unsigned | 255 >= 255 | 1 (TRUE) |
Code Examples — From Basics to Production
Example 1 — Beginner: All Operators with Clean Values
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
endmoduleExpected output:
a < b : 1
a > b : 0
a <= c : 1
a >= b : 0
a == c : 1
a != b : 1
a === c : 1
a !== b : 1Example 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.
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
endmoduleExpected output:
[CLEAN MATCH] == 1 !== 0
[CLEAN MISMATCH] == 0 !== 1
[DUT IS X] == x !== 1
[DUT IS Z] == x !== 1
[BOTH X] == x === 1Example 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.
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
endmoduleExpected 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=1Example 4 — Tricky Corner Case: 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
endmoduleExpected 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.
| Expression | Returns | Use when |
|---|---|---|
$isunknown(sig) | 1 if any bit is X or Z | Checking any unknown bit — most common X-check |
sig === 'x | 1 only if ALL bits are X | Checking if fully undriven — stricter |
sig === 'z | 1 only if ALL bits are Z | Checking tristate/bus release |
sig !== expected | 1 if any bit differs (including X/Z vs 0/1) | Scoreboard mismatch — catches everything |
Synthesis Implications
| Operator | Synthesizable | What synthesis tools do |
|---|---|---|
==, != | ✅ Yes | Maps to comparator gates. X/Z behavior does not exist in silicon |
<, >, <=, >= | ✅ Yes | Maps to subtractor/comparator logic |
===, !== | ❌ No | Synthesis tools either error or silently drop these. RTL with === will not build correctly |
Where You'll Use These in Real Projects
// ── 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();
endfunctionCommon 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.
// 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// 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
// 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.// CORRECT RTL — == is synthesizable
always_ff @(posedge clk) begin
if (data_in == 8'hFF) // == is synthesizable, maps to comparator
state <= DONE;
endBug 3 — Signed vs Unsigned Comparison: Silent Wrong Behavior
// 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// 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
| Context | Use | Avoid | Reason |
|---|---|---|---|
| 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_zzzz | sig == 32'hzzzz_zzzz | == returns X when comparing against Z |
| Assertion guard (disable iff) | !$isunknown(sig) | Nothing — always add this | Prevents false assertion failures during X states in sim |
| Loop/counter comparison | i < N with int | Mixed signed/unsigned | Unsigned 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 anifand 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.