Embedded

Bit manipulation in embedded C

By CourseTron Team ·  2 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: In embedded C you manipulate individual bits with the bitwise operators and shift masks — reg |= (1u< to set, reg &= ~(1u< to clear, reg ^= (1u< to toggle — and you prefer explicit masks over C struct bitfields because bitfields have implementation-defined layout that silently breaks hardware register maps.

    Every firmware engineer eventually lives inside a datasheet, poking single bits into a peripheral control register to turn on a UART, enable an interrupt, or kick off an ADC conversion. Getting this right — and understanding exactly what the compiler emits — is one of the most reliably tested skills in embedded interviews and one of the most common sources of field bugs. This guide walks through the mechanics, the canonical idioms, and the pitfalls that bite even experienced developers.

    The four primitive operations

    A mask is just an integer whose set bits mark the positions you care about. The classic single-bit mask for bit n is (1u << n). From there everything is composition of four bitwise operators:

    • Set a bit without disturbing the others: reg |= (1u << n); — OR forces the target bit to 1.
    • Clear a bit: reg &= ~(1u << n); — AND with the inverted mask forces it to 0.
    • Toggle a bit: reg ^= (1u << n); — XOR flips it.
    • Test a bit: if (reg & (1u << n)) — AND isolates it; non-zero means set.

    Always suffix your literal with u. 1 << 31 shifts a signed int into the sign bit — undefined behaviour in C. 1u << 31 is well-defined. For 64-bit positions use 1ull << n or UINT32_C(1) << n.

    Set and clear on an 8-bit register (0x2D)originalb7b6b5b4b3b2b1b000101101reg |= (1u<<1)00101111SET bit 1: 0x2Freg &= ~(1u<<5)00001101CLEAR bit 5: 0x0DEach op is applied to the original value. Orange = the changed bit; others are untouched.CourseTron · coursetron.com
    Set and clear act on one position; every other bit is untouched because OR-with-0 and AND-with-1 are identity operations.

    Multi-bit fields: shift, mask, write-back

    Peripherals rarely give you clean one-bit flags. A clock prescaler or a UART word-length selector lives in a field of 2–4 contiguous bits. The read-modify-write idiom is: build a field mask, clear the whole field, then OR in the shifted value.

    #define BAUD_POS   3u
    #define BAUD_MSK   (0x7u << BAUD_POS)   /* 3-bit field */
    
    uint32_t r = UART->CR;
    r &= ~BAUD_MSK;                      /* clear field  */
    r |= (speed_code << BAUD_POS) & BAUD_MSK; /* insert, clamped */
    UART->CR = r;                        /* single store */

    The final & BAUD_MSK clamps an out-of-range speed_code so a caller can never corrupt neighbouring bits. Vendor CMSIS headers for STM32, SiFive or Nordic parts ship exactly these _Pos/_Msk pairs for this reason.

    Field write-back: 3-bit field at bit 3 (mask 0x38)b7b6b5b4b3b2b1b0r = CR10101101r &= ~MSK10000101field clearedcode << 300010000code = 0b010r |= ...10010101CR = r (one store)Dashed box = field bits 5:3. Neighbouring bits keep their values throughout.CourseTron · coursetron.com
    Worked example of the read-modify-write idiom: clear the field with the inverted mask, OR in the shifted value, store once.
    A bit shifting left: 1u << n in motionbit5bit4bit3bit2bit1bit0The 1 starts at bit 0 and moves one position left per shift.Value after n shifts = 1u << n, so the bit lands in position n.CourseTron · coursetron.com
    Animated: the "1" walks from bit 0 toward the MSB — one position per shift — exactly what 1u< computes.

    Why C struct bitfields are a trap

    C lets you write struct { unsigned en:1; unsigned mode:3; } cr; and it looks like the perfect register map. Resist it for hardware. The C standard (and the Arm EABI) leaves three things implementation-defined:

    • Allocation order — whether en occupies the LSB or the MSB end of the storage unit depends on the compiler and the target's endianness.
    • Straddling and padding — the compiler may pad a field to the next unit rather than packing across a boundary.
    • Access width — the compiler may emit a byte, half-word, or word load/store. Many peripherals only tolerate 32-bit accesses; a byte write to a control register can be ignored or fault.

    The same bitfield struct compiled with GCC for a little-endian Cortex-M and for a big-endian PowerPC produces different bit orders. Register maps built on bitfields are a classic silent-portability bug — explicit _Msk masks behave identically everywhere.

    ApproachLayout guaranteed?Access width controlVerdict
    Explicit masks + volatile pointerYesFullUse for registers
    struct bitfieldsNo (impl-defined)NoAvoid for hardware
    Bitfields for internal packingWithin one compilerN/AOK for local state

    volatile, barriers, and the compiler

    A hardware register must be declared volatile so the compiler reloads it on every read and never caches or reorders the access away. A typical definition is #define UART ((volatile UART_t*)0x4000'4400u). Without volatile, a polling loop like while (!(SR & RXNE)); may be hoisted into an infinite loop because the compiler assumes memory never changes on its own. On Cortex-M you sometimes also need a __DMB() data memory barrier when a peripheral write must complete before a dependent one. Related territory is covered in our memory-mapped I/O and volatile and memory barriers articles.

    Practical idioms and gotchas

    • Atomicity: reg |= mask is read-modify-write — not atomic. If an ISR touches the same register, use a bit-banding alias (Cortex-M3/M4) or a hardware set/clear register pair (e.g. STM32 BSRR) that writes are genuinely atomic.
    • Counting set bits: __builtin_popcount(x) maps to a single instruction on Arm v7; the textbook x &= x-1 loop clears the lowest set bit each pass.
    • Lowest set bit isolation: x & -x gives you just the least-significant 1.
    • Static analysis: tools like Siemens' Polyspace or the MISRA C checkers in Synopsys Coverity flag signed shifts and undefined bit operations before they ship.

    Bit manipulation is also where RTL and firmware meet: the register map the firmware pokes is the same one verified in the digital-design flow. If you are building that hardware side, Avecas design services work across the RTL-to-GDS chain where these register semantics originate.

    Want to go from datasheet to working driver with confidence? CourseTron's Embedded C & Firmware track drills exactly these register-access patterns on real Cortex-M hardware. Explore it on our courses page.

    Frequently asked questions

    Why use 1u instead of 1 when shifting?

    Shifting a signed 1 into or past the sign bit (position 31 on a 32-bit int) is undefined behaviour in C. The unsigned literal 1u makes the shift well-defined for all positions and prevents sign-extension surprises when the value is later widened.

    Are C struct bitfields ever acceptable?

    Yes — for packing internal software state where you control the compiler and never rely on the bit order matching external hardware or a wire protocol. For memory-mapped registers and serialised formats, use explicit masks and shifts whose layout is guaranteed by the language.

    How do I set a bit atomically with respect to an interrupt?

    A plain reg |= mask is three operations and can be interrupted mid-sequence. Use a hardware atomic set/clear register (STM32 GPIOx->BSRR), bit-banding on Cortex-M3/M4, or disable the relevant interrupt around the access.

    What is the fastest way to count set bits?

    Use the compiler builtin __builtin_popcount(), which lowers to a single instruction on Arm v7-A and many DSPs. Where no instruction exists, the x &= (x-1) loop runs in exactly as many iterations as there are set bits.

    Advertisement