Bit Masks and Register Manipulation
Microcontroller peripherals are controlled through bits in registers. A single wrong shift can enable the wrong interrupt, clear a fault flag, or drive the wrong pin. Embedded C code must manipulate those bits precisely without disturbing unrelated fields.
Learning Objectives
By the end of this lesson, you should be able to:
- create masks with shifts;
- set, clear, toggle, and test individual bits;
- update multi-bit fields safely;
- explain read-modify-write hazards around hardware registers.
Single-Bit Masks
#include <stdint.h>
#define BIT_U32(n) (UINT32_C(1) << (n))
#define GPIO_ENABLE BIT_U32(0)
#define GPIO_OUTPUT BIT_U32(1)
Set a bit:
reg |= GPIO_ENABLE;
Clear a bit:
reg &= ~GPIO_ENABLE;
Toggle a bit:
reg ^= GPIO_ENABLE;
Test a bit:
bool enabled = (reg & GPIO_ENABLE) != 0u;
Use unsigned constants for bit operations. The expression 1 << 31 has type int on many systems and can invoke undefined behavior when the sign bit is reached. UINT32_C(1) << 31 is explicit.
Worked Single-Bit Example
If reg = 0b0000_1010 and GPIO_ENABLE = 0b0000_0001:
| Operation | Expression | Result |
|---|---|---|
| set bit 0 | reg |= GPIO_ENABLE |
0000_1011 |
| clear bit 1 | reg &= ~BIT_U32(1) |
0000_1001 |
| toggle bit 3 | reg ^= BIT_U32(3) |
0000_0001 |
| test bit 3 before toggle | (reg & BIT_U32(3)) != 0u |
true |
Multi-Bit Fields
#define TIMER_PRESCALE_SHIFT 4u
#define TIMER_PRESCALE_MASK (7u << TIMER_PRESCALE_SHIFT)
static inline uint32_t timer_prescale_field(uint32_t value)
{
return (value << TIMER_PRESCALE_SHIFT) & TIMER_PRESCALE_MASK;
}
Update only that field:
reg = (reg & ~TIMER_PRESCALE_MASK) | timer_prescale_field(3u);
The pattern is always:
- clear the old field with
reg & ~mask; - shift and mask the new value;
- combine the two with OR.
For a three-bit prescaler field at bits [6:4], value 3 becomes 0b011 << 4, so the register field receives 0x30.
Named Field Macros
For repeated fields, keep the shift and mask together:
#define FIELD_PREP(mask, shift, value) (((uint32_t)(value) << (shift)) & (mask))
#define FIELD_GET(mask, shift, value) (((uint32_t)(value) & (mask)) >> (shift))
#define UART_CR_PARITY_SHIFT 8u
#define UART_CR_PARITY_MASK (3u << UART_CR_PARITY_SHIFT)
#define UART_CR_PARITY_EVEN 2u
reg = (reg & ~UART_CR_PARITY_MASK) |
FIELD_PREP(UART_CR_PARITY_MASK, UART_CR_PARITY_SHIFT, UART_CR_PARITY_EVEN);
This avoids magic numbers in driver code.
Read-Modify-Write
Many operations read a register, modify some bits, then write it back.
This is not always safe. Some registers clear flags when written, some bits are write-only, and an interrupt can change the register between read and write.
Read-modify-write is acceptable only when the datasheet says all involved bits can be safely read and written back. Many MCUs provide dedicated set, clear, or bit set/reset registers so software can update outputs atomically.
Hardware Register Pointer Example
#include <stdint.h>
#define GPIO_BASE_ADDR 0x40020000u
#define GPIO_MODER (*(volatile uint32_t *)(GPIO_BASE_ADDR + 0x00u))
#define GPIO_ODR (*(volatile uint32_t *)(GPIO_BASE_ADDR + 0x14u))
void gpio_set_pin(uint32_t pin)
{
GPIO_ODR |= BIT_U32(pin);
}
The volatile qualifier tells the compiler that the value can change outside normal program flow.
This is still a read-modify-write operation. If another context can also update GPIO_ODR, prefer an atomic hardware register when available:
#define GPIO_BSRR (*(volatile uint32_t *)(GPIO_BASE_ADDR + 0x18u))
void gpio_set_pin_atomic(uint32_t pin)
{
GPIO_BSRR = BIT_U32(pin);
}
Safer Field Helper
static inline void reg_update_field(volatile uint32_t *reg,
uint32_t mask,
uint32_t value)
{
uint32_t current = *reg;
current &= ~mask;
current |= value & mask;
*reg = current;
}
Use helpers like this only after confirming the target register allows read-modify-write.
Register Access Checklist
Before writing driver code, classify each bit:
| Access type | Meaning | Safe code pattern |
|---|---|---|
| read/write | normal control bit | masked read-modify-write |
| read-only | hardware status | read and test only |
| write-only | command or FIFO | write exact value |
| write-one-to-clear | status flag clear | write only the flag mask |
| set/reset | atomic update | write set or reset register |
If the datasheet says "reserved bits must be kept at reset value", avoid writing arbitrary constants to the whole register.
Common Mistakes
- Writing
1 << 31with signedintinstead of1u. - Forgetting parentheses in macro definitions.
- Updating a field without clearing the old field first.
- Using read-modify-write on write-one-to-clear status registers.
- Forgetting
volatileon memory-mapped registers. - Shifting by the register width or more, such as
1u << 32on a 32-bit value. - Clearing reserved bits by assigning a whole register constant.
Summary
Register manipulation is controlled bit arithmetic. Use named masks, unsigned shifts, explicit field helpers, and the datasheet's register access rules. Most embedded bugs in this area come from changing one bit while accidentally disturbing another.