SystemVerilog · Module 4
Wildcard Equality — ==? and !=?
X/Z don't-care matching, mask-based pattern matching, comparison with casex/casez.
Module 4 · Page 4.9
Four Equality Operators — One Confusing Family
SystemVerilog has four equality operators, and getting them confused is one of the most common sources of silent simulation bugs. The difference matters most when X or Z values are involved — which is constantly: reset sequences, uninitialized signals, Z-state buses, and partial-drive scenarios all generate X/Z.
| Operator | Name | X/Z in operand | Result type | Primary use |
|---|---|---|---|---|
== | Logical equality | Propagates X → result can be X | 0, 1, or X | General comparisons |
!= | Logical inequality | Propagates X → result can be X | 0, 1, or X | General comparisons |
=== | Case equality | X and Z are literal values — must match exactly | 0 or 1 only | X/Z detection in testbench |
!== | Case inequality | X and Z are literal values | 0 or 1 only | X/Z detection in testbench |
==? | Wildcard equality | X/Z in the right operand = don't-care bit. X/Z in the left operand is a literal value | 0 or 1 only | Pattern/mask matching |
!=? | Wildcard inequality | Same asymmetric rule — right operand supplies the wildcards | 0 or 1 only | Pattern/mask matching |
==? always returns a clean 0 or 1. It does this by treating X and Z in the right operand as don't-care bits that match anything in the left. This makes it ideal for opcode pattern matching, bus mask comparisons, and anywhere you want to classify a signal without caring about specific bit positions.
The Don't-Care Mechanism — Bit by Bit
For each bit position, ==? asks one question: "is the RIGHT operand's bit an X or Z here?" If yes, that position is a wildcard and counts as equal regardless of what the left operand holds — including if the left operand's bit is itself X or Z. If the right operand's bit is known (0 or 1), it falls back to standard equality for that position. Only when all non-masked positions agree does the operator return 1.
The practical use: write a mask pattern on the right operand where X marks the bits you don't care about. cmd ==? 8'b1XXX_XXXX matches any command where bit 7 is 1, regardless of the other seven bits. This is exactly the same matching rule as casex.
==? Wildcard Equality
Returns 1 if every bit position where the right operand is known agrees with the left. X/Z in the right operand masks that position; X/Z in the left is a literal value. Always returns 0 or 1.
!=? Wildcard Inequality
Logical inverse of ==?. Returns 1 when the operands do NOT wildcard-match. Same asymmetric rule — the right operand supplies the wildcards.
vs === (Case Equality)
=== treats X and Z as literal values — X === X is 1. Used to detect unknown states. ==? masks them — used to ignore specific bits.
Syntax and the Bit-Level Rule
// Syntax: value ==? pattern <-- ORDER MATTERS
// Returns: 1-bit - always 0 or 1, NEVER X
// Rule: for each bit - if the RIGHT operand's bit is X or Z, that position
// is a wildcard and matches anything on the left. Otherwise the bits
// must match exactly, with X and Z on the LEFT treated as literal
// values (as `===` does), not as don't-cares.
logic [7:0] cmd = 8'b1010_0011;
// ── Pattern matching: X marks don't-care bits ─────────────────────
cmd ==? 8'b1XXX_XXXX // 1 — bit7=1 matches; all other bits are X (don't-care)
cmd ==? 8'b1010_XXXX // 1 — bits[7:4]=1010 match; lower nibble is X
cmd ==? 8'b1010_0011 // 1 — exact match (no wildcards needed)
cmd ==? 8'b0XXX_XXXX // 0 — bit7=1 in cmd, pattern requires bit7=0
// ── Inequality ───────────────────────────────────────────────────
cmd !=? 8'b1XXX_XXXX // 0 — they wildcard-match, so !=? is 0
cmd !=? 8'b0XXX_XXXX // 1 — they don't match, so !=? is 1
// ── X in the LEFT operand is NOT a wildcard ──────────────────────
// This is where the symmetric intuition fails. The pattern on the
// right is fully known, so nothing is masked, and the X on the left
// is matched literally - exactly as === would match it.
logic [3:0] val = 4'bx101;
val ==? 4'b0101 // 0 - bit3: left X vs right 0 -> literal mismatch
val ==? 4'b1101 // 0 - bit3: left X vs right 1 -> literal mismatch
val ==? 4'b0100 // 0 - bit0 also mismatches on a known bit
val ==? 4'bx101 // 1 - NOW bit3 is a wildcard: it is on the RIGHT
// ── Swapping the operands changes the answer ─────────────────────
4'b0101 ==? val // 1 - val is now the pattern; its X masks bit 3
val ==? 4'b0101 // 0 - val is now the value; its X is literal
// Same two values. Opposite results.
// ── Use in if statement ───────────────────────────────────────────
if (opcode ==? 8'b1010_XXXX)
$display("ALU class opcode");
// ── casex is NOT the same as ==? It is SYMMETRIC: an X in the
// expression `opcode` is also a don't-care and can match the
// wrong item. Prefer casez with `?`, or an if-else on ==?.
casex (opcode)
8'b1010_XXXX: /* ALU */
8'b1100_XXXX: /* LOAD */
default: /* ILLEGAL */
endcaseBit-Level Visual — Exactly What Gets Compared
Complete Equality Operator Truth Table
For a single bit position, comparing left operand bit (L) against right operand bit (R):
| L bit | R bit | == result | === result | ==? result |
|---|---|---|---|---|
| 0 | 0 | 1 | 1 | 1 |
| 0 | 1 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 1 | 1 |
| X | 0 | X | 0 | 1 (don't-care) |
| X | 1 | X | 0 | 1 (don't-care) |
| X | X | X | 1 (exact match) | 1 (don't-care) |
| 0 | X | X | 0 | 1 (don't-care) |
| 1 | X | X | 0 | 1 (don't-care) |
| Z | 0 | X | 0 | 1 (don't-care) |
| Z | Z | X | 1 (exact match) | 1 (don't-care) |
Multi-Bit Pattern Match — Step by Step
cmd = 8'b1010_0011, pattern = 8'b1010_XXXX
| Bit | cmd bit | Pattern bit | ==? for this bit | Reason |
|---|---|---|---|---|
| [7] | 1 | 1 | Match | Both known, equal |
| [6] | 0 | 0 | Match | Both known, equal |
| [5] | 1 | 1 | Match | Both known, equal |
| [4] | 0 | 0 | Match | Both known, equal |
| [3] | 0 | X | Match (don't-care) | Pattern has X → ignore |
| [2] | 0 | X | Match (don't-care) | Pattern has X → ignore |
| [1] | 1 | X | Match (don't-care) | Pattern has X → ignore |
| [0] | 1 | X | Match (don't-care) | Pattern has X → ignore |
| Overall ==? result | 1 — all bits match or are masked |
All Four Operators on the Same Values — Side by Side
| Expression | == | === | ==? | Key takeaway |
|---|---|---|---|---|
4'b1010 == 4'b1010 | 1 | 1 | 1 | All agree on clean match |
4'b1010 == 4'b1011 | 0 | 0 | 0 | All agree on clean mismatch |
4'b1X10 == 4'b1010 | X | 0 | 1 | == propagates X; === rejects; ==? masks |
4'bXXXX == 4'b0000 | X | 0 | 1 | ==? masks all bits — matches anything |
4'bXXXX === 4'bXXXX | X | 1 | 1 | === requires exact X match; ==? masks |
4'b1010 ==? 4'b1X1X | — | — | 1 | Bits 3,1 match; bits 2,0 masked by X in pattern |
4'b1000 ==? 4'b1X1X | — | — | 0 | Bit 1: val=0, pattern=X (masked), bit 2: val=0, pattern=1 → mismatch |
Code Examples — Pattern Matching to Protocol Checking
Example 1 — Beginner: All Equality Operators Compared
module tb_equality_compare;
logic [3:0] clean = 4'b1010;
logic [3:0] dirty = 4'b1X10; // bit2 is X
logic [3:0] pattn = 4'b1X1X; // wildcard pattern: care about bits 3,1 only
initial begin
$display("--- Clean vs clean ---");
$display("1010 == 1010 : %0b", clean == clean); // 1
$display("1010 === 1010 : %0b", clean === clean); // 1
$display("1010 ==? 1010 : %0b", clean ==? clean); // 1
$display("--- Dirty (1X10) vs clean (1010) ---");
$display("1X10 == 1010 : %0b", dirty == clean); // X — X propagates
$display("1X10 === 1010 : %0b", dirty === clean); // 0 — X != 0 literally
$display("1X10 ==? 1010 : %0b", dirty ==? clean); // 1 — X in dirty masks bit2
$display("--- Pattern (1X1X) matching ---");
$display("1010 ==? 1X1X : %0b", clean ==? pattn); // 1 — bit3=1✓ bit1=1✓
$display("1000 ==? 1X1X : %0b", 4'b1000 ==? pattn); // 0 — bit1: 0≠1
$display("1110 ==? 1X1X : %0b", 4'b1110 ==? pattn); // 1 — bit3=1✓ bit1=1✓
$display("--- Detecting X with === ---");
$display("1X10 === 1X10 : %0b", dirty === dirty); // 1 — X matches X exactly
$display("1X10 !== 1010 : %0b", dirty !== clean); // 1 — they are not case-equal
$finish;
end
endmoduleExpected output:
--- Clean vs clean ---
1010 == 1010 : 1
1010 === 1010 : 1
1010 ==? 1010 : 1
--- Dirty (1X10) vs clean (1010) ---
1X10 == 1010 : x
1X10 === 1010 : 0
1X10 ==? 1010 : 1
--- Pattern (1X1X) matching ---
1010 ==? 1X1X : 1
1000 ==? 1X1X : 0
1110 ==? 1X1X : 1
--- Detecting X with === ---
1X10 === 1X10 : 1
1X10 !== 1010 : 1Example 2 — Intermediate: Instruction Class Decoder with ==?
// 8-bit opcode classification by bit pattern:
// ALU: 1010_XXXX (bit7:4 = 1010, lower nibble = don't-care)
// LOAD/STORE: 110X_XXXX (bit7:5 = 110, bit4 = R/W, lower = don't-care)
// BRANCH: 0001_XXXX
// NOP: 0000_0000
module tb_pattern_decoder;
function automatic string classify(input logic [7:0] op);
if (op ==? 8'b1010_XXXX) return "ALU";
else if (op ==? 8'b110X_XXXX) return "LD/ST";
else if (op ==? 8'b0001_XXXX) return "BRANCH";
else if (op ==? 8'b0000_0000) return "NOP";
else return "ILLEGAL";
endfunction
initial begin
$display("0xA0 → %s", classify(8'hA0)); // ALU (1010_0000)
$display("0xAF → %s", classify(8'hAF)); // ALU (1010_1111)
$display("0xC5 → %s", classify(8'hC5)); // LD/ST (1100_0101)
$display("0xD3 → %s", classify(8'hD3)); // LD/ST (1101_0011)
$display("0x17 → %s", classify(8'h17)); // BRANCH(0001_0111)
$display("0x00 → %s", classify(8'h00)); // NOP
$display("0x55 → %s", classify(8'h55)); // ILLEGAL
$finish;
end
endmoduleExpected output:
0xA0 → ALU
0xAF → ALU
0xC5 → LD/ST
0xD3 → LD/ST
0x17 → BRANCH
0x00 → NOP
0x55 → ILLEGALExample 3 — Verification: Scoreboard X Detection vs Pattern Match
module tb_scoreboard_operators;
logic [7:0] dut_out;
logic [7:0] expected;
task automatic check(input logic [7:0] got, exp);
// Step 1: ALWAYS detect X first using === before comparing values
// If DUT output has X bits, that is always a bug — flag it immediately
if (got === 8'hXX || got !=? 8'hXX == 0) // simpler: check for X
if (^got === 1'bX) // reduction XOR: X if any bit is X
$error("DUT output has X bits: %08b", got);
// Step 2: Compare known bits — use === for exact match
if (got !== exp)
$error("MISMATCH: got=0x%02h exp=0x%02h", got, exp);
else
$display("PASS: 0x%02h", got);
endtask
// Protocol checker: verify opcode class using ==?
function automatic void check_opcode_class(input logic [7:0] op);
// Use ==? when you only care about WHICH class the opcode belongs to
if (!(op ==? 8'b1010_XXXX || op ==? 8'b110X_XXXX ||
op ==? 8'b0001_XXXX || op == 8'h00))
$error("Illegal opcode received from DUT: 0x%02h", op);
endfunction
initial begin
// Normal pass
dut_out = 8'hAB; expected = 8'hAB;
check(dut_out, expected); // PASS
// X in output — always an error
dut_out = 8'hXX; expected = 8'hAB;
check(dut_out, expected); // ERROR: X bits detected
// Opcode class check
check_opcode_class(8'hA5); // ALU class: passes
check_opcode_class(8'h55); // ILLEGAL: error
$finish;
end
endmoduleExample 4 — Corner Case: ==? in casex and Priority Matching
module tb_casex_wildcard;
logic [3:0] op;
logic [1:0] class_out;
// casex uses ==? semantics — X in case item = don't-care
always_comb begin
casex (op)
4'b1XXX: class_out = 2'b11; // any op with bit3=1
4'b01XX: class_out = 2'b10; // bit3=0, bit2=1
4'b001X: class_out = 2'b01; // bit3=0, bit2=0, bit1=1
default: class_out = 2'b00;
endcase
end
// Equivalent using ==? chained if-else
function automatic logic [1:0] classify_wc(input logic [3:0] v);
if (v ==? 4'b1XXX) return 2'b11;
else if (v ==? 4'b01XX) return 2'b10;
else if (v ==? 4'b001X) return 2'b01;
else return 2'b00;
endfunction
initial begin
$display("4'b1010 → %02b (expect 11)", classify_wc(4'b1010)); // 11
$display("4'b0110 → %02b (expect 10)", classify_wc(4'b0110)); // 10
$display("4'b0011 → %02b (expect 01)", classify_wc(4'b0011)); // 01
$display("4'b0000 → %02b (expect 00)", classify_wc(4'b0000)); // 00
// Priority trap: 4'b1100 matches BOTH 4'b1XXX and 4'b01XX patterns
// casex/if-else takes the FIRST match — this is priority encoding
// 4'b1100: bit3=1 → matches 4'b1XXX first → class_out = 2'b11
$display("4'b1100 → %02b (expect 11 — first match wins)",
classify_wc(4'b1100)); // 11, NOT 10
$finish;
end
endmoduleProving It — The Asymmetry, and the Three-Operator Matrix
Everything above rests on one claim: that ==? wildcards its right operand and not its left. That claim is directly testable, and the test takes ten seconds.
// wildcard_equality_proof.sv
//
// Self-checking proof that ==? is ASYMMETRIC, plus the canonical
// == / === / ==? matrix on shared operands.
module wildcard_equality_proof;
int errors = 0;
task automatic chk(string name, logic got, logic exp);
if (got !== exp) begin
errors++;
$display("FAIL %-52s got=%b exp=%b", name, got, exp);
end else
$display("pass %-52s = %b", name, got);
endtask
initial begin
logic [3:0] val = 4'bx101; // an X in the DATA
logic [3:0] pat = 4'bx101; // the same bits, used as a PATTERN
// ================================================================
// 1. THE ASYMMETRY - the whole point of the page
// Identical bit patterns. Opposite results. Operand order decides.
// ================================================================
chk("val ==? 4'b0101 (left X is LITERAL -> mismatch)",
val ==? 4'b0101, 1'b0);
chk("4'b0101 ==? pat (right X is WILDCARD -> match)",
4'b0101 ==? pat, 1'b1);
// With no wildcards on the right, ==? degenerates to ===:
chk("4'bxxxx ==? 4'b0000 (right known -> like ===)",
4'bxxxx ==? 4'b0000, 1'b0);
chk("4'b0000 ==? 4'bxxxx (right all wildcards)",
4'b0000 ==? 4'bxxxx, 1'b1);
chk("4'bxxxx ==? 4'b0000 equals 4'bxxxx === 4'b0000",
(4'bxxxx ==? 4'b0000), (4'bxxxx === 4'b0000));
// A wildcard matches ANY left value, including X and Z:
chk("4'bxxxx ==? 4'bxxxx (wildcards match X too)",
4'bxxxx ==? 4'bxxxx, 1'b1);
chk("4'bzzzz ==? 4'bxxxx (wildcards match Z too)",
4'bzzzz ==? 4'bxxxx, 1'b1);
// ================================================================
// 2. THE CANONICAL MATRIX - one pair of operands, three operators
// a = 8'b1010_01xz (X and Z in the DATA)
// b = 8'b1010_01xz (the identical literal, as a PATTERN)
// ================================================================
begin
logic [7:0] a = 8'b1010_01xz;
logic r;
r = (a == 8'b1010_01xz); chk("a == same-bits -> X (propagates)", r, 1'bx);
r = (a === 8'b1010_01xz); chk("a === same-bits -> 1 (literal match)", r, 1'b1);
r = (a ==? 8'b1010_01xz); chk("a ==? same-bits -> 1 (right wildcards)", r, 1'b1);
// Now compare against a fully-KNOWN value. The X/Z are on the left
// only, so ==? has nothing to wildcard and behaves like ===.
r = (a == 8'b1010_0100); chk("a == known -> X", r, 1'bx);
r = (a === 8'b1010_0100); chk("a === known -> 0", r, 1'b0);
r = (a ==? 8'b1010_0100); chk("a ==? known -> 0 (NOT masked!)", r, 1'b0);
// And a pattern that deliberately wildcards the low nibble:
r = (a ==? 8'b1010_xxxx); chk("a ==? 8'b1010_xxxx -> 1 (masked)", r, 1'b1);
end
// ================================================================
// 3. THE CHECKER CONSEQUENCE
// ==? does NOT hide an X in the value under test.
// ================================================================
begin
logic [7:0] dut_out = 8'hxx;
logic [7:0] expected = 8'hA5;
chk("dut_out ==? expected -> 0 (X is caught)",
dut_out ==? expected, 1'b0);
chk("expected ==? dut_out -> 1 (X MASKS - operands reversed!)",
expected ==? dut_out, 1'b1);
// ^ The second line is the bug in Debug Lab 5. Same two signals,
// written the other way round, and the checker stops working.
end
if (errors == 0) $display("\nwildcard_equality_proof: ALL CHECKS PASSED");
else $display("\nwildcard_equality_proof: %0d FAILURES", errors);
$finish;
end
endmoduleThe matrix, as a table
One pair of operands, a = 8'b1010_01xz, compared three ways. This is the mental model worth memorising; everything else on this page follows from it.
| Right operand | a == r | a === r | a ==? r | Why ==? behaves that way |
|---|---|---|---|---|
8'b1010_01xz (identical) | X | 1 | 1 | The right operand's X and Z are wildcards; the rest matches |
8'b1010_0100 (fully known) | X | 0 | 0 | Nothing to wildcard — ==? reduces to ===, and X ≠ 0 |
8'b1010_xxxx (masked nibble) | X | 0 | 1 | Low nibble wildcarded; high nibble matches exactly |
Read the middle row twice. ==? returns 0, not 1, when the value under test carries X and the pattern does not. That is the row the symmetric reading gets wrong, and it is the row that decides whether a checker works.
A scoreboard written with the operands reversed passed on every X the DUT produced
WILDCARD-ON-THE-WRONG-SIDE// ❌ BUG: the operands are the wrong way round. The author believed ==?
// was symmetric, so the order "did not matter".
class packet_scoreboard extends uvm_scoreboard;
function void compare(bit [63:0] got, bit [63:0] exp);
// Reads naturally as "expected matches got". It does not mean that.
if (EXP_MASK ==? got) // ❌ got is the RIGHT operand
`uvm_info("SB", "payload matched", UVM_HIGH)
else
`uvm_error("SB", $sformatf("payload mismatch: got %0h exp %0h", got, exp))
endfunction
endclass
// Every X bit in `got` is now a WILDCARD, because got is on the right.
// A fully-X payload wildcards all 64 positions and matches anything:
// EXP_MASK ==? 64'hxxxx_xxxx_xxxx_xxxx -> 1 (silent pass)
// ✅ FIX: data on the left, pattern on the right. Then an X in the data
// is a literal value and fails to match, as it should.
function void compare(bit [63:0] got, bit [63:0] exp);
if (got ==? EXP_MASK) // ✅ pattern on the right
`uvm_info("SB", "payload matched", UVM_HIGH)
else
`uvm_error("SB", $sformatf("payload mismatch: got %0h exp %0h", got, exp))
endfunctionA payload scoreboard reported clean for six weeks. A silicon bring-up team then found that one DMA descriptor path was returning an all-X payload in simulation whenever a particular back-pressure sequence occurred — and the scoreboard had recorded every one of those transactions as a match.
transaction 41,882
got = 64'hxxxx_xxxx_xxxx_xxxx <-- DUT drove nothing
EXP_MASK = 64'hFFFF_FFFF_0000_0000
scoreboard verdict: MATCHThe scoreboard's own coverage showed the comparison executing four million times with a 100% match rate — which nobody questioned, because a 100% match rate is what a passing scoreboard looks like.
==? is asymmetric, and the operands were reversed.
IEEE 1800 wildcards X and Z in the right operand only. Writing EXP_MASK ==? got puts the DUT's data on the wildcarding side, so every X bit the DUT produces becomes a don't-care that matches whatever the expected value holds. A fully-X payload wildcards all sixty-four positions and matches unconditionally.
Written the other way — got ==? EXP_MASK — the DUT's X bits sit on the left, where they are literal four-state values compared exactly as === compares them. x against 1 is a mismatch, the check fails, and the bug is reported on the first transaction.
Three things made this survive six weeks, and each is worth recognising separately.
The wrong form reads more naturally in English. "Expected matches got" is how the sentence forms in the head, and it produces the operand order that breaks the check. The correct order reads as "got matches the pattern", which is a slightly less natural sentence and the correct comparison.
It only differs on X. For every clean transaction the two orders give identical results, so the check was genuinely working for the 99.9% of traffic that was healthy. It failed exactly on the transactions that mattered.
The failure is a silent pass, and the metric that would reveal it looks like success. A comparison that always matches produces a 100% match rate. There is no error to triage, no mismatch to investigate, and the coverage report says the check ran.
The belief underneath all of it — that ==? treats X in either operand as a don't-care — is widespread enough to be worth stating as a correction rather than a nuance. It is not what the standard says, and the sibling inside chapter has always documented the asymmetry correctly.
Put the data on the left and the pattern on the right, then add the two guards that make the class of error detectable rather than reliant on remembering the rule.
// 1. The fix: data left, pattern right.
if (got ==? EXP_MASK)
// 2. Screen for X explicitly, and report it as its own failure rather
// than folding it into a value mismatch. An all-X payload is a
// different bug from a wrong payload, and deserves a different message.
if ($isunknown(got)) begin
`uvm_error("SB", $sformatf("DUT drove X/Z on payload: %0h", got))
end else if (got ==? EXP_MASK) begin
...
end
// 3. Cover the mismatch outcome. A check whose failing branch has never
// been taken has never been shown to discriminate - whatever the cause.
covergroup cg_compare;
cp_verdict : coverpoint verdict {
bins matched = {1'b1};
bins mismatch = {1'b0}; // zero hits after a full regression means
} // this comparison never said no
endgroupThree habits, and the third is the one that generalises past this operator:
- Data on the left, pattern on the right — always, with
==?and withinside. The set members of aninsideare the right operand of an implied==?and inherit the same asymmetry. - Never put runtime data on the wildcarding side. If the right operand of a
==?is a signal rather than a literal, its X bits are wildcards you did not write. The right operand should be a constant or a parameter that a reader can see. - A check should get stricter when data is unknown, never looser. That single principle rules out
!=for scoreboard comparisons (it returns X and takes theelse), rules outcasexon an expression that can carry X, and rules out this operand order. The same question decides?:againstif/elsefor a mux — see Concatenation, Replication & Conditional. When choosing a comparison operator, ask what it does on an all-X input; if the answer is "passes", choose something else.
Simulation Behavior and Synthesis Notes
==? Always Returns 0 or 1 — Never X
==? never returns X, and the reason is worth stating precisely because it is not "X bits are masked". It is that the comparison is built on case equality: for every bit position, either the right operand is a wildcard (that position matches, unconditionally) or both bits are compared as literal 4-state values, exactly as === compares them. Neither path can produce X, so the result is always a clean 0 or 1.
That makes ==? safe in an if condition, where ordinary == is not — an == that returns X takes the else branch and silently disables a check. But note what it does not buy you: because an X in the left operand is matched literally rather than masked, dut_out ==? PATTERN fails when the DUT drives X. That is the behaviour you want in a checker, and it is the opposite of what the symmetric reading predicts.
==? vs casex/casez — Relation
| Construct | Don't-care character | Where the don't-care may appear | Symmetric? | Notes |
|---|---|---|---|---|
==? | X or Z | Right operand only | No | X/Z on the left is a literal value, matched as === does |
!=? | X or Z | Right operand only | No | Logical inverse of ==?, same asymmetry |
casex | X or Z | Both the case expression and the case items | Yes | Not equivalent to ==? — an X in the expression is also a don't-care |
casez | Z and ? only | Both expression and case items | Yes | X is not a don't-care — stricter than casex, and the safer of the two |
inside {…} | X or Z | Set members only (they are the right operand of an implied ==?) | No | Inherits ==?'s asymmetry — see inside |
=== | none | — | n/a | X and Z are literal values everywhere; the reference point for all of the above |
The row that is most often stated wrongly is casex versus ==?. They are not equivalent. casex is symmetric — an X arriving on the case expression during reset is a don't-care and can match a case item it has nothing to do with. ==? is asymmetric, so the same X on the left is matched literally and simply fails to match. ==? is the safer of the two, and that is the opposite of the usual assumption.
Synthesis
==? with a constant pattern synthesizes to a masked comparator — only the non-X/Z bit positions generate comparison logic. The X/Z positions are literally removed from the comparison tree by the synthesizer. A pattern like 8'b1010_XXXX generates a 4-bit comparator on bits [7:4] only, and the lower nibble produces no gates at all. This is more efficient than writing (op >> 4) == 4'b1010 and is the preferred form for instruction decode logic.
Where You'll Use This in Real Projects
// ── 1. MONITOR: classify received AXI transaction ─────────────────
function automatic string axi_op_type(input logic [7:0] awid);
if (awid ==? 8'b0000_XXXX) return "MGMT";
else if (awid ==? 8'b0001_XXXX) return "DATA";
else if (awid ==? 8'b1XXX_XXXX) return "DMA";
else return "UNKNOWN";
endfunction
// ── 2. SCOREBOARD: detect X in DUT output (use ===, not ==?) ──────
function automatic void assert_no_x(input logic [31:0] data, input string sig);
if (^data === 1'bX) // XOR reduction: 1'bX if any bit is X
$error("%s contains X bits: %08h", sig, data);
endfunction
// ── 3. SVA ASSERTION: pattern-based property ──────────────────────
// assert property (@(posedge clk)
// cmd_valid |-> cmd_opcode ==? 8'b1010_XXXX || cmd_opcode ==? 8'b0001_XXXX);
// ── 4. PROTOCOL CHECKER: bus mask verification ─────────────────────
// AHB: HSIZE must be 3'b000, 3'b001, or 3'b010 — upper bits always 0
function automatic void check_hsize(input logic [2:0] hsize);
if (!(hsize inside {3'b000, 3'b001, 3'b010}))
$error("Illegal HSIZE: 3'b%03b", hsize);
endfunction
// ── 5. COVERAGE: sample by pattern class ──────────────────────────
// covergroup cg_opcode;
// cp_class: coverpoint {
// (opcode ==? 8'b1010_XXXX), // ALU
// (opcode ==? 8'b110X_XXXX) // LD/ST
// }
// endgroup
// ── 6. DRIVER: choose payload based on address pattern ────────────
logic [31:0] addr;
logic [31:0] write_data;
// Peripheral region: addr[31:28] = 4'hA
write_data = (addr ==? 32'hAXXX_XXXX) ? 32'h0000_0001 // reg init value
: 32'hCAFE_BABE; // memory fillBugs Engineers Actually Hit
Bug 1 — Using == Instead of === to Detect X: Silent Pass
logic [7:0] dut_out = 8'hXX; // DUT output corrupted with X
// BUGGY: == with X on left operand → result is X, not 0 or 1
// if (dut_out == 8'hXX) evaluates to X — the if-body never runs!
if (dut_out == 8'hXX)
$error("X detected"); // NEVER FIRES — condition is X, treated as false
// ALSO BUGGY: != doesn't detect X either
if (dut_out != 8'hAB)
$error("Mismatch"); // also X → doesn't fire! Bug passes silently
// CORRECT: use === (case equality) to detect X
if (dut_out === 8'hXX)
$error("X detected"); // FIRES correctly
// CORRECT: use !== (case inequality) for mismatch check
if (dut_out !== 8'hAB)
$error("Mismatch"); // FIRES correctly — X !== AB is true
// BEST PRACTICE: always use !== in scoreboard comparisons
// This catches both value mismatches AND unexpected X/Z valuesBug 2 — ==? Masking an Unexpected X in the Value
logic [7:0] opcode = 8'bXXXX_1010; // upper nibble is X!
// BUGGY: engineer checks if opcode is an ALU instruction
// But upper nibble of opcode is X, not 1010
if (opcode ==? 8'b1010_XXXX)
$display("ALU instruction"); // FIRES — but upper nibble is unknown!
// X in opcode[7:4] + X in pattern[3:0] = ALL bits masked = always matches!
// The engineer doesn't notice the DUT sent garbage in the upper nibble
// CORRECT: first verify no X in the value, THEN pattern-match
if (^opcode === 1'bX) // reduction XOR detects any X bit
$error("Opcode has X bits");
else if (opcode ==? 8'b1010_XXXX)
$display("ALU instruction"); // now safe to pattern-matchBug 3 — casex Overlapping Patterns: Wrong Priority
logic [3:0] op = 4'b1100;
logic [1:0] result;
// BUGGY: both 4'b1XXX and 4'b11XX match op=1100
// casex picks the FIRST matching arm — order matters!
casex (op)
4'b1XXX: result = 2'b01; // matches 4'b1100 FIRST
4'b11XX: result = 2'b10; // never reached for op=1100
default: result = 2'b00;
endcase
// result = 2'b01 — engineer expected 2'b10 for the more specific pattern
// CORRECT: put more specific patterns FIRST
casex (op)
4'b11XX: result = 2'b10; // more specific — check first
4'b1XXX: result = 2'b01; // less specific — fallthrough
default: result = 2'b00;
endcase
// result = 2'b10 — correct
// BETTER: use unique casex to get a warning if patterns overlap
// unique casex (op) — tool reports overlap if any two patterns can both matchBug 4 — Using ==? in Constraint Instead of ==
class txn;
rand logic [7:0] opcode;
// INTENT: constrain opcode to ALU class (upper nibble = 4'b1010)
// BUGGY: ==? is NOT valid in a constraint expression
// Constraint solver works on 2-state values — no X/Z
// Most simulators will error or ignore this
// constraint alu_c { opcode ==? 8'b1010_XXXX; } // ILLEGAL / undefined
// CORRECT option 1: constrain the specific bits directly
constraint alu_c { opcode[7:4] == 4'b1010; }
// Solver picks any opcode where upper nibble is exactly 1010
// CORRECT option 2: use inside with explicit values
// constraint alu_c { opcode inside {[8'hA0:8'hAF]}; }
endclassInterview Questions
Best Practices & Coding Guidelines
Use !== in all scoreboard comparisons
Never use != in a scoreboard. It silently passes X values. !== catches both value mismatches and unexpected X/Z — always.
Check for X before ==? matching
Use ^val === 1'bX (XOR reduction) to detect X bits before applying ==? pattern matching. X in the left operand masks positions silently.
Prefer ==? over casex in testbench
Explicit if-else with ==? is more readable and avoids the casex trap where X in the switched expression causes unintended matches.
More specific patterns first
In casex and ==? chains, always put the most specific (fewest X bits) pattern first. Less specific patterns are fall-through matches.
| Task | Right operator | Wrong operator, and why |
|---|---|---|
| Detect X/Z in a signal | === 'X or ^val === 1'bX | == 'X — returns X, not 1 |
| Scoreboard mismatch check | !== | != — silent on X output |
| Opcode class / bit-pattern match | ==? with X-mask pattern | == — need full bit match, can't mask |
| Exact X/Z position match | === | ==? — masks X, won't detect it |
| Constraint: restrict to bit pattern class | Field slice equality: val[7:4] == 4'b1010 | val ==? 8'b1010_XXXX — not valid in constraints |
Summary
==? and !=? fill a specific gap in SystemVerilog's equality toolkit. They always return 0 or 1, they enable concise bit-pattern matching without cascading bit slices, and they are the mechanism behind both inside and casex. Understanding when to use them — and when === is the right choice instead — is what separates engineers who write solid scoreboards from those who ship bugs that pass silently.
==?always returns 0 or 1, because every bit position either matches unconditionally (right operand is a wildcard) or is compared as a literal 4-state value, as===does. Neither path yields X.==?is asymmetric. X/Z in the right operand is a wildcard; X/Z in the left is a literal value.a ==? bandb ==? aare different comparisons. Put the pattern on the right, always.- Use
===to detect X, use==?to ignore specific bits. These are opposite intents and opposite tools. - Every scoreboard comparison should use
!==. It catches X values in DUT output.!=lets them through silently — arguably the most common verification correctness bug. - Check for X before pattern-matching with
==?. A fully-X value matches any pattern. Validate cleanliness first with reduction XOR. - More specific patterns first in
casexchains. Priority is top-to-bottom — a broad wildcard first will shadow every specific pattern below it.
Related Pages & References
The rest of the equality family. Relational & Equality Operators owns ==, !=, ===, !== and the relational operators — including why != returning X silently disables a scoreboard check. This page owns only ==? and !=?, and the matrix above is the bridge between them.
The constructs that look like ==? and are not. case, casex & casez — casex is symmetric where ==? is asymmetric, which makes casex the more dangerous of the two, not the equivalent. inside compares each set member with an implied ==?, with the member as the right operand, so it inherits this page's asymmetry exactly.
Where the X values come from. 2-state vs 4-state Types for what X and Z mean and where they originate; Logical Operators for why an X condition takes the else branch, which is the mechanism behind the != scoreboard trap. For where ==? sits among the other operators — at equality level, above the bitwise operators — see Operator Precedence.
References.
- IEEE 1800 (SystemVerilog) — the wildcard equality operators are defined in the operators-and-expressions clause. The definition is explicit and is the basis for every correction on this page: X and Z values in a given bit position of the right operand are treated as wildcards, and a wildcard bit matches any value — 0, 1, X or Z — in the corresponding bit of the left operand. Nothing in the definition wildcards the left operand. The
casexandcasezdon't-care rules, which are symmetric across the case expression and the case items, are in the case-statement clause. - IEEE 1364 (Verilog) —
casexandcasezoriginate here;==?and!=?were introduced in SystemVerilog.
Requirement versus practice. The asymmetry, the never-X result, and the casex/casez don't-care rules are language requirements. The conventions built on them — data on the left and pattern on the right, patterns as literals rather than signals, and the principle that a check should get stricter rather than looser on unknown data — are engineering practice. They are not optional in the sense that violating them produces the silent-pass failure in Debug Lab 5, but the language will compile either order without complaint.
Part of SystemVerilog Fundamentals·Operators & Expressions·Lesson 27 of 53
View program