Skip to content

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
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
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
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
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
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
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
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 operand is extended to match the wider one before the bitwise operation. For unsigned types, zero-extension is used. For signed types, sign extension is used. This matters most when mixing int (signed 32-bit) with logic [N:0] (unsigned) in bitwise operations.

Width Extension in Bitwise Ops
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
// ── 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
// 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
// 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
// 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
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
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 ==
// 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");

Interview Questions

Beginner Level

Q1: How do you clear bit 3 of an 8-bit register without affecting any other bits? Use AND with the complement of a mask: reg = reg & ~8'h08. The mask 8'h08 has only bit 3 set. Inverting it with ~ gives 8'hF7 (all bits 1 except bit 3). ANDing with this forces bit 3 to 0 while leaving all other bits at their original values. Q2: What is the result of XORing any value with itself? Always zero. Every bit XORed with itself gives 0 (0^0=0, 1^1=0). This property is widely used: in parity verification (if input XOR received gives 0, no errors detected), in simple data integrity checks, and to zero a register without using a literal 0 (reg ^ reg synthesizes to constant 0).

Intermediate Level

Q3: You want to set bits [5:4] of a register to value 2'b10. Show the correct sequence of operations. Two steps: clear the field first, then set the new value. reg = (reg & ~8'h30) | ((2'b10 << 4) & 8'h30). Step 1: reg & ~8'h30 clears bits [5:4] to 00. Step 2: | (2'b10 << 4) OR-sets those bits to 10. Using only OR without clearing first would leave old bits set, since OR cannot clear a 1 bit. Q4: Does 0 & X equal 0 or X in SystemVerilog? It equals 0. This is the X-absorption property of AND. Because 0 AND anything is always 0, the value of X doesn't matter — the result is deterministically 0. This is a per-bit rule: in a multi-bit AND, the bits where the mask is 0 will produce clean 0 results even if the data has X in those positions, while bits where the mask is 1 will still propagate X.

Experienced Engineer Level

Q5: In an error injection testbench, you inject a single-bit error with XOR, then verify that the DUT corrects it. How do you use XOR to confirm recovery? Since XOR is its own inverse, re-applying the same error mask to the corrected output should reproduce the original clean data. dut_corrected ^ err_mask === original_data using === to catch any X. If the DUT correctly recovered the bit error, this expression is true. If the DUT changed the wrong bit or didn't correct it, the expression is false and the XOR result shows exactly which additional bits changed.

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.