Skip to content
Home » Embedded Systems » Embedded C » Bitwise Operations and Bit Fields in C for Embedded Systems

Bitwise Operations and Bit Fields in C for Embedded Systems

Embedded Systems Learning Path
Part 35 of 129View Full Path →

KEY TAKEAWAYS

  • Bitwise AND (&), OR (|), XOR (^), NOT (~), and shifts (<<, >>) are the foundation of all register-level embedded programming.
  • The SET/CLEAR/TOGGLE/CHECK pattern using |=, &=~, ^=, and & is used in every embedded C project.
  • Bit fields provide named access to individual bits within a register but have portability and compiler-dependent behavior.
  • Use macros or inline functions for bit manipulation in production code — bit fields for internal structures only.
  • Masking and shifting are essential for packing/unpacking multi-bit fields in protocol data and hardware registers.
  • Always use unsigned types for bitwise operations to avoid sign-extension and undefined behavior with shifts.

Why Bitwise Operations Matter

In embedded systems, you don’t just write to variables — you write to hardware registers where individual bits control specific functions. A single 32-bit register might contain an enable flag in bit 0, a mode selector in bits 1-2, a clock divider in bits 3-7, and a status flag in bit 31. You need to change specific bits without affecting the others. That’s what bitwise operations are for.

Every microcontroller datasheet describes registers at the bit level. Without mastering bitwise operations, you simply cannot program bare-metal embedded systems. It will also be very helpful to understand Number Systems — Binary, Hex, Decimal before diving in.

The Six Bitwise Operators

C provides six operators that work on individual bits rather than on entire values. In embedded programming, you use these constantly — every register configuration, every protocol byte, and every flag check uses at least one. Memorize what each operator does at the bit level, because reading and writing embedded C code without this knowledge is like reading a foreign language without knowing the alphabet.

/* The six C bitwise operators and what they do */

#include <stdint.h>
#include <stdio.h>

void bitwise_basics(void) {
    uint8_t a = 0b11001010;  /* 0xCA = 202 */
    uint8_t b = 0b10110101;  /* 0xB5 = 181 */

    /* 1. AND (&) — both bits must be 1 */
    uint8_t and_result = a & b;
    /*   11001010
     * & 10110101
     *   --------
     *   10000000  = 0x80
     * Use: Masking — extract specific bits */

    /* 2. OR (|) — at least one bit must be 1 */
    uint8_t or_result = a | b;
    /*   11001010
     * | 10110101
     *   --------
     *   11111111  = 0xFF
     * Use: Setting bits — force specific bits to 1 */

    /* 3. XOR (^) — bits must differ */
    uint8_t xor_result = a ^ b;
    /*   11001010
     * ^ 10110101
     *   --------
     *   01111111  = 0x7F
     * Use: Toggling bits, detecting changes */

    /* 4. NOT (~) — flip every bit */
    uint8_t not_result = ~a;
    /*   ~11001010
     *   --------
     *    00110101  = 0x35
     * Use: Creating masks for clearing bits */

    /* 5. Left shift (<<) — multiply by powers of 2 */
    uint8_t lshift = 1 << 3;
    /*   00000001 << 3
     *   --------
     *   00001000  = 8
     * Use: Creating bit masks (1 << n) */

    /* 6. Right shift (>>) — divide by powers of 2 */
    uint8_t rshift = 0x80 >> 3;
    /*   10000000 >> 3
     *   --------
     *   00010000  = 0x10
     * Use: Extracting bit fields from higher positions */
}

The diagrams below illustrate the truth tables for AND, OR, XOR, and NOT at a glance:

Bitwise AND operator

Bitwise OR operator

Bitwise XOR operator

Bitwise NOT operator

Bitwise left shift operator

Bitwise right shift operator

The Four Essential Patterns

All register manipulation in embedded C boils down to four patterns. Memorize these — you’ll use them hundreds of times in every project.

/* The four patterns every embedded C programmer must know */

#define LED_PIN     5
#define ENABLE_BIT  0
#define MODE_BIT    3

volatile uint32_t *GPIO_ODR = (volatile uint32_t *)0x40010C0C;
volatile uint32_t *CTRL_REG = (volatile uint32_t *)0x40004400;

/* ─── Pattern 1: SET a bit (force to 1) ─── */
/* Formula: register |= (1 << bit) */

*GPIO_ODR |= (1 << LED_PIN);      /* Turn on LED (set bit 5) */
*CTRL_REG |= (1 << ENABLE_BIT);   /* Enable peripheral */

/* Before: xxxx xxxx  (x = whatever was there)
 * Mask:   0010 0000  (1 << 5)
 * After:  xx1x xxxx  (bit 5 is now 1, others unchanged) */


/* ─── Pattern 2: CLEAR a bit (force to 0) ─── */
/* Formula: register &= ~(1 << bit) */

*GPIO_ODR &= ~(1 << LED_PIN);     /* Turn off LED (clear bit 5) */
*CTRL_REG &= ~(1 << ENABLE_BIT);  /* Disable peripheral */

/* Before: xxxx xxxx
 * ~Mask:  1101 1111  ~(1 << 5) = all 1s except bit 5
 * After:  xx0x xxxx  (bit 5 is now 0, others unchanged) */


/* ─── Pattern 3: TOGGLE a bit (flip) ─── */
/* Formula: register ^= (1 << bit) */

*GPIO_ODR ^= (1 << LED_PIN);      /* Toggle LED */

/* Before: xx1x xxxx  (LED was on)
 * Mask:   0010 0000
 * After:  xx0x xxxx  (LED is now off) */


/* ─── Pattern 4: CHECK a bit (read) ─── */
/* Formula: (register & (1 << bit)) */

if (*CTRL_REG & (1 << ENABLE_BIT)) {
    /* Peripheral is enabled */
}

/* Normalize to 0 or 1 */
uint8_t is_enabled = (*CTRL_REG >> ENABLE_BIT) & 1;

Multi-Bit Field Manipulation

Many register fields span multiple bits. For example, a UART baud rate divider might be 12 bits wide, or a timer prescaler might be 3 bits. Manipulating these requires a mask-shift-OR pattern.

/* Working with multi-bit fields in registers
 *
 * Example register layout:
 * Bits [31:8]  Reserved
 * Bits [7:4]   MODE (4-bit field, values 0-15)
 * Bits [3:2]   SPEED (2-bit field: 00=low, 01=med, 10=high, 11=very_high)
 * Bit  [1]     DIRECTION (0=input, 1=output)
 * Bit  [0]     ENABLE
 */

#define CONFIG_REG  (*(volatile uint32_t *)0x40004000)

/* Field definitions: mask and position */
#define MODE_MASK     (0xF << 4)    /* 0xF0 = 1111 0000 */
#define MODE_POS      4
#define SPEED_MASK    (0x3 << 2)    /* 0x0C = 0000 1100 */
#define SPEED_POS     2
#define DIR_BIT       (1 << 1)      /* 0x02 */
#define ENABLE_BIT    (1 << 0)      /* 0x01 */

/* Speed values */
#define SPEED_LOW       0
#define SPEED_MED       1
#define SPEED_HIGH      2
#define SPEED_VERY_HIGH 3

/* SET a multi-bit field (clear old value, write new value) */
void set_speed(uint8_t speed) {
    uint32_t reg = CONFIG_REG;
    reg &= ~SPEED_MASK;                  /* Clear the field bits */
    reg |= ((speed << SPEED_POS) & SPEED_MASK);  /* Insert new value */
    CONFIG_REG = reg;
}

/* GET a multi-bit field */
uint8_t get_speed(void) {
    return (CONFIG_REG & SPEED_MASK) >> SPEED_POS;
}

/* Generic field macros */
#define FIELD_SET(reg, mask, pos, val)  \
    ((reg) = ((reg) & ~(mask)) | (((val) << (pos)) & (mask)))
#define FIELD_GET(reg, mask, pos)  \
    (((reg) & (mask)) >> (pos))

/* Usage */
void configure_peripheral(void) {
    uint32_t reg = 0;

    /* Build the register value */
    FIELD_SET(reg, MODE_MASK, MODE_POS, 0x0A);     /* Mode = 10 */
    FIELD_SET(reg, SPEED_MASK, SPEED_POS, SPEED_HIGH);  /* Speed = high */
    reg |= DIR_BIT;      /* Output */
    reg |= ENABLE_BIT;   /* Enabled */

    CONFIG_REG = reg;

    /* Read back */
    uint8_t mode = FIELD_GET(CONFIG_REG, MODE_MASK, MODE_POS);
    uint8_t speed = FIELD_GET(CONFIG_REG, SPEED_MASK, SPEED_POS);
}

Bit Manipulation Patterns for Embedded Registers

For reusable, driver-quality code, wrap the four core patterns in small helper functions and a BIT macro. This is the form you’ll see in professional embedded codebases and HAL libraries:

#include <stdint.h>

#define BIT(n) (1U << (n))

/* Set a specific bit (turn ON) */
void set_bit(volatile uint32_t *reg, uint8_t bit) {
    *reg |= BIT(bit);
}

/* Clear a specific bit (turn OFF) */
void clear_bit(volatile uint32_t *reg, uint8_t bit) {
    *reg &= ~BIT(bit);
}

/* Toggle a specific bit (flip) */
void toggle_bit(volatile uint32_t *reg, uint8_t bit) {
    *reg ^= BIT(bit);
}

/* Check if a specific bit is set */
int is_bit_set(volatile uint32_t *reg, uint8_t bit) {
    return (*reg & BIT(bit)) != 0;
}

/* Set multiple bits using a mask */
void set_mask(volatile uint32_t *reg, uint32_t mask) {
    *reg |= mask;
}

/* Clear multiple bits using a mask */
void clear_mask(volatile uint32_t *reg, uint32_t mask) {
    *reg &= ~mask;
}

/* Modify a bit field: clear old value, set new value */
void write_field(volatile uint32_t *reg, uint32_t mask, uint32_t value, uint8_t offset) {
    *reg = (*reg & ~mask) | (value << offset);
}

The write_field pattern is essential for configuring peripheral registers where multiple settings share a single register — for example, configuring UART baud rate bits or GPIO mode bits.

Practical Examples: GPIO and Timer Configuration

Here’s how bitwise operators are used in real STM32 GPIO configuration:

/* Configure PA5 as output, push-pull, high speed */
/* MODER register: 2 bits per pin (00=input, 01=output, 10=alternate, 11=analog) */
GPIOA->MODER &= ~(0x3 << (5 * 2));    /* Clear bits 10-11 */
GPIOA->MODER |=  (0x1 << (5 * 2));    /* Set as output (01) */

/* OSPEEDR register: 2 bits per pin (00=low, 01=medium, 10=high, 11=very high) */
GPIOA->OSPEEDR &= ~(0x3 << (5 * 2));
GPIOA->OSPEEDR |=  (0x3 << (5 * 2));  /* Set very high speed (11) */

/* Toggle LED on PA5 */
GPIOA->ODR ^= (1 << 5);

Timer configuration example:

/* Configure TIM2 for PWM on channel 1 */
/* Set PWM mode 1 (OC1M = 110 in bits 6:4 of CCMR1) */
TIM2->CCMR1 &= ~(0x7 << 4);     /* Clear OC1M bits */
TIM2->CCMR1 |=  (0x6 << 4);     /* Set PWM mode 1 (110) */

/* Enable preload (OC1PE = bit 3) */
TIM2->CCMR1 |= (1 << 3);

/* Enable capture/compare output (CC1E = bit 0 of CCER) */
TIM2->CCER |= (1 << 0);

/* Set period and duty cycle */
TIM2->ARR = 999;    /* Period = 1000 counts */
TIM2->CCR1 = 500;   /* 50% duty cycle */

Bitmask Techniques: Extracting and Inserting Fields

Working with bit fields within registers requires extracting and inserting values at specific positions:

/* Extract a field from a register value */
/* Example: extract bits 7:4 (4-bit field at offset 4) */
uint8_t extract_field(uint32_t reg, uint8_t offset, uint8_t width) {
    uint32_t mask = ((1U << width) - 1) << offset;
    return (reg & mask) >> offset;
}

/* Insert a field into a register value */
uint32_t insert_field(uint32_t reg, uint8_t offset, uint8_t width, uint32_t value) {
    uint32_t mask = ((1U << width) - 1) << offset;
    reg &= ~mask;                    /* Clear the field */
    reg |= (value << offset) & mask; /* Insert new value */
    return reg;
}

/* Usage example: ADC configuration register
 * Bits 7:6 = resolution (00=12bit, 01=10bit, 10=8bit, 11=6bit)
 * Bits 5:4 = alignment (00=right, 01=left) */
uint32_t config = ADC1->CR1;
uint8_t resolution = extract_field(config, 6, 2);  /* Get current resolution */
config = insert_field(config, 6, 2, 0x02);          /* Set to 8-bit (10) */
ADC1->CR1 = config;

Common patterns using masks:

/* Check if ANY bits in a mask are set */
if (status_reg & ERROR_MASK) { handle_errors(); }

/* Check if ALL bits in a mask are set */
if ((status_reg & READY_MASK) == READY_MASK) { proceed(); }

/* Count set bits (population count) — useful for counting active channels */
int count_bits(uint32_t n) {
    int count = 0;
    while (n) {
        count++;
        n &= (n - 1);  /* Clear lowest set bit (Brian Kernighan's algorithm) */
    }
    return count;
}

/* Find the position of the lowest set bit */
int lowest_bit_position(uint32_t n) {
    if (n == 0) return -1;
    return __builtin_ctz(n);  /* GCC built-in: count trailing zeros */
}

Bit Fields: Named Bit Access

C provides a built-in language feature for accessing individual bits: bit fields within structs. They let you use named members instead of manual shifting and masking.

/* Bit fields — language-level bit access */

/* Define a register using bit fields */
typedef struct {
    uint32_t enable    : 1;   /* Bit 0 */
    uint32_t direction : 1;   /* Bit 1 */
    uint32_t speed     : 2;   /* Bits 2-3 */
    uint32_t mode      : 4;   /* Bits 4-7 */
    uint32_t reserved  : 24;  /* Bits 8-31 */
} config_reg_t;

/* Map it to a hardware register */
#define CONFIG  ((volatile config_reg_t *)0x40004000)

void configure_with_bitfields(void) {
    /* Clean, readable access */
    CONFIG->enable    = 1;
    CONFIG->direction = 1;        /* Output */
    CONFIG->speed     = 2;        /* High speed */
    CONFIG->mode      = 0x0A;     /* Mode 10 */

    /* Reading is equally clean */
    if (CONFIG->enable) {
        uint8_t current_mode = CONFIG->mode;
    }
}

/* ─── Bit Field WARNINGS ─── */

/* 1. Bit ordering is compiler-dependent!
 * Some compilers pack from LSB (bit 0 first).
 * Others pack from MSB (bit 31 first).
 * GCC on ARM: LSB first (matches our definition above).
 * But this is NOT guaranteed by the C standard!
 */

/* 2. Bit fields may cause read-modify-write on the ENTIRE register */
/* CONFIG->enable = 1; might:
 *   - Read all 32 bits of CONFIG register
 *   - Modify bit 0
 *   - Write all 32 bits back
 * This can clear status flags or cause race conditions with interrupts!
 */

/* 3. You cannot take the address of a bit field */
/* uint32_t *p = &CONFIG->mode;  ← COMPILE ERROR */

/* 4. Bit fields cannot be volatile individually */
/* The volatile applies to the whole struct, not individual fields.
 * This means the compiler might optimize multiple bit field writes
 * into a single register write. */

Bit Fields vs Manual Masking: The Verdict

C offers two ways to work with individual bits: manual masking with the bitwise operators above, or bit fields — a language feature that lets you name individual bits within a struct. Both have their place, but the choice matters more in embedded code than in any other domain because it affects portability, compiler output, and whether your code matches the hardware register layout reliably.

/* When to use each approach */

/* ✅ Use bit fields for INTERNAL data structures */
/* (Not mapped to hardware — no portability concern) */

typedef struct {
    uint8_t  active      : 1;
    uint8_t  error       : 1;
    uint8_t  direction   : 1;
    uint8_t  speed_mode  : 2;
    uint8_t  reserved    : 3;
} motor_flags_t;

typedef struct {
    motor_flags_t flags;
    int16_t       target_rpm;
    int16_t       current_rpm;
} motor_state_t;

/* Clean, readable code for internal state */
void motor_update(motor_state_t *m) {
    if (m->flags.active && !m->flags.error) {
        /* Run motor */
    }
}


/* ✅ Use manual masking for HARDWARE registers */
/* (Portable, predictable, no read-modify-write surprises) */

#define CTRL_REG   (*(volatile uint32_t *)0x40004000)
#define CTRL_EN    (1u << 0)
#define CTRL_DIR   (1u << 1)

static inline void motor_enable(void) {
    CTRL_REG |= CTRL_EN;  /* Explicit, portable, atomic */
}

static inline void motor_set_dir(uint8_t forward) {
    if (forward)
        CTRL_REG |= CTRL_DIR;
    else
        CTRL_REG &= ~CTRL_DIR;
}

Common Pitfalls with Bitwise Operations in C

Several subtle bugs commonly arise with bitwise operators. Being aware of them will save you hours of debugging mysterious hardware behavior.

1. Sign extension with signed types: right-shifting a signed integer is implementation-defined — it may fill with 0s (logical shift) or 1s (arithmetic shift).

int8_t val = -1;     /* Binary: 11111111 */
int8_t shifted = val >> 4;  /* Could be 0x0F (logical) or 0xFF (arithmetic) */
/* Solution: always use unsigned types for bitwise operations */
uint8_t uval = 0xFF;
uint8_t ushifted = uval >> 4;  /* Always 0x0F */

2. Shifting by the type width: shifting a 32-bit value by 32 or more bits is undefined behavior.

uint32_t x = 1;
uint32_t bad = x << 32;   /* UB! Shift amount >= type width */
uint32_t also_bad = x << -1;  /* UB! Negative shift amount */

3. Implicit promotion to int: in C, values smaller than int are promoted to int before operations. This can cause unexpected sign extension.

uint8_t a = 0xFF;
uint32_t result = ~a;  /* WRONG: a is promoted to int (0x000000FF), ~a = 0xFFFFFF00 */
                        /* You might expect 0x00, but you get 0xFFFFFF00 */
/* Solution: cast explicitly */
uint32_t result_correct = (uint8_t)(~a);  /* Correct: 0x00 */
/* Or use a mask */
uint32_t result_masked = (~a) & 0xFF;    /* Correct: 0x00 */

4. Operator precedence: bitwise operators have lower precedence than comparison operators. Always use parentheses.

/* WRONG: parsed as status & (0x01 == 0) */
if (status & 0x01 == 0) { /* ... */ }

/* CORRECT: use parentheses */
if ((status & 0x01) == 0) { /* ... */ }

Practical Examples

Theory only sticks when you apply it. These three examples demonstrate how bitwise operations solve real embedded problems — controlling hardware, packing data for communication protocols, and managing system state efficiently. Each example uses only the operators and patterns covered above.

Example 1: LED Pattern Controller

This example shows how shift and mask operations can create animated LED patterns on a port — a common task in embedded development. The approach uses bitwise rotation to cycle through patterns without needing lookup tables or complex logic, keeping code size minimal for resource-constrained microcontrollers.

/* LED pattern controller using bitwise operations
 * Controls 8 LEDs connected to GPIOB pins 0-7
 */

#define GPIOB_ODR  (*(volatile uint32_t *)0x40010C0C)
#define LED_MASK   0xFF  /* Bits 0-7 */

/* Set specific LED pattern without affecting other pins */
void led_set_pattern(uint8_t pattern) {
    uint32_t reg = GPIOB_ODR;
    reg &= ~LED_MASK;          /* Clear LED bits */
    reg |= (pattern & LED_MASK);  /* Set new pattern */
    GPIOB_ODR = reg;
}

/* Rotate pattern left */
uint8_t rotate_left(uint8_t pattern, uint8_t count) {
    return (pattern << count) | (pattern >> (8 - count));
}

/* Knight Rider (KITT) scanner effect */
void kitt_scanner(void) {
    uint8_t pos = 0;
    int8_t  dir = 1;

    while (1) {
        led_set_pattern(1 << pos);
        pos += dir;
        if (pos >= 7) dir = -1;
        if (pos == 0)  dir = 1;

        delay_ms(100);
    }
}

/* Binary counter display */
void binary_counter(void) {
    uint8_t count = 0;
    while (1) {
        led_set_pattern(count++);
        delay_ms(500);
    }
}

Example 2: Protocol Byte Packing

Communication protocols often pack multiple fields into a single byte or word to minimize bandwidth. This example demonstrates how to pack sensor readings, status flags, and sequence numbers into compact protocol frames using shifts and masks — exactly the kind of code you write when implementing custom UART or radio protocols.

/* Packing and unpacking data using bitwise operations
 *
 * Sensor data protocol:
 * Byte 0: [SENSOR_ID:4][STATUS:2][PARITY:1][VALID:1]
 * Byte 1-2: 12-bit reading (big-endian, upper 4 bits unused)
 */

typedef struct {
    uint8_t sensor_id;    /* 0-15 */
    uint8_t status;       /* 0-3 */
    uint16_t reading;     /* 0-4095 */
    uint8_t valid;        /* 0-1 */
} sensor_data_t;

/* Pack sensor data into 3 bytes for transmission */
void sensor_pack(const sensor_data_t *data, uint8_t *buf) {
    /* Calculate parity (XOR all data bits) */
    uint8_t parity = 0;
    parity ^= data->sensor_id;
    parity ^= data->status;
    parity ^= (uint8_t)(data->reading & 0xFF);
    parity ^= (uint8_t)(data->reading >> 8);
    parity = parity & 1;  /* Keep only LSB */

    /* Pack byte 0 */
    buf[0] = ((data->sensor_id & 0x0F) << 4)  |  /* Bits 7:4: sensor_id */
             ((data->status & 0x03) << 2)      |  /* Bits 3:2: status */
             ((parity & 0x01) << 1)            |  /* Bit 1: parity */
             (data->valid & 0x01);                /* Bit 0: valid flag */

    /* Pack bytes 1-2: 12-bit reading, big-endian */
    buf[1] = (uint8_t)(data->reading >> 4);       /* Upper 8 bits */
    buf[2] = (uint8_t)((data->reading & 0x0F) << 4);  /* Lower 4 bits in upper nibble */
}

/* Unpack 3 bytes back into sensor_data_t */
int sensor_unpack(const uint8_t *buf, sensor_data_t *data) {
    data->sensor_id = (buf[0] >> 4) & 0x0F;
    data->status    = (buf[0] >> 2) & 0x03;
    uint8_t parity  = (buf[0] >> 1) & 0x01;
    data->valid     = buf[0] & 0x01;

    data->reading = ((uint16_t)buf[1] << 4) | ((buf[2] >> 4) & 0x0F);

    /* Verify parity */
    uint8_t calc_parity = 0;
    calc_parity ^= data->sensor_id;
    calc_parity ^= data->status;
    calc_parity ^= (uint8_t)(data->reading & 0xFF);
    calc_parity ^= (uint8_t)(data->reading >> 8);

    if ((calc_parity & 1) != parity) {
        return -1;  /* Parity error */
    }
    return 0;
}

Example 3: Efficient Flag Sets

Using a single integer as a set of boolean flags is one of the most powerful embedded C patterns. Instead of creating separate bool variables (each consuming at least one byte), you store up to 32 independent flags in a single uint32_t. This saves RAM, enables atomic flag operations, and makes it trivial to test multiple conditions at once with a single mask comparison.

/* Using bitwise flags for efficient state tracking
 *
 * Instead of multiple booleans (1 byte each),
 * use a single integer where each bit is a flag.
 * Saves RAM and enables atomic operations.
 */

/* System event flags */
#define EVT_BUTTON_PRESS    (1u << 0)
#define EVT_TIMER_TICK      (1u << 1)
#define EVT_UART_RX         (1u << 2)
#define EVT_ADC_COMPLETE    (1u << 3)
#define EVT_I2C_DONE        (1u << 4)
#define EVT_SPI_DONE        (1u << 5)
#define EVT_ERROR           (1u << 6)
#define EVT_WATCHDOG        (1u << 7)

static volatile uint32_t event_flags = 0;

/* ISRs set flags (atomic on ARM Cortex-M for single-bit ops) */
void USART1_IRQHandler(void) {
    /* ... handle RX ... */
    event_flags |= EVT_UART_RX;
}

void TIM2_IRQHandler_b(void) {
    event_flags |= EVT_TIMER_TICK;
}

/* Main loop processes and clears flags */
void main_loop(void) {
    while (1) {
        uint32_t flags = event_flags;  /* Snapshot */

        if (flags & EVT_BUTTON_PRESS) {
            event_flags &= ~EVT_BUTTON_PRESS;  /* Clear */
            handle_button();
        }

        if (flags & EVT_UART_RX) {
            event_flags &= ~EVT_UART_RX;
            handle_uart();
        }

        if (flags & EVT_TIMER_TICK) {
            event_flags &= ~EVT_TIMER_TICK;
            handle_tick();
        }

        /* Check multiple flags at once */
        if (flags & (EVT_I2C_DONE | EVT_SPI_DONE)) {
            /* At least one communication completed */
            event_flags &= ~(EVT_I2C_DONE | EVT_SPI_DONE);
            handle_comm_complete();
        }

        /* Check if ANY event is pending */
        if (event_flags == 0) {
            __WFI();  /* No events: sleep until next interrupt */
        }
    }
}

Common Bitwise Tricks

Over decades of embedded C programming, a collection of bitwise idioms has emerged — concise patterns that solve common problems in a single expression. These tricks appear frequently in production firmware, driver code, and RTOS kernels. Learning to recognize them makes reading other people’s embedded code much easier.

/* Useful bitwise tricks for embedded programming */

/* Check if a number is a power of 2 */
static inline uint8_t is_power_of_2(uint32_t n) {
    return n && !(n & (n - 1));
    /* 8 = 1000, 7 = 0111, 8 & 7 = 0000 → is power of 2
     * 6 = 0110, 5 = 0101, 6 & 5 = 0100 → not power of 2 */
}

/* Round up to next power of 2 */
static inline 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;
}

/* Count leading zeros (useful for log2, priority encoders) */
static inline uint8_t clz(uint32_t x) {
    #ifdef __GNUC__
    return x ? __builtin_clz(x) : 32;
    #else
    uint8_t n = 0;
    if (x == 0) return 32;
    if (!(x & 0xFFFF0000)) { n += 16; x <<= 16; }
    if (!(x & 0xFF000000)) { n += 8;  x <<= 8; }
    if (!(x & 0xF0000000)) { n += 4;  x <<= 4; }
    if (!(x & 0xC0000000)) { n += 2;  x <<= 2; }
    if (!(x & 0x80000000)) { n += 1; }
    return n;
    #endif
}

/* Extract individual bytes from a 32-bit word */
#define BYTE0(x)  ((uint8_t)((x) & 0xFF))
#define BYTE1(x)  ((uint8_t)(((x) >> 8) & 0xFF))
#define BYTE2(x)  ((uint8_t)(((x) >> 16) & 0xFF))
#define BYTE3(x)  ((uint8_t)(((x) >> 24) & 0xFF))

/* Build a 32-bit word from 4 bytes */
#define MAKE_U32(b3, b2, b1, b0)  \
    ((uint32_t)(b3) << 24 | (uint32_t)(b2) << 16 | \
     (uint32_t)(b1) << 8  | (uint32_t)(b0))

Usage Scenarios Summary

To recap where these techniques fit in real embedded work:

  • Bit manipulation: Control individual bits within hardware registers, flags, or settings without disturbing neighboring bits.
  • Efficient storage: Pack multiple Boolean flags or status values into a single variable to minimize RAM usage.
  • Networking and low-level protocols: Manipulate packet headers, perform checksums, and encode/decode multi-bit fields in custom protocols.
  • Graphics and image processing: Apply masks, extract color components, or perform bitwise image transforms on pixel data.
  • Embedded systems: Configure peripheral registers, read status flags, and implement ISR-safe event systems — the core daily use case.

Video Tutorials

Related Articles

Related on this site

Leave a Reply

Your email address will not be published. Required fields are marked *