Bitwise Operations in C

A practical guide to bit manipulation in C programming — covering every operator, embedded system register configuration, bit-field structs, and the patterns I use daily in production firmware code.

C language code on monitor showing bitwise operators in systems programming
The Six Operators Set, Clear, Toggle GPIO Register Config Bit-Field Structs Signed vs Unsigned FAQ

The Six Bitwise Operators in C

C gives you six bitwise operators that work on any integer type: & (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), and >> (right shift). These operators act on each individual bit of their operands. I use them constantly in embedded work because they map directly to the hardware registers I'm configuring.

Here is what each operator does at the bit level — this is the same truth table you will see in microcontroller datasheets:

I have used bitwise operations in C extensively when programming microcontrollers — setting and clearing individual GPIO pins with PORTB |= (1 << 3) and PORTB &= ~(1 << 3) is the canonical embedded C pattern that every firmware developer learns early.

ABA & BA | BA ^ B~A
000001
010111
100110
111100

C also provides compound assignment forms: &=, |=, ^=, <<=, and >>=. These modify the left operand in place, which is both shorter and expresses intent more clearly. I almost never write x = x & maskx &= mask is the standard idiom.

// Compound assignment examples
uint8_t flags = 0b00000000;
flags |= (1 << 3);   // set bit 3
flags &= ~(1 << 5);  // clear bit 5
flags ^= (1 << 1);   // toggle bit 1

The Four Foundational Idioms: Set, Clear, Toggle, Check

In my experience writing firmware for ARM Cortex-M microcontrollers, about 80 percent of bit manipulation boils down to just four patterns. If you memorize these, you can handle almost any register configuration task:

// Set bit N: OR with a mask that has a 1 at position N
reg |= (1 << N);

// Clear bit N: AND with a mask that has 0 at position N
reg &= ~(1 << N);

// Toggle bit N: XOR with a mask that has 1 at position N
reg ^= (1 << N);

// Check bit N: AND and test for nonzero
if (reg & (1 << N)) { /* bit N is set */ }

When you need to operate on multiple bits at once, combine masks with OR. For example, to set bits 0, 2, and 4 in one operation: reg |= (1 << 0) | (1 << 2) | (1 << 4);. The compiler folds the constant expression at compile time, so there is zero runtime cost for the combined mask.

Always Use volatile for Hardware Registers

When you are writing to memory-mapped hardware registers in C, always declare the pointer as volatile. The compiler will otherwise optimize away repeated reads or writes that are actually required by hardware. I have debugged more than one bug where a register write vanished because the variable was not marked volatile and the compiler thought the write was dead code.

GPIO Register Configuration — A Real Embedded Example

Let me walk through a concrete example from my own STM32 work. Most microcontroller GPIO ports are configured through a set of 32-bit registers: MODER (mode), OTYPER (output type), OSPEEDR (speed), PUPDR (pull-up/down), and ODR (output data). Each pin occupies a specific range of bits within these registers.

Here is how I configure PA5 as an output push-pull at high speed with no pull-up. PA5 means pin 5 in port A. The MODER register uses 2 bits per pin, so pin 5 occupies bits 10-11. General purpose output is the value 0b01 in those bits:

// Configure PA5 as output (STM32 LL-style)
#define PIN5         5
#define GPIO_MODER_MASK  (0x03 << (PIN5 * 2))  // mask for bits 10-11
#define GPIO_MODER_OUT   (0x01 << (PIN5 * 2))  // output mode value

// Step 1: clear the mode bits for pin 5
GPIOA->MODER &= ~GPIO_MODER_MASK;

// Step 2: set output mode
GPIOA->MODER |= GPIO_MODER_OUT;

// Step 3: set output type to push-pull (bit 5 = 0)
GPIOA->OTYPER &= ~(1 << PIN5);

// Step 4: set speed to very high
GPIOA->OSPEEDR |= (0x03 << (PIN5 * 2));

// Step 5: no pull-up, no pull-down
GPIOA->PUPDR &= ~(0x03 << (PIN5 * 2));

// Step 6: write the output high
GPIOA->BSRR = (1 << PIN5);

The pattern is always the same: clear the relevant bits with AND-NOT, then set the desired values with OR. Doing it in two steps prevents a read-modify-write race condition if you tried to set bits that were not already zero. This is the standard idiom you will see in every STM32 HAL, CMSIS, and bare-metal codebase.

Bit-Field Structs in C

C lets you declare struct members with an explicit bit width using the colon syntax. Bit-fields are useful when you want the compiler to handle the packing for you, particularly for hardware register maps or memory-constrained data structures.

// Bit-field struct for an RTC time register
struct rtc_time {
    uint32_t seconds : 6;   // bits 0-5   (0-59)
    uint32_t minutes : 6;   // bits 6-11  (0-59)
    uint32_t hours   : 5;   // bits 12-16 (0-23)
    uint32_t day     : 5;   // bits 17-21 (1-31)
    uint32_t month   : 4;   // bits 22-25 (1-12)
    uint32_t year    : 6;   // bits 26-31 (0-63, offset from 2000)
};

// Access is clean and readable
struct rtc_time tm;
tm.seconds = 45;
tm.minutes = 30;
tm.hours   = 14;

That said, I have a rule of thumb: use bit-fields for readability inside your own code, but never use them for serialization or cross-compiler binary formats. The C standard leaves the memory layout implementation-defined. Bit-field ordering (big-endian vs little-endian), alignment padding, and whether a field can cross a byte boundary all depend on the compiler. For anything that leaves your process — a file format, a network packet, or a shared memory region — use explicit masks and shifts instead.

// Same RTC register using explicit masks (portable)
uint32_t pack_rtc_time(int s, int m, int h, int d, int mo, int y) {
    uint32_t reg = 0;
    reg |= (s & 0x3F);                // bits 0-5
    reg |= ((m & 0x3F) << 6);         // bits 6-11
    reg |= ((h & 0x1F) << 12);        // bits 12-16
    reg |= ((d & 0x1F) << 17);        // bits 17-21
    reg |= ((mo & 0x0F) << 22);       // bits 22-25
    reg |= ((y & 0x3F) << 26);        // bits 26-31
    return reg;
}

Signed vs Unsigned and Implementation-Defined Behavior

C's bitwise operators have a few gotchas that I have learned the hard way. The most important one is that right shift of a signed integer is implementation-defined. On GCC and Clang, it performs an arithmetic shift (sign-extending), but the C standard does not guarantee this. If you need a logical shift that always fills with zeros, cast to the unsigned type first:

int16_t x = -8;        // 0xFFF8 in 16-bit two's complement
int16_t y = x >> 2;    // GCC: -2 (1111 1111 1111 1110)
                       // Standard: implementation-defined!

uint16_t z = (uint16_t)x >> 2;  // always 0x3FFE (16382)

Another trap: when you left-shift a signed integer and the result overflows, that is undefined behavior. The compiler can legally do anything. Shifting into the sign bit of a signed int is UB, even though the same operation is perfectly well-defined on an unsigned int:

int x = 1 << 31;    // UNDEFINED BEHAVIOR on 32-bit int
                     // (shifting into the sign bit)

unsigned int y = 1u << 31;  // OK: 0x80000000, value 2147483648u

My rule: prefer unsigned types for all bitwise manipulation. Use uint32_t, uint16_t, uint8_t from <stdint.h> rather than plain int when you are doing bit work. This avoids sign-extension surprises, undefined behavior from overflow, and makes your intent explicit.

The sizeof Trap

A common bug: 1 << 31 on a 16-bit int produces 0 because 1 is an int and shifting a 16-bit value by 31 is undefined. Always use suffixed constants: 1u << 31 on a 32-bit type, or 1ULL << 63 for 64-bit shifts. Our 64-bit programmer calculator uses BigInt and avoids this class of bug entirely.

Try It Yourself

Enter any two 32-bit integers in our interactive bitwise calculator and see the binary, decimal, and hex results for AND, OR, XOR, and shifts. The live display makes it easy to verify your C code.

Frequently Asked Questions About Bitwise Operations in C

What bitwise operators does C support?

C supports six bitwise operators: AND (&), OR (|), XOR (^), NOT (~), left shift (<<), and right shift (>>). These operators work on integer types (char, short, int, long, long long) and their unsigned variants. C also provides compound assignment operators like &=, |=, ^=, <<=, and >>= for modifying variables in place.

How do I set, clear, and toggle individual bits in C?

To set bit N: value |= (1 << N). To clear bit N: value &= ~(1 << N). To toggle bit N: value ^= (1 << N). To check bit N: if (value & (1 << N)). These four patterns are the foundational idioms of C bit manipulation and appear in virtually every embedded C codebase.

What is a bit-field in C and when should I use it?

A bit-field is a struct member that occupies a specified number of bits. Declared with a colon followed by the bit width, e.g., 'unsigned int flags : 3'. Bit-fields are useful for packing multiple small values into a single machine word, modeling hardware registers, or reducing memory usage in large arrays of structs. However, the exact memory layout is implementation-defined, so they are not suitable for serialization or cross-platform binary formats.

How does right shift behave with signed vs unsigned integers in C?

For unsigned integers, right shift (>>) is always a logical shift — zeros are shifted in from the left. For signed integers, the behavior is implementation-defined, but on virtually all modern compilers, right shift of a signed integer is an arithmetic shift that preserves the sign bit. This means negative numbers remain negative after right shift because 1-bits are shifted in from the left.

Why should I use uint32_t instead of int for bit manipulation?

Using uint32_t and other fixed-width types from <stdint.h> avoids several classes of bugs: undefined behavior from shifting into the sign bit, implementation-defined right-shift behavior, and unexpected sign extension. Fixed-width types also make your code portable across platforms where int may be 16 or 64 bits. I always use uint32_t for bit manipulation in my firmware projects.

Related Tools

Bitwise Calculator →

AND, OR, XOR, NOT, shifts

Bitwise Guide →

Complete operations reference

Hex to ASCII →

Decode hex strings

SHA256 Generator →

Compute hashes