#035 – How to use std::bitset for EFFORTLESS bit manipulation introduced std::bitset and its single-bit methods — set, reset, flip, test. Those methods are useful but they only support one bit per call.
When you need to read or modify several bits at once, or when you’re working with raw integer types from a hardware register, you need a different tool.
Bit masks
Bit masks allows us manipulate specific bits, whilst leaving the others unchanged. This matters because embedded systems often pack many Boolean states into a single byte or word.
Every bit-mask operation is built from one of four bitwise operators. The mask itself is an integer with 1s in the positions you want to act on:
| Operation | Expression | Effect |
|---|---|---|
| Test | value & mask | Non-zero if any masked bit is set. |
| Set | `value | mask` |
| Clear | value & ~mask | Forces masked bits to 0. |
| Toggle | value ^ mask | Flips masked bits. |
The mask for a single bit at position n is 1u << n. For multiple bits, OR several single-bit masks together: (1u << 3) | (1u << 5) selects bits 3 and 5 in one expression.
The basic premise
- Construct the bit mask by bit shifting the bits into the desired positions.
- Apply the mask to a
std::bitset. You can either set, reset or toggle
Reading from a Battery Management System (BMS)
A Battery Management System (BMS) communicating over I²C typically returns its status as a packed 16-bit register. Each bit signals one condition: charging, fully charged, alarm raised, and so on.
#include <cstdint>
struct Status {
bool discharging;
bool fully_charged;
bool fully_discharged;
};
Status decodeStatus(uint16_t status_bits)
{
Status s{};
s.discharging = (status_bits >> 6) & 0x1;
s.fully_charged = (status_bits >> 5) & 0x1;
s.fully_discharged = (status_bits >> 4) & 0x1;
return s;
}
Each line shifts the bit of interest down to position 0, then masks with 0x1 to discard everything else. The result is always 0 or 1, which converts cleanly to bool.
A common alternative writes the same logic without the shift:
s.discharging = status_bits & (1u << 6);
This works, but with an important caveat: the result is 0 if the bit is clear, and 64 (i.e. 2^6) if the bit is set.
Writing: set, clear, and toggle
Reading is half the story. The other half is modifying bits in place.
Consider a program that stores flags in specific bit positions.
#include <cstdint>
constexpr uint16_t kCharging = 1u << 6;
constexpr uint16_t kFullyCharged = 1u << 5;
constexpr uint16_t kAlarmRaised = 1u << 13;
void demonstrate(uint16_t& status_bits)
{
// Set a single flag
status_bits |= kCharging;
// Set two flags at once
status_bits |= (kCharging | kAlarmRaised);
// Clear a flag (note the ~ to invert the mask)
status_bits &= ~kFullyCharged;
// Toggle a flag
status_bits ^= kAlarmRaised;
}
status_bits |= (kCharging | kAlarmRaised) reads almost as English — set the charging and alarm flags — and produces a single read-modify-write to the underlying registe
&= ~mask says AND the value with the inverse of the mask. This preserves every bit except the masked ones, which become 0.
Summary
Reach for bit masks when the work involves several bits at once or when you’re operating on raw integer types from a hardware register.
- Read with
&:(value >> n) & 0x1extracts bitn. - Write with
|,&~, and^: set, clear, and toggle respectively. - Bit masks let you act on several bits in a single operation — something
std::bitset‘s indexed methods cannot.