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
| Operator | Name | Gate equivalent | Primary use |
|---|---|---|---|
& | Bitwise AND | AND gate per bit pair | Masking — extract or clear specific bits |
| | Bitwise OR | OR gate per bit pair | Setting — force specific bits to 1 |
^ | Bitwise XOR | XOR gate per bit pair | Toggling, parity, difference detection |
~ | Bitwise NOT | Inverter per bit | Invert every bit — generate complement |
~& | Bitwise NAND | NAND gate per bit pair | Uncommon in RTL — used in reduction form |
~| | Bitwise NOR | NOR gate per bit pair | Uncommon in RTL — used in reduction form |
~^ or ^~ | Bitwise XNOR | XNOR gate per bit pair | Equivalence 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
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'hAAPer-Bit Truth Table — All Operators
| a | b | a & b | a | b | a ^ b | ~a | a ~^ b |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| 0 | 1 | 0 | 1 | 1 | 1 | 0 |
| 1 | 0 | 0 | 1 | 1 | 0 | 0 |
| 1 | 1 | 1 | 1 | 0 | 0 | 1 |
| 0 | X | 0 ← 0 absorbs | X | X | 1 | X |
| 1 | X | X | 1 ← 1 absorbs | X | 0 | X |
| X | X | X | X | X | X | X |
| 0 | Z | 0 ← 0 absorbs | X | X | 1 | X |
| 1 | Z | X | 1 ← 1 absorbs | X | 0 | X |
Step-by-Step Visual Evaluation
Bit-Level Walkthrough: a = 8'hA5, mask = 8'h0F
| Operation | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | Result (hex) |
|---|---|---|---|---|---|---|---|---|---|
| a | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 8'hA5 |
| mask | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 8'h0F |
| a & mask (EXTRACT) | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 8'h05 |
| a | mask (SET) | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 8'hAF |
| a & ~mask (CLEAR) | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 8'hA0 |
| a ^ mask (TOGGLE) | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 8'hAA |
| ~a (INVERT) | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 8'h5A |
XOR Properties Worth Memorizing
| XOR expression | Result | Why this matters |
|---|---|---|
a ^ 0 | a unchanged | XOR with 0 is identity — no effect |
a ^ 1 | ~a (inverted) | XOR with 1 inverts the bit — useful for toggling |
a ^ a | 0 (all zeros) | XOR with itself cancels — used in parity checks and diff detection |
a ^ ~a | all 1s | XOR with complement is always all-ones |
(a ^ b) ^ b | a (original) | XOR is its own inverse — decode by re-applying the key |
a ^ 8'hFF | ~a | XOR 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.
| a | b | a ^ b (XOR — differ?) | a ~^ b (XNOR — match?) |
|---|---|---|---|
| 0 | 0 | 0 (same) | 1 (match) |
| 0 | 1 | 1 (differ) | 0 (mismatch) |
| 1 | 0 | 1 (differ) | 0 (mismatch) |
| 1 | 1 | 0 (same) | 1 (match) |
Code Examples — From Basics to Production
Example 1 — Beginner: All Operators on Known Values
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
endmoduleExpected 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.
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
endmoduleExample 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.
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
endmoduleExample 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.
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
endmoduleExpected 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.
| Expression | Example | Result | Why |
|---|---|---|---|
8'hxx & 8'h0F | X AND lower-nibble mask | 8'h0x | Upper nibble absorbed to 0, lower nibble propagates X |
8'hxx & 8'h00 | X AND zero mask | 8'h00 | ALL bits absorbed — result is clean 0 |
8'hxx | 8'hFF | X OR all-ones mask | 8'hFF | ALL bits absorbed — result is clean FF |
8'hxx ^ 8'h0F | X XOR any mask | 8'hxx | XOR never absorbs — X propagates through all positions |
~8'hxx | NOT of X | 8'hxx | Inversion 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.
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); // 0xffSynthesis Behavior
| Operator | Synthesized to | Notes |
|---|---|---|
& | AND gate array | One gate per bit pair. Fully synthesizable |
| | OR gate array | One gate per bit pair. Fully synthesizable |
^ | XOR gate array | One gate per bit pair. Used in parity generators, comparators |
~ | Inverter array | One inverter per bit. Fully synthesizable |
~^ | XNOR gate array | One XNOR per bit pair. Common in comparator synthesis |
Where You'll Use These in Real Projects
// ── 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;
endfunctionCommon Bugs & How to Debug Them
Bug 1 — Wrong Mask: Extracting the Wrong Field
// 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!// 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 approachBug 2 — OR Without Clear: Old Bits Contaminate New Field Value
// 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 — unchangedlocalparam 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 correctlyBug 3 — XOR to Check Equality: Wrong Interpretation
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!// 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 ==.
| Task | Correct pattern | Common mistake |
|---|---|---|
| Extract a field | sig[hi:lo] or (sig & mask) >> shift | Wrong mask — off-by-one in position |
| Set specific bits | reg | mask | Using ^ (toggles, not sets) |
| Clear specific bits | reg & ~mask | Using reg & mask (keeps, not clears) |
| Update a field | Clear first: (reg & ~mask) | new_val | Just OR: reg | new_val — old bits remain |
| Find differing bits | a ^ 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 = 0means 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.