C Bitwise Operations & Low-Level I/O: Bit Manipulation, Flags, and Hardware Register Access

C Bitwise Operations & Low-Level I/O: Bit Manipulation, Flags, and Hardware Register Access
Table of Contents
- Binary Representation Refresher
- The Bitwise Operators
- Bitmasking: Setting, Clearing, Toggling, and Testing Bits
- Bit Shifting: Fast Arithmetic
- Hardware Register Access in Embedded Systems
- Bit Flags: Space-Efficient Boolean Sets
- Advanced Bit Tricks
- Portable Bit Manipulation with stdint.h
- Real-World Applications
- Frequently Asked Questions
- Key Takeaway
Binary Representation Refresher
Every integer type in C is stored as a sequence of binary digits in memory. Understanding the layout:
#include <stdio.h>
#include <stdint.h>
void print_binary(uint8_t value) {
for (int bit = 7; bit >= 0; bit--) {
printf("%d", (value >> bit) & 1);
if (bit == 4) printf(" "); // Visual space between nibbles
}
printf(" = 0x%02X = %u\n", value, value);
}
int main(void) {
print_binary(0b01001101); // 0100 1101 = 0x4D = 77
print_binary(255); // 1111 1111 = 0xFF = 255
print_binary(0); // 0000 0000 = 0x00 = 0
return 0;
}Key insight: Bit position n has value 2^n. Bit 0 (the rightmost) = 1, Bit 7 (the leftmost in a byte) = 128.
The Bitwise Operators
| Operator | Operation | Example | Result |
|---|---|---|---|
a & b | AND: 1 only if both bits are 1 | 0b1100 & 0b1010 | 0b1000 |
a | b | OR: 1 if either bit is 1 | 0b1100 | 0b1010 | 0b1110 |
a ^ b | XOR: 1 if bits are different | 0b1100 ^ 0b1010 | 0b0110 |
~a | NOT: flips every bit | ~0b11001010 | 0b00110101 |
a << n | Left shift n positions | 0b0001 << 3 | 0b1000 (= 8) |
a >> n | Right shift n positions | 0b1000 >> 3 | 0b0001 (= 1) |
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint8_t a = 0b11001010; // 0xCA = 202
uint8_t b = 0b10110101; // 0xB5 = 181
printf("a & b = 0x%02X\n", a & b); // 0x80 (10000000)
printf("a | b = 0x%02X\n", a | b); // 0xFF (11111111)
printf("a ^ b = 0x%02X\n", a ^ b); // 0x7F (01111111)
printf("~a = 0x%02X\n", (uint8_t)~a); // 0x35 (00110101)
printf("a << 2 = 0x%02X\n", (uint8_t)(a << 2)); // 0x28
printf("a >> 2 = 0x%02X\n", a >> 2); // 0x32
return 0;
}Bitmasking: Setting, Clearing, Toggling, and Testing Bits
The four fundamental bit operations using a mask (a value with specific bits set):
#include <stdint.h>
#include <stdio.h>
// Define bit positions as constants
#define BIT(n) (1U << (n)) // Creates a mask with only bit n set
#define BIT_SET(v, mask) ((v) | (mask)) // Set bits in mask to 1
#define BIT_CLEAR(v, mask) ((v) & ~(mask)) // Clear bits in mask to 0
#define BIT_TOGGLE(v, mask)((v) ^ (mask)) // Flip bits in mask
#define BIT_TEST(v, mask) (((v) & (mask)) != 0) // True if ANY bit in mask is set
int main(void) {
uint8_t reg = 0b00000000; // All bits off
// Set bit 3 and bit 5
reg = BIT_SET(reg, BIT(3) | BIT(5));
printf("After set: 0b%08b = 0x%02X\n", reg, reg); // 0b00101000 = 0x28
// Clear bit 3
reg = BIT_CLEAR(reg, BIT(3));
printf("After clear: 0b%08b = 0x%02X\n", reg, reg); // 0b00100000 = 0x20
// Toggle bit 5
reg = BIT_TOGGLE(reg, BIT(5));
printf("After toggle: 0b%08b = 0x%02X\n", reg, reg); // 0b00000000 = 0x00
// Test bit 3 (should be false after clearing it)
printf("Bit 3 set: %s\n", BIT_TEST(reg, BIT(3)) ? "yes" : "no"); // no
return 0;
}This BIT(n) macro pattern is used extensively in the Linux kernel (include/linux/bitops.h) and every embedded systems codebase for hardware register manipulation.
Bit Shifting: Fast Arithmetic
Left shifting by n multiplies by 2^n; right shifting by n divides by 2^n (for unsigned integers):
#include <stdint.h>
#include <stdio.h>
int main(void) {
uint32_t x = 16;
printf("x = %u\n", x); // 16
printf("x << 1 = %u (×2)\n", x << 1); // 32
printf("x << 3 = %u (×8)\n", x << 3); // 128
printf("x >> 1 = %u (÷2)\n", x >> 1); // 8
printf("x >> 2 = %u (÷4)\n", x >> 2); // 4
// Compute powers of 2 efficiently
for (int i = 0; i < 10; i++) {
printf("2^%d = %u\n", i, 1U << i);
}
return 0;
}[!WARNING] Left shifting into the sign bit of signed integers is undefined behavior in C. Always use
unsignedtypes for bit shifting. Also, shifting by ≥ width of the type (e.g.,uint32_t x << 32) is undefined behavior.
Hardware Register Access in Embedded Systems
In embedded systems (STM32, ESP32, Arduino), hardware peripherals are controlled by writing to memory-mapped registers at specific addresses. Bitwise operations are the only way to interface with hardware:
#include <stdint.h>
// Microcontroller GPIO register layout (simplified STM32 style)
// RCC_AHB1ENR: Clock enable register
// GPIOA_MODER: GPIO port A mode register
// GPIOA_ODR: GPIO port A output data register
#define RCC_AHB1ENR (*((volatile uint32_t*)0x40023830))
#define GPIOA_MODER (*((volatile uint32_t*)0x40020000))
#define GPIOA_ODR (*((volatile uint32_t*)0x40020014))
#define GPIOA_EN BIT(0) // Clock enable bit for GPIOA
#define PIN5_MODE_SHIFT (10) // MODER bits for pin 5
#define OUTPUT_MODE (0x01U) // 01 = general purpose output
void led_init(void) {
RCC_AHB1ENR |= GPIOA_EN; // Enable GPIOA clock
GPIOA_MODER &= ~(0x03U << PIN5_MODE_SHIFT); // Clear mode bits for pin 5
GPIOA_MODER |= (OUTPUT_MODE << PIN5_MODE_SHIFT); // Set to output
}
void led_on(void) { GPIOA_ODR |= BIT(5); } // Set pin 5 high
void led_off(void) { GPIOA_ODR &= ~BIT(5); } // Set pin 5 low
void led_toggle(void) { GPIOA_ODR ^= BIT(5); } // Toggle pin 5The volatile keyword is critical — it tells the compiler never to optimize away reads/writes to hardware registers, even if they appear redundant from a pure C logic perspective.
Bit Flags: Space-Efficient Boolean Sets
Instead of an array of booleans (1 byte each), use a single integer with each bit representing one flag:
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
// File permission flags (Unix-style)
typedef enum {
PERM_READ = BIT(0), // 001
PERM_WRITE = BIT(1), // 010
PERM_EXECUTE = BIT(2), // 100
} FilePermission;
// HTTP response flags
typedef enum {
RESP_COMPRESSED = BIT(0), // gzip/br
RESP_CACHED = BIT(1), // from CDN cache
RESP_AUTHENTICATED = BIT(2), // valid auth token
RESP_CHUNKED = BIT(3), // transfer-encoding: chunked
} ResponseFlags;
void process_response(uint32_t flags) {
if (BIT_TEST(flags, RESP_COMPRESSED)) printf("Response is compressed\n");
if (BIT_TEST(flags, RESP_CACHED)) printf("Served from cache\n");
if (BIT_TEST(flags, RESP_AUTHENTICATED)) printf("User authenticated\n");
}
int main(void) {
// A read-write file (not executable)
uint8_t file_perms = PERM_READ | PERM_WRITE;
printf("Can read: %s\n", BIT_TEST(file_perms, PERM_READ) ? "yes" : "no"); // yes
printf("Can execute: %s\n", BIT_TEST(file_perms, PERM_EXECUTE) ? "yes" : "no"); // no
// An HTTP response: compressed + authenticated
process_response(RESP_COMPRESSED | RESP_AUTHENTICATED);
return 0;
}Bit flags are used in: POSIX file permissions, socket options (SO_REUSEADDR | SO_REUSEPORT), OpenGL/Vulkan state flags, OS kernel capability sets, and virtually every systems API.
Advanced Bit Tricks
#include <stdint.h>
#include <stdio.h>
// 1. Test if a number is a power of 2 (clean, branchless)
bool is_power_of_2(uint32_t n) {
return n != 0 && (n & (n - 1)) == 0;
// n=8: 8 & 7 = 1000 & 0111 = 0000 ✓
}
// 2. Round up to next power of 2
uint32_t next_power_of_2(uint32_t n) {
n--;
n |= n >> 1; n |= n >> 2;
n |= n >> 4; n |= n >> 8;
n |= n >> 16;
return n + 1;
}
// 3. Count set bits (popcount) — also: __builtin_popcount(x) on GCC/Clang
int popcount(uint32_t n) {
int count = 0;
while (n) { n &= n - 1; count++; } // Each iteration clears lowest set bit
return count;
}
// 4. XOR swap (no temp variable)
void xor_swap(int *a, int *b) {
*a ^= *b;
*b ^= *a;
*a ^= *b;
// Caution: undefined behavior if a == b (same address)!
}
// 5. Extract N bits starting at position P
uint32_t extract_bits(uint32_t value, int pos, int n) {
return (value >> pos) & ((1U << n) - 1);
}
int main(void) {
printf("Is 64 power of 2: %d\n", is_power_of_2(64)); // 1
printf("Is 100 power of 2: %d\n", is_power_of_2(100)); // 0
printf("Next pow2(100) = %u\n", next_power_of_2(100)); // 128
printf("popcount(0xFF) = %d\n", popcount(0xFF)); // 8
printf("Bits [2..4] of 0b11101100 = %u\n", extract_bits(0b11101100, 2, 3)); // 0b011 = 3
return 0;
}Real-World Applications
| Application | Bit Technique |
|---|---|
| IPv4 subnet masking | ip & netmask extracts network address |
| Bloom filters | Hash → bit position, OR to insert, AND to test |
| RAID-5 parity | XOR of all data drives = parity; recover any one lost drive |
| Chess engines (bitboards) | 64-bit integer = full chessboard; OR/AND for piece lookup |
| Huffman compression | Variable-length bit codes packed into byte streams |
| Network packet flags | TCP flags: SYN/ACK/FIN bits in a 6-bit field |
| Image processing | Pixel channel extraction: (pixel >> 16) & 0xFF for red |
Frequently Asked Questions
When should I use bitwise AND to check a flag vs equaling to a constant?
Use if (flags & SOME_FLAG) to test if that specific bit is set, regardless of other bits. Use if (flags == SOME_FLAG) only if you need the flags value to be exactly equal to that value (all other bits zero). For flag testing, always prefer &.
Is right-shifting signed integers portable?
Right-shifting a signed negative integer is implementation-defined in C — some architectures fill with 1s (arithmetic shift, preserving sign), others fill with 0s (logical shift). For portable bit manipulation, always use uint8_t, uint16_t, uint32_t, or uint64_t from <stdint.h>.
What is __builtin_popcount and when should I use it?
GCC and Clang's __builtin_popcount(x) compiles to the CPU's POPCNT instruction on x86-64 — a single-cycle operation that counts set bits. It's dramatically faster than the manual loop. For portable code, use __builtin_popcount with a fallback implementation when __GNUC__ is not defined.
Key Takeaway
Bitwise manipulation is C's interface to the Physical Silicon. By mastering the six bitwise operators and their combination patterns, you communicate directly with hardware registers, build space-efficient flag systems, and implement algorithms (RAID parity, chess engines, bloom filters) that are impossible to express efficiently in higher-level languages.
Read next: Processes, Fork & Exec: System Multitasking →
Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.
