Skip to content

SystemVerilog · Module 4

Bitwise Operators

&, |, ^, ~, ~^ — masking, bit manipulation, XOR parity, X propagation.

Module 4 · Page 4.4

The Workhorses of Signal Manipulation

Bitwise operators are the most physically intuitive operators in hardware description. Every operation maps directly to a gate. AND masks bits. OR sets bits. XOR toggles bits. NOT inverts them. These four primitives are the foundation of everything from register access macros to CRC engines to one-hot encoders.

In verification, you use bitwise operators constantly — extracting protocol fields from a packed bus, injecting a specific error bit into a transaction payload, checking whether a particular flag is set in a status register, computing expected parity. The patterns are mechanical once you understand what each gate does at the bit level.

The operator that gets the most interesting treatment is XOR. At face value it toggles bits. But XOR has a mathematical property — it is its own inverse — that makes it central to parity computation, error detection, and even simple encryption. A signal XORed with itself is always zero. A signal XORed with all-ones inverts every bit. These properties appear regularly in RTL datapath design and in the testbenches that verify them.

The Bitwise Operator Family

OperatorNameGate equivalentPrimary use
&Bitwise ANDAND gate per bit pairMasking — extract or clear specific bits
|Bitwise OROR gate per bit pairSetting — force specific bits to 1
^Bitwise XORXOR gate per bit pairToggling, parity, difference detection
~Bitwise NOTInverter per bitInvert every bit — generate complement
~&Bitwise NANDNAND gate per bit pairUncommon in RTL — used in reduction form
~|Bitwise NORNOR gate per bit pairUncommon in RTL — used in reduction form
~^ or ^~Bitwise XNORXNOR gate per bit pairEquivalence check per bit, comparator design

The Four Core Bit-Manipulation Patterns

Everything you do with bitwise operators in practice reduces to one of these four patterns. Memorise them. They appear in every register access implementation, every protocol driver, every error-injection routine.

MASK — Extract bits

result = data & mask Keeps only the bits where mask is 1. All other bits become 0. Used to isolate a field from a packed bus.

SET — Force bits to 1

result = data | mask Forces all bits where mask is 1 to become 1. Other bits unchanged. Used to enable flags or set control bits.

CLEAR — Force bits to 0

result = data & ~mask Forces all bits where mask is 1 to become 0. Other bits unchanged. Used to disable flags.

TOGGLE — Flip bits

result = data ^ mask Flips all bits where mask is 1. Other bits unchanged. Used for error injection, parity insertion.

Syntax & Truth Tables

SystemVerilog — Bitwise Operator Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] a = 8'hA5;    // 1010_0101
logic [7:0] b = 8'hF0;    // 1111_0000
logic [7:0] result;
 
result = a &  b;   // AND:  1010_0000 = 8'hA0
result = a |  b;   // OR:   1111_0101 = 8'hF5
result = a ^  b;   // XOR:  0101_0101 = 8'h55
result = ~a;       // NOT:  0101_1010 = 8'h5A
result = a ~^ b;   // XNOR: 1010_1010 = 8'hAA  (complement of XOR)
 
// ── Four manipulation patterns ────────────────────────────────────
localparam logic [7:0] MASK = 8'h0F;   // 0000_1111
 
result = a &  MASK;    // EXTRACT lower nibble: 0000_0101 = 8'h05
result = a |  MASK;    // SET lower nibble:     1010_1111 = 8'hAF
result = a & ~MASK;    // CLEAR lower nibble:   1010_0000 = 8'hA0
result = a ^  MASK;    // TOGGLE lower nibble:  1010_1010 = 8'hAA

Per-Bit Truth Table — All Operators

aba & ba | ba ^ b~aa ~^ b
0000011
0101110
1001100
1111001
0X0 ← 0 absorbsXX1X
1XX1 ← 1 absorbsX0X
XXXXXXX
0Z0 ← 0 absorbsXX1X
1ZX1 ← 1 absorbsX0X

Step-by-Step Visual Evaluation

Bit-Level Walkthrough: a = 8'hA5, mask = 8'h0F

OperationBit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0Result (hex)
a101001018'hA5
mask000011118'h0F
a & mask (EXTRACT)000001018'h05
a | mask (SET)101011118'hAF
a & ~mask (CLEAR)101000008'hA0
a ^ mask (TOGGLE)101010108'hAA
~a (INVERT)010110108'h5A

XOR Properties Worth Memorizing

XOR expressionResultWhy this matters
a ^ 0a unchangedXOR with 0 is identity — no effect
a ^ 1~a (inverted)XOR with 1 inverts the bit — useful for toggling
a ^ a0 (all zeros)XOR with itself cancels — used in parity checks and diff detection
a ^ ~aall 1sXOR with complement is always all-ones
(a ^ b) ^ ba (original)XOR is its own inverse — decode by re-applying the key
a ^ 8'hFF~aXOR with all-ones inverts every bit — same as ~ but via XOR

XNOR — Bit-Wise Equivalence

XNOR (~^ or ^~) produces 1 where two bits match, and 0 where they differ. It is the complement of XOR. A common use in RTL is building bit-parallel comparators: if all bits of a ~^ b are 1, then a == b at every bit position. In verification, XOR is more commonly used to find which bits differ between expected and actual — a non-zero XOR result tells you exactly which bits are wrong.

aba ^ b (XOR — differ?)a ~^ b (XNOR — match?)
000 (same)1 (match)
011 (differ)0 (mismatch)
101 (differ)0 (mismatch)
110 (same)1 (match)

Code Examples — From Basics to Production

Example 1 — Beginner: All Operators on Known Values

Example 1 — Bitwise Operator Basics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_bitwise_basic;
 
  logic [7:0] a = 8'hA5;   // 1010_0101
  logic [7:0] b = 8'hF0;   // 1111_0000
 
  initial begin
    $display("a       = %08b  (0x%0h)", a, a);
    $display("b       = %08b  (0x%0h)", b, b);
    $display("────────────────────────────────");
    $display("a & b   = %08b  (0x%0h)", a & b,  a & b);   // AND
    $display("a | b   = %08b  (0x%0h)", a | b,  a | b);   // OR
    $display("a ^ b   = %08b  (0x%0h)", a ^ b,  a ^ b);   // XOR
    $display("~a      = %08b  (0x%0h)", ~a,     ~a);       // NOT
    $display("a ~^ b  = %08b  (0x%0h)", a ~^ b, a ~^ b);  // XNOR
 
    // XOR self-cancels
    $display("a ^ a   = %08b  (should be 0)", a ^ a);
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a       = 10100101  (0xa5)
b       = 11110000  (0xf0)
────────────────────────────────
a & b   = 10100000  (0xa0)
a | b   = 11110101  (0xf5)
a ^ b   = 01010101  (0x55)
~a      = 01011010  (0x5a)
a ~^ b  = 10101010  (0xaa)
a ^ a   = 00000000  (should be 0)

Example 2 — Intermediate: Register Bit Manipulation

This is the production pattern for register field access — the kind of code you write in register model methods, driver register writes, and scoreboard register mirror updates.

Example 2 — Register Bit Manipulation Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_reg_manipulation;
 
  // Imagine a control register layout:
  // [7:6] MODE    [5:4] SPEED    [3] ERR_INT_EN    [2] TX_EN    [1] RX_EN    [0] RST
 
  localparam logic [7:0]
    MASK_MODE     = 8'hC0,   // 1100_0000
    MASK_SPEED    = 8'h30,   // 0011_0000
    MASK_ERR_INT  = 8'h08,   // 0000_1000
    MASK_TX_EN    = 8'h04,   // 0000_0100
    MASK_RX_EN    = 8'h02,   // 0000_0010
    MASK_RST      = 8'h01;   // 0000_0001
 
  logic [7:0] ctrl_reg = 8'h00;
 
  initial begin
 
    // ── SET: enable TX and RX ─────────────────────────────────────
    ctrl_reg = ctrl_reg | MASK_TX_EN | MASK_RX_EN;
    $display("After SET TX+RX: 0x%0h (%08b)", ctrl_reg, ctrl_reg);
    // 0x06 = 0000_0110
 
    // ── CLEAR: disable RX only ───────────────────────────────────
    ctrl_reg = ctrl_reg & ~MASK_RX_EN;
    $display("After CLR RX:    0x%0h (%08b)", ctrl_reg, ctrl_reg);
    // 0x04 = 0000_0100
 
    // ── EXTRACT: read the MODE field ─────────────────────────────
    logic [1:0] mode_val;
    ctrl_reg = 8'hC4;   // set mode bits for demo
    mode_val = (ctrl_reg & MASK_MODE) >> 6;
    $display("MODE field = %0d", mode_val);
    // (0xC4 & 0xC0) >> 6 = 0xC0 >> 6 = 3
 
    // ── TOGGLE: flip error interrupt enable ──────────────────────
    ctrl_reg = ctrl_reg ^ MASK_ERR_INT;
    $display("After TOGGLE ERR_INT: 0x%0h", ctrl_reg);
 
    $finish;
  end
 
endmodule

Example 3 — Verification-Oriented: XOR for Mismatch Diagnosis

XOR between expected and actual output tells you exactly which bits are wrong — every 1 in the XOR result marks a bit that differs. This is more informative than just reporting "mismatch" and is used in production scoreboards to accelerate debug.

Example 3 — XOR-Based Mismatch Diagnosis in Scoreboard
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class DiagnosticScoreboard;
 
  function void check(
    input logic [31:0] expected,
    input logic [31:0] actual,
    input string        tag
  );
    logic [31:0] diff;
 
    if (expected !== actual) begin
      diff = expected ^ actual;   // 1 where bits differ, 0 where they match
      $error("[SB] MISMATCH %s", tag);
      $display("  expected : 0x%08h  (%032b)", expected, expected);
      $display("  actual   : 0x%08h  (%032b)", actual,   actual);
      $display("  XOR diff : 0x%08h  (%032b) ← 1=wrong bit", diff, diff);
      // Count wrong bits
      $display("  %0d bit(s) wrong", $countones(diff));
    end
  endfunction
 
endclass
 
module tb_xor_diag;
  DiagnosticScoreboard sb;
 
  initial begin
    sb = new();
    sb.check(32'hA5A5_A5A5, 32'hA5A5_A5A5, "txn_0");  // PASS
    sb.check(32'hA5A5_A5A5, 32'hA5A5_A5A0, "txn_1");  // 3 bits wrong
    $finish;
  end
endmodule

Example 4 — Corner Case: Error Injection with XOR

A standard error-injection pattern in constrained-random verification: XOR a good payload with an error mask to flip specific bits. Because XOR is its own inverse, you can also use the same operation to "repair" an injected error — useful in error recovery test scenarios.

Example 4 — Error Injection and Recovery via XOR
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ErrorInjector;
 
  // Inject a single-bit error at a random bit position
  function logic [31:0] inject_single_bit(
    input logic [31:0] data,
    input int           bit_pos
  );
    logic [31:0] err_mask = 32'h1 << bit_pos;  // single 1-bit at bit_pos
    return data ^ err_mask;                     // flip that bit
  endfunction
 
  // Inject burst errors across a range of bits
  function logic [31:0] inject_burst(
    input logic [31:0] data,
    input logic [31:0] err_mask
  );
    return data ^ err_mask;   // flip every bit where mask is 1
  endfunction
 
  // Recovery: XOR is its own inverse — re-apply same mask to fix
  function logic [31:0] repair(
    input logic [31:0] corrupted,
    input logic [31:0] err_mask
  );
    return corrupted ^ err_mask;   // XOR same mask again → original data
  endfunction
 
endclass
 
module tb_error_inject;
  ErrorInjector ei;
  logic [31:0] original, corrupted, recovered;
 
  initial begin
    ei        = new();
    original  = 32'hA5A5_A5A5;
    corrupted = ei.inject_single_bit(original, 4);
    recovered = ei.repair(corrupted, 32'h10);   // same mask: 1<<4
 
    $display("Original : 0x%08h", original);
    $display("Corrupted: 0x%08h  (bit 4 flipped)", corrupted);
    $display("Recovered: 0x%08h  (matches original: %0b)",
             recovered, recovered === original);
    $finish;
  end
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Original : 0xa5a5a5a5
Corrupted: 0xa5a5a5b5  (bit 4 flipped)
Recovered: 0xa5a5a5a5  (matches original: 1)

Waveform & Simulation Thinking

X Propagation — AND and OR Special Cases

The X propagation rules for bitwise operators have an important asymmetry. AND can "absorb" X when the other operand is 0. OR can absorb X when the other operand is 1. But this is per-bit — if your mask has some 0 bits and some 1 bits, the 0-bit positions absorb X while the 1-bit positions propagate it.

ExpressionExampleResultWhy
8'hxx & 8'h0FX AND lower-nibble mask8'h0xUpper nibble absorbed to 0, lower nibble propagates X
8'hxx & 8'h00X AND zero mask8'h00ALL bits absorbed — result is clean 0
8'hxx | 8'hFFX OR all-ones mask8'hFFALL bits absorbed — result is clean FF
8'hxx ^ 8'h0FX XOR any mask8'hxxXOR never absorbs — X propagates through all positions
~8'hxxNOT of X8'hxxInversion of unknown is still unknown

Width and Sign Extension in Bitwise Operations

When operands have different widths, the shorter one is extended to match the wider before the operation. Which kind of extension is used is decided by the signedness of the whole expression, not by the operand's own declaration — and that is where this gets dangerous.

The rule: an expression is signed only if every operand is signed. One unsigned operand makes the whole expression unsigned, and then even a signed operand is zero-extended.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic signed [7:0] s = -1;        // 8'hFF
logic        [7:0] u = 8'h0F;     // unsigned
logic signed [15:0] r1, r2;
 
// Both operands signed -> expression is signed -> s SIGN-extends.
r1 = s & 16'shFFFF;               // 16'hFFFF & 16'hFFFF = 16'hFFFF
 
// One operand unsigned -> the WHOLE expression is unsigned ->
// s is ZERO-extended, despite being declared signed.
r2 = s & 16'hFFFF;                // 16'h00FF & 16'hFFFF = 16'h00FF

s is the same variable, declared signed, in both lines. It sign-extends in the first and zero-extends in the second, because of what it was combined with. Nothing in the expression involving s changed.

This is the mechanism behind a whole family of "the mask works on positive values and not on negative ones" reports. It matters most when mixing int (signed 32-bit) with logic [N:0] (unsigned) — a single unsigned literal or port in the expression silently switches the extension for everything in it. $signed() on the offending operand restores the signed context; see Shift Operators, where the same rule decides what >>> fills with.

Width Extension in Bitwise Ops
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0]  byte_val  = 8'hFF;
logic [15:0] word_val  = 16'hFFFF;
 
// byte_val zero-extended to 16 bits before AND
logic [15:0] result = byte_val & word_val;
// = 16'h00FF & 16'hFFFF = 16'h00FF
// Not 16'hFFFF — only lower 8 bits are preserved
 
$display("result = 0x%0h", result);   // 0xff

Synthesis Behavior

OperatorSynthesized toNotes
&AND gate arrayOne gate per bit pair. Fully synthesizable
|OR gate arrayOne gate per bit pair. Fully synthesizable
^XOR gate arrayOne gate per bit pair. Used in parity generators, comparators
~Inverter arrayOne inverter per bit. Fully synthesizable
~^XNOR gate arrayOne XNOR per bit pair. Common in comparator synthesis

Where You'll Use These in Real Projects

Real Verification Usage Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. Extract protocol field from packed bus ─────────────────────
logic [31:0] axi_araddr;
logic [7:0]  page_id   = (axi_araddr >> 12) & 8'hFF;  // bits [19:12]
logic [11:0] page_offset = axi_araddr & 12'hFFF;          // bits [11:0]
 
// ── 2. Check specific flag in status register ─────────────────────
logic [7:0] status;
if (status & 8'h04)                // bit 2 set?
  $display("TX FIFO overflow detected");
 
// ── 3. Build expected register value from fields ──────────────────
function logic [31:0] build_ctrl_reg(
  input logic [1:0] mode,
  input logic        tx_en,
  input logic        rx_en
);
  return ((mode & 2'h3) << 6)
       | (tx_en << 2)
       | (rx_en << 1);
endfunction
 
// ── 4. Even parity generation using XOR (across all bits) ─────────
// Note: reduction ^ covered in 4.5, but multi-input XOR is bitwise
function logic calc_parity(input logic [7:0] data);
  return ^data;   // reduction XOR — counts 1s, 1 if odd
endfunction
 
// ── 5. Error injection — corrupt specific byte in a 32-bit word ──
function logic [31:0] corrupt_byte(
  input logic [31:0] data,
  input int           byte_idx   // 0=LSB, 3=MSB
);
  logic [31:0] err_mask = 32'hFF << (byte_idx * 8);
  return data ^ err_mask;
endfunction

Common Bugs & How to Debug Them

Bug 1 — Wrong Mask: Extracting the Wrong Field

Bug 1 — Off-by-One in Mask Bit Position
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Register: [7:5]=FLAGS  [4:2]=TYPE  [1:0]=STATUS
logic [7:0] reg_val = 8'b101_011_10;   // FLAGS=5, TYPE=3, STATUS=2
 
// BUGGY: wrong mask for TYPE field — mask should cover bits [4:2]
logic [2:0] type_buggy = (reg_val & 8'h0E) >> 1;
// 8'h0E = 0000_1110 — covers bits [3:1], not [4:2]!
// Extracting the WRONG field by one bit
$display("Buggy TYPE = %0d", type_buggy);   // 7 — wrong!
Bug 1 — Fixed: Correct Mask and Shift
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// CORRECT: mask bits [4:2] = 0001_1100 = 8'h1C, shift right by 2
logic [2:0] type_correct = (reg_val & 8'h1C) >> 2;
$display("Correct TYPE = %0d", type_correct);   // 3 — correct
 
// Better: use part-select directly when possible
logic [2:0] type_clean = reg_val[4:2];
$display("Clean TYPE  = %0d", type_clean);     // 3 — cleanest approach

Bug 2 — OR Without Clear: Old Bits Contaminate New Field Value

Bug 2 — Setting a Field Without Clearing First
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// BUGGY: attempting to set SPEED field (bits [5:4]) to value 2'b10
// But the old value still has bits set in that field!
logic [7:0] ctrl = 8'h3F;   // 0011_1111 — SPEED field = 2'b11
 
ctrl = ctrl | (2'b10 << 4);
// 0011_1111 | 0010_0000 = 0011_1111  ← old 2'b11 | new 2'b10 = 2'b11
// OR cannot clear bits — old 1s remain. SPEED is still 2'b11, not 2'b10
$display("Buggy ctrl = 0x%0h", ctrl);   // 0x3f — unchanged
Bug 2 — Fixed: Clear Field First, Then Set
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam logic [7:0] SPEED_MASK = 8'h30;   // 0011_0000
logic [7:0] ctrl = 8'h3F;
 
// CORRECT: clear field first with AND ~mask, then set with OR
ctrl = (ctrl & ~SPEED_MASK)        // clear bits [5:4]
     | ((2'b10 << 4) & SPEED_MASK); // set new value
$display("Fixed ctrl  = 0x%0h", ctrl);
// (0x3F & 0xCF) | 0x20 = 0x0F | 0x20 = 0x2F — SPEED=2'b10 correctly

Bug 3 — XOR to Check Equality: Wrong Interpretation

Bug 3 — Misusing XOR as an Equality Check
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] expected = 8'hA5, actual = 8'hA5;
 
// BUGGY: XOR gives 0 when equal, but 0 in if-condition is FALSE
// Engineer reads "if (a ^ b)" as "if a equals b" — wrong!
if (expected ^ actual)
  $display("Values are equal");    // never prints — 0 is false
else
  $display("Values differ");       // always prints when equal — backwards!
Bug 3 — Fixed: Correct Usage of XOR and ==
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// XOR for DIFFERENCE DETECTION — 0 means equal, nonzero means differ
if (expected ^ actual)
  $display("Values DIFFER: diff=0x%0h", expected ^ actual);
else
  $display("Values EQUAL");    // else branch = XOR was 0 = equal
 
// For direct equality check, use == or ===
if (expected === actual)
  $display("Values EQUAL");

Proving It — Masking, Extension, and X Absorption

The three things that decide a bitwise result are the mask, the extension, and the absorbing element. All three are checkable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// bitwise_semantics_proof.sv
//
// Self-checking proof of: the read-modify-write field update, the
// signedness rule that decides zero- vs sign-extension, and the
// per-bit X absorption that makes masking X-tolerant but XOR not.
module bitwise_semantics_proof;
 
  int errors = 0;
 
  task automatic chk(string name, logic [31:0] got, logic [31:0] exp);
    if (got !== exp) begin
      errors++;
      $display("FAIL %-48s got=%08h exp=%08h", name, got, exp);
    end else
      $display("pass %-48s = %08h", name, got);
  endtask
 
  initial begin
    logic [7:0] reg8, mask;
 
    // ================================================================
    // 1. THE THREE SINGLE-BIT OPERATIONS
    // ================================================================
    reg8 = 8'b1010_0101;
    chk("SET    bit 1", reg8 |  (8'h01 << 1), 8'b1010_0111);
    chk("CLEAR  bit 0", reg8 & ~(8'h01 << 0), 8'b1010_0100);
    chk("TOGGLE bit 7", reg8 ^  (8'h01 << 7), 8'b0010_0101);
 
    // The '!' trap: logical NOT reduces the mask to ONE BIT.
    mask = 8'h01;
    chk("reg & ~mask  (correct, clears bit 0)", reg8 & ~mask, 8'b1010_0100);
    chk("reg & !mask  (WRONG - !mask is 1'b0)", reg8 & !mask, 8'h00);
    // !8'h01 is 1'b0, so the whole register is destroyed. No warning.
 
    // ================================================================
    // 2. FIELD UPDATE - the clear step is not optional
    // ================================================================
    reg8 = 8'b0011_0000;              // bits [5:4] currently 2'b11
    // WRONG: OR alone can only ever set bits.
    chk("OR without clear leaves 2'b11", (reg8 | (2'b10 << 4)) >> 4 & 2'b11, 2'b11);
    // RIGHT: clear the field, then insert.
    chk("clear-then-OR gives 2'b10",
        ((reg8 & ~(2'b11 << 4)) | (2'b10 << 4)) >> 4 & 2'b11, 2'b10);
 
    // ================================================================
    // 3. EXTENSION IS DECIDED BY THE WHOLE EXPRESSION
    //    An expression is signed only if EVERY operand is signed.
    // ================================================================
    begin
      logic signed  [7:0] s = -1;      // 8'hFF
      logic signed [15:0] r_signed, r_mixed;
 
      r_signed = s & 16'shFFFF;        // both signed -> SIGN-extend
      r_mixed  = s & 16'hFFFF;         // one unsigned -> ZERO-extend
 
      chk("s & 16'shFFFF  (all signed -> sign-extend)", r_signed, 16'hFFFF);
      chk("s & 16'hFFFF   (mixed -> ZERO-extend)",      r_mixed,  16'h00FF);
      // Same `s`, declared signed, extended two different ways.
 
      // $signed on the offending operand restores the signed context:
      chk("s & $signed(16'hFFFF)  restores it",
          s & $signed(16'hFFFF), 16'hFFFF);
 
      // And note it is invisible on non-negative data - the reason a
      // positive-only test suite cannot find this:
      begin
        logic signed [7:0] p = 8'sd5;
        chk("positive operand: both forms agree",
            (p & 16'shFFFF), (p & 16'hFFFF));
      end
    end
 
    // ================================================================
    // 4. X ABSORPTION - per bit position
    // ================================================================
    chk("8'h0F & 8'hxx  - AND absorbs 0 in the top nibble",
        8'h0F & 8'hxx, 8'h0x);
    chk("8'hF0 | 8'hxx  - OR absorbs 1 in the top nibble",
        8'hF0 | 8'hxx, 8'hFx);
    chk("8'h0F ^ 8'hxx  - XOR absorbs NOTHING",
        8'h0F ^ 8'hxx, 8'hxx);
    // Masking is X-tolerant. XOR is not, and no mask makes it so.
 
    if (errors == 0) $display("\nbitwise_semantics_proof: ALL CHECKS PASSED");
    else             $display("\nbitwise_semantics_proof: %0d FAILURES", errors);
    $finish;
  end
endmodule

Section 3 is the one to run. s is declared signed and is the same variable on both lines. It sign-extends once and zero-extends once, decided entirely by what it was ANDed with.

A register is ANDed with an inverted field mask to clear the target field, then ORed with the shifted new value to insert it. Skipping the clear step leaves old set bits merged with the new value.reg = 8'b0011_0000field [5:4] holds 2'b11~(2'b11 << 4)inverted field maskAND: field cleared8'b0000_00002'b10 << 4new value, positionedOR: 8'b0010_0000field is 2'b10 — correctSkip the clear:8'b0011_0000old 1 survives — 2'b1112
Figure 1 - the read-modify-write field update, and the step that is easy to omit. Starting from a register whose target field already holds a value, ANDing with the inverted field mask clears just that field and leaves every other bit alone. ORing the shifted new value then inserts it. Skipping the clear means the OR can only add bits, so an old 1 survives and merges with the new value - a write that appears correct the first time and is wrong on every write after it.
1

A logical NOT in a mask expression cleared an entire configuration register

LOGICAL-NOT-COLLAPSED-A-MASK
Buggy Code & Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: one character. `!` where `~` was meant. Both compile.
task automatic clear_field(input logic [31:0] field_mask);
  cfg_reg = cfg_reg & !field_mask;   // ❌ !mask is ONE BIT
endtask
 
// What this computes for field_mask = 32'h0000_00F0:
//   !32'h0000_00F0  ->  1'b0        (nonzero reduces to true, inverted)
//   cfg_reg & 1'b0  ->  32'h0000_0000
//
// Every bit of cfg_reg is cleared, not just the field. And when the mask
// happens to be ZERO:
//   !32'h0000_0000  ->  1'b1
//   cfg_reg & 1'b1  ->  cfg_reg[0]  zero-extended  ->  almost everything
//   cleared too, but not quite - which is why the failure looked random.
 
// ✅ FIX: bitwise NOT preserves the width and inverts each bit.
task automatic clear_field(input logic [31:0] field_mask);
  cfg_reg = cfg_reg & ~field_mask;   // ✅ 32'hFFFF_FF0F - clears only [7:4]
endtask
Symptom

A device driver's field-update routine, exercised through a register-model testbench, produced a configuration register that was mostly zero after any field write. The failure was reported as intermittent, because it depended on which field was written and on what the register happened to hold:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
write field [7:4] = 4'hA   (mask 32'h0000_00F0)
  before: cfg_reg = 32'hDEAD_BEEF
  after:  cfg_reg = 32'h0000_00A0     <-- everything else gone
 
write field [7:4] = 4'h0   (mask 32'h0000_0000, a no-op write)
  before: cfg_reg = 32'hDEAD_BEEF
  after:  cfg_reg = 32'h0000_0001     <-- different kind of wrong

The register model's own read-back check passed, because the model applied the same task.

Root Cause

! is logical NOT. It reduces its operand to a single truth value and returns one bit. ~ is bitwise NOT, which inverts each bit and preserves the width.

For a nonzero mask, !field_mask is 1'b0, and ANDing a 32-bit register with 1'b0 — after the one-bit value is zero-extended to 32 bits — clears everything. For a zero mask, !field_mask is 1'b1, which zero-extends to 32'h0000_0001, so the AND keeps bit 0 and clears the other thirty-one. That is why the two failures above look like different bugs: they are the same bug on the two sides of the reduction.

Three properties made this expensive rather than trivial.

Both operators are legal here. cfg_reg & !field_mask is a well-formed expression producing a 32-bit result. There is no width mismatch to warn about, because a one-bit value zero-extends silently into a 32-bit context. No compiler, and no default lint rule, has grounds to object.

The zero-mask case gave a different signature. Because a zero mask leaves bit 0 alive, the corrupted values differed depending on the field being written, and the first three bug reports were filed as separate issues against three different fields.

The register model shared the routine. The testbench's expected value was computed by calling the same task, so the model and the design agreed and the scoreboard was silent. This is the same failure shape as a reference model sharing a type definition with its DUT: the comparison cannot see an error that is upstream of both sides.

The general rule: ~ computes a value, ! answers a question. If the result of the expression is going to be ANDed, ORed, assigned to a vector, or used as a mask, it must be ~. If it is going into an if or an assertion, it should be !. The width of the operand is the tell — a ! applied to anything wider than one bit deserves a second look.

Fix

Use ~, then make the collapse detectable rather than relying on the character being read correctly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. The fix.
cfg_reg = cfg_reg & ~field_mask;
 
// 2. An elaboration-time width check. A one-bit expression cannot satisfy
//    this, so the logical form fails to elaborate rather than failing in
//    the lab.
generate
  if ($bits(~field_mask) != 32)
    $error("field mask expression is not 32 bits - a logical operator has "
         , "collapsed it");
endgenerate
 
// 3. The invariant that catches any mask bug, not just this one: a field
//    update must leave every bit OUTSIDE the mask unchanged.
property p_only_masked_bits_change;
  @(posedge clk) disable iff (!rst_n)
  field_write |=> ((cfg_reg & ~$past(field_mask)) ==
                   ($past(cfg_reg) & ~$past(field_mask)));
endproperty
a_only_masked_bits_change: assert property (p_only_masked_bits_change)
  else $error("field update modified bits outside the mask");
 
// 4. And the modelling rule that would have made the scoreboard useful:
//    the reference model must compute the expected value INDEPENDENTLY,
//    not by calling the design's own update routine.

Three habits, in increasing order of what they buy:

  • Choose by what the result feeds. ~ for values, ! for questions. A ! on a multi-bit operand is nearly always wrong.
  • Assert the invariant, not the implementation. "Only bits inside the mask changed" is true of every correct field update regardless of how it is written, and it catches ! for ~, a missing clear step, an off-by-one shift, and a mask computed from the wrong parameter — all with one property.
  • Never let the reference model call the design's routine. A model that shares code with what it checks can only verify self-consistency. Whatever else it costs, the expected value has to be computed a second, independent way.

Interview Questions

Best Practices & Coding Guidelines

Named constants for masks

Always define bit masks as named localparam or parameter constants. reg & MASK_TX_EN is readable; reg & 8'h04 requires a comment to explain.

Part-select over masking

For fixed-boundary field extraction, sig[hi:lo] is always clearer than (sig & mask) >> shift. Reserve masking for dynamically computed field positions.

Clear before set

When writing a multi-bit field, always AND with the inverse mask first to clear old bits, then OR the new value. OR alone silently corrupts fields.

XOR = diff, not equality

XOR gives 0 when values are equal and nonzero when they differ. Use it for mismatch diagnosis and error injection — not as a substitute for ==.

TaskCorrect patternCommon mistake
Extract a fieldsig[hi:lo] or (sig & mask) >> shiftWrong mask — off-by-one in position
Set specific bitsreg | maskUsing ^ (toggles, not sets)
Clear specific bitsreg & ~maskUsing reg & mask (keeps, not clears)
Update a fieldClear first: (reg & ~mask) | new_valJust OR: reg | new_val — old bits remain
Find differing bitsa ^ b (nonzero = differ)if (a ^ b) confused with "if equal"
Count wrong bits$countones(a ^ b)Not using XOR — missing diagnostic info

Summary

Bitwise operators map one-to-one to hardware gates and are synthesizable without exception. The four manipulation patterns — mask, set, clear, toggle — cover the vast majority of bit-level work in both RTL and verification.

Three things worth carrying forward:

  • AND has an absorbing element (0). OR has an absorbing element (1). XOR has neither. This determines how X propagates: 0 & X = 0, 1 | X = 1, but any X in an XOR input always propagates to the output. Plan your masking operations accordingly.
  • To update a multi-bit field: clear first, then set. OR cannot clear bits. Using OR alone to write a new field value leaves old bits set wherever the old value had 1s and the new value has 0s.
  • XOR measures bit difference, not equality. a ^ b = 0 means equal. A nonzero result tells you which exact bits differ. Use $countones(a ^ b) for Hamming distance. This is a diagnostic tool, not an equality operator.

The two families these are confused with. Logical Operators covers &&, ||, ! — the reducing forms that answer questions where these compute values. Substituting one for the other is the most common one-character bug in RTL, and Debug Lab 1 above is the ! for ~ version of it. Reduction Operators covers the unary &v, |v, ^v, which use the same symbols with one operand and collapse a vector to a bit. Because the unary forms bind tightest, &a & b is a reduction of a ANDed with b.

What decides the result besides the operator. Signedness governs whether a narrower operand zero- or sign-extends, and one unsigned operand makes the whole expression unsigned — see Integer Types and the same rule applied to fills in Shift Operators. Where these sit relative to equality — a & b == c groups as a & (b == c) — is in Operator Precedence. For building masked fields into wider words, Concatenation, Replication & Conditional. For the X values that make the absorbing-element rule matter, 2-state vs 4-state Types.

References.

  • IEEE 1800 (SystemVerilog) — the binary bitwise operators and their per-bit truth tables, including the X and Z rows, are in the operators-and-expressions clause. Two rules on this page trace to it: an expression is signed only if every operand is signed, so a single unsigned operand causes even a signed operand to zero-extend; and the extension happens before the operation, at the width of the whole expression. $signed() and $unsigned() are the explicit overrides.
  • IEEE 1364 (Verilog) — the same operators and tables, inherited unchanged.

Requirement versus practice. The truth tables, the extension rules, and the X absorption are language requirements. The convention of reserving &/|/~ for values and &&/||/! for conditions is style — both compile — but it is the habit that survives a signal being widened later, which is the change that turns a harmless equivalence into a real bug. The read-modify-write idiom (clear the field, then OR in the value) is likewise practice; the language will happily let you skip the clear.

Part of SystemVerilog Fundamentals·Operators & Expressions·Lesson 22 of 53

View program

Continue learning