In short: In embedded C you manipulate individual bits with the bitwise operators and shift masks — reg |= (1u<reg &= ~(1u<reg ^= (1u<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.
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.
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
enoccupies 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.
| Approach | Layout guaranteed? | Access width control | Verdict |
|---|---|---|---|
Explicit masks + volatile pointer | Yes | Full | Use for registers |
struct bitfields | No (impl-defined) | No | Avoid for hardware |
| Bitfields for internal packing | Within one compiler | N/A | OK 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 |= maskis 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. STM32BSRR) that writes are genuinely atomic. - Counting set bits:
__builtin_popcount(x)maps to a single instruction on Arm v7; the textbookx &= x-1loop clears the lowest set bit each pass. - Lowest set bit isolation:
x & -xgives 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.