Bitwise Operations in C

From register bit-banging to interrupt flag clearing — the C bitwise reference that actually shows you the hardware

C is the language of systems programming because it gives you direct access to memory and hardware registers. Bitwise operations are not optional extras in C — they are the primary way you talk to peripherals, configure microcontroller pins, and manage protocol flags. Each of the six operators maps directly to a single CPU instruction. Here is the complete reference, with embedded systems examples you can use today. Use the BitwiseCalc to verify bit-level results as you work through the examples.

All Six Operators at a Glance

OperatorNameExample
&ANDa & b
|ORa | b
^XORa ^ b
~NOT~a
<<Left shifta << n
>>Right shifta >> n

Operator Precedence: Always Use Parentheses

This is the number one C bitwise bug. The equality operators == and != have higher precedence than & and |. That means if (flags & MASK == MASK) is parsed as if (flags & (MASK == MASK)), which always evaluates MASK == MASK to 1 first. The fix is unconditional parentheses:

if ((flags & MASK) == MASK) { /* correct */ }

When in doubt, add parentheses. The compiler generates identical code either way, but your future self will thank you. Verify any expression on the bitwise AND calculator to catch precedence mistakes early.

Unsigned vs Signed: Pick a Side and Stick to It

Right-shifting a signed negative integer is implementation-defined behavior in C. On most compilers it uses arithmetic shift (sign extension), but the standard does not guarantee it. For register manipulation, always use unsigned int, uint8_t, uint16_t, or uint32_t from <stdint.h>. This gives you predictable logical shifts and well-defined overflow behavior.

uint32_t reg = 0x000000FF;
reg = (reg >> 4) & 0x0F;  // guaranteed logical shift

volatile and Hardware Registers

Hardware registers are memory-mapped and can change outside your program's control (interrupts, DMA, peripheral state changes). Without volatile, the compiler may optimize away repeated reads or writes. Always declare register pointers as volatile:

#define PORTA (*(volatile uint8_t *)0x3B)

void set_pin(uint8_t pin) {
    PORTA |= (1 << pin);   // set pin high
}

void clear_pin(uint8_t pin) {
    PORTA &= ~(1 << pin);  // set pin low (clear bit)
}

void toggle_pin(uint8_t pin) {
    PORTA ^= (1 << pin);   // toggle state
}

The pattern PORTA |= (1 << pin) reads the current register value, sets the target bit with OR, and writes it back — without touching any other bits. This is the exact pattern used in Arduino digitalWrite() under the hood. Try our bitwise OR calculator and XOR calculator to see these patterns in action with arbitrary values.

Struct Bit-Fields

C lets you pack multiple fields into a single integer with bit-field syntax. The compiler handles the shifting and masking for you:

struct uart_config {
    uint32_t baud_rate  : 20;  // 20 bits for baud rate
    uint32_t parity     : 2;   // 2 bits: 00=none, 01=odd, 10=even
    uint32_t stop_bits  : 1;   // 1 bit: 0=1 stop, 1=2 stop
    uint32_t enabled    : 1;   // 1 bit: enable flag
    uint32_t reserved   : 8;   // padding to 32 bits
};

Bit-fields are convenient but come with portability caveats: the order of bits within a byte depends on the compiler and endianness. For cross-platform code or network protocols, explicit shifting and masking is safer.

Interrupt Flag Clearing

Many microcontrollers use "write 1 to clear" interrupt flags. You cannot use a read-modify-write sequence because that would accidentally clear other pending flags. The standard pattern writes the specific flag bit directly:

// Clear the TIMER1 overflow interrupt flag
TIFR1 = (1 << TOV1);  // write 1 to the flag bit, clears it

This is fundamentally a bitwise operation, and understanding why a simple |= would be wrong here is what separates firmware developers from generalist programmers.

Endianness: One Final Warning

Bitwise operations work on the value, not the byte layout in memory. 0x12345678 >> 8 always gives 0x00123456 regardless of whether your CPU is little-endian or big-endian. Endianness only matters when you cast a pointer and read bytes individually. As long as you stay in the world of unsigned integers and bitwise operators, you are endianness-safe.

C's bitwise operators are the vocabulary of hardware programming. Master these patterns, and every datasheet register map becomes a set of solvable problems. Bookmark the BitwiseCalc for quick verification during development, and check the bitwise operations guide for deeper operator theory.