7 Bitwise Operations and Endianness
Chapter Scope (Avoiding Duplicate Deep Dives)
This chapter covers byte order and bit-level manipulation in implementation detail.
- Stay here for endianness, masks, shifts, and register operations.
- Use Data Representation Fundamentals for the broader conceptual map.
- Use Packet Structure and Framing for higher-level protocol framing.
7.1 Learning Objectives
By the end of this chapter, you will be able to:
- Differentiate endianness formats: Classify big-endian vs little-endian byte ordering and identify which IoT protocols use each format
- Apply bitwise operations: Implement AND, OR, XOR, NOT, and shift operations to manipulate individual bits in IoT sensor data
- Configure hardware registers: Select appropriate masks to set, clear, and toggle individual bits in microcontroller control registers
- Convert byte order for cross-platform communication: Implement network-to-host and host-to-network byte order conversions using
htonl,htons,ntohl, andntohs - Construct compact data payloads: Design bit-packed data structures that minimize LoRaWAN and BLE payload sizes for bandwidth-constrained IoT deployments
No-One-Left-Behind Bitwise Loop
- Start with one byte and label each bit position.
- Apply one mask operation and verify bit-by-bit.
- Repeat with a real register or packet field from a datasheet.
- Confirm behavior with a quick code test before scaling up.
For Beginners: Bitwise Operations
Bitwise operations let you work with individual bits – the tiny on/off switches that make up all computer data. Imagine a row of eight light switches: you can flip just one switch without touching the others. This is essential in IoT because sensors and microcontrollers use individual bits to represent status flags, settings, and compact data. Learning these operations is like learning to read the control panel of your IoT device at the most fundamental level.
Related Chapters, Products, and Tools
Foundation Topics:
- Data Representation Fundamentals - Overview and index
- Number Systems and Data Units - Binary, decimal, hexadecimal
- Text Encoding for IoT - ASCII, Unicode, and UTF-8
- Packet Structure and Framing - How data is packaged
Apply These Concepts:
- Sensor Circuits and Signals - Signal processing
- Encryption Architecture - Cryptographic data
- Prototyping Hardware - Microcontroller programming
Learning Hubs:
- Quiz Navigator - Test your understanding
- Simulation Playground - Interactive tools
7.2 Prerequisites
Before reading this chapter, you should understand:
- Binary and hexadecimal number systems from Number Systems and Data Units
- What bytes are and how they store data (bits 0-7, MSB/LSB)
7.3 Endianness and Byte Ordering
Key Concepts
- Byte order governs interoperability: A 16-bit or 32-bit value is only portable when both systems agree on which byte comes first.
- Network order is big-endian: TCP/IP and many wire protocols expect most-significant-byte first even when the MCU stores data little-endian.
- Masks isolate meaning: Bitwise AND keeps only the status flag or field you care about so packed bytes can be decoded safely.
- Shifts reposition fields: Left and right shifts move values into the correct bit positions before packing, unpacking, or scaling.
- Register writes must preserve neighbors:
|=,&= ~, and^=change one feature bit without clobbering the rest of a control register. - Packed payloads save real resources: Combining flags into bytes reduces airtime, battery drain, and network cost on constrained links.
- Named masks prevent silent bugs: Explicit bit positions, masks, and host/network conversions are easier to audit than raw magic numbers.
When storing multi-byte numbers (16-bit, 32-bit, 64-bit), the order of bytes in memory matters. This is called endianness.
7.3.1 Big-Endian vs Little-Endian
Example: The 32-bit hexadecimal number 0x12345678
Big-Endian (Most Significant Byte First):
- Used by: Network protocols (TCP/IP, Modbus), Java Virtual Machine
- Why “Big”? The biggest (most significant) byte comes first
- Memory layout: 0x12 at lowest address, 0x78 at highest address
- Human-readable: Matches how we write numbers left-to-right
Little-Endian (Least Significant Byte First):
- Used by: Intel x86, ARM Cortex-M (ESP32, STM32), Bluetooth LE
- Why “Little”? The littlest (least significant) byte comes first
- Memory layout: 0x78 at lowest address, 0x12 at highest address
- Performance advantage: Processor can read lower bytes first during arithmetic
7.3.2 Endianness in IoT Protocols
Network protocols use big-endian (also called “network byte order”):
// Sending 16-bit temperature value 1234 (0x04D2) over TCP/IP
uint16_t temp = 1234;
uint16_t temp_network = htons(temp); // Host TO Network Short
// Bytes sent: 0x04, 0xD2 (big-endian, regardless of CPU)Bluetooth LE uses little-endian:
// BLE GATT characteristic: heart rate 72 BPM (0x0048)
uint16_t heart_rate = 72;
// Bytes sent: 0x48, 0x00 (little-endian, LSB first)
Real-World Example: Multi-Sensor Gateway Data Aggregation
Scenario: A smart building gateway collects data from 50 sensors every minute. Each sensor sends a 32-bit timestamp (Unix epoch seconds).
Problem: Temperature sensors use ESP32 (little-endian ARM), but the central server expects network byte order (big-endian). Without conversion:
Sensor sends timestamp 1609459200 (Jan 1, 2021, 00:00:00 UTC):
- Correct big-endian bytes:
0x5F 0xEE 0x66 0x00(hex of 1609459200 = 0x5FEE6600) - ESP32 sends (WRONG):
0x00 0x66 0xEE 0x5F(little-endian, LSB first) - Server interprets as: 6,745,695 seconds (~78 days from epoch)
- Error: Over 50 years off! Data appears to be from March 1970 instead of January 2021
Impact:
- Historical analytics show impossible negative time deltas
- Energy usage graphs show 51 years of missing data
- Triggered false “clock drift” alerts across all 50 sensors
- Cost: 8 hours of debugging time ($800+ engineering cost)
Fix: Use htonl() (Host TO Network Long) function:
uint32_t timestamp = 1609459200;
uint32_t network_order = htonl(timestamp); // Converts to big-endian
client.write((uint8_t*)&network_order, 4); // Sends: 5F EE 66 00Prevention: Always use network byte order functions (htonl, htons, ntohl, ntohs) for multi-byte values in protocols.
Endianness Mismatch Bug
Real scenario: ESP32 (little-endian) sends sensor data to Python server expecting network byte order:
// ESP32 code (WRONG - sends little-endian)
uint32_t timestamp = 1609459200;
client.write((uint8_t*)×tamp, 4); // Sends: 00 66 EE 5F (LSB first)
// Python server receives
data = sock.recv(4)
timestamp = struct.unpack('>I', data)[0] # Expects big-endian
# Result: 6745695 (wrong!) instead of 1609459200Fix: Use explicit byte order conversion:
// ESP32 code (CORRECT)
uint32_t timestamp = 1609459200;
uint32_t timestamp_be = htonl(timestamp); // Convert to big-endian
client.write((uint8_t*)×tamp_be, 4); // Sends: 5F EE 66 00Practical rule: Always check protocol specifications for endianness. When in doubt, network protocols use big-endian.
Interactive: Endianness Byte Order Converter
Try different values to see how byte order changes between little-endian and big-endian:
Try it: Enter the timestamp from the example above (0x5FE0AD00) and see how the bytes are arranged differently in each format.
7.4 Bitwise Operations
Bitwise operations manipulate individual bits within bytes, essential for: - Setting/clearing hardware register flags - Packing multiple boolean values into single bytes - Extracting protocol header fields - Efficient sensor data encoding
7.4.1 Basic Bitwise Operators
Truth tables:
| A | B | A AND B | A OR B | A XOR B |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 | 0 |
7.4.2 Bit Shifts
Left shift (<<): Moves bits left, fills with zeros (multiplies by 2^n)
0b00001101 << 2 = 0b00110100
(13 << 2 = 52 = 13 x 4)
Right shift (>>): Moves bits right, fills with sign bit or zero (divides by 2^n)
0b00001100 >> 2 = 0b00000011
(12 >> 2 = 3 = 12 / 4)
Interactive: Bitwise Operation Explorer
Experiment with different bitwise operations:
How to use: Adjust the sliders to see how different bitwise operations affect the binary representation in real-time.
7.4.3 Common Bit Manipulation Patterns
Setting a bit (turn ON):
// Set bit 3 of register to 1 (enable I2C)
register |= (1 << 3); // OR with mask 0b00001000Clearing a bit (turn OFF):
// Clear bit 5 of status byte (disable LED)
status &= ~(1 << 5); // AND with inverted mask 0b11011111Toggling a bit (flip state):
// Toggle bit 7 (invert MSB)
value ^= (1 << 7); // XOR with mask 0b10000000Checking if a bit is set:
// Check if error flag (bit 2) is set
if (status & (1 << 2)) {
printf("Error detected!\n");
}Extracting multi-bit fields:
// Extract bits 4-6 from sensor reading
// Example: 0b11010110, bits 4-6 = 101 (value 5)
uint8_t sensor = 0xD6; // 11010110
uint8_t field = (sensor >> 4) & 0x07; // Shift right 4, mask with 0b00000111
// Result: 0b00000101 = 5
Check Your Understanding: Match the Bitwise Operation to Its Purpose
7.4.4 Real IoT Example: Sensor Status Byte
Many sensors pack multiple flags into a single status byte:
// BME280 sensor status register (0xF3)
// Bit 7-4: Reserved
// Bit 3: measuring (1 = conversion running)
// Bit 2-1: Reserved
// Bit 0: im_update (1 = NVM data copying)
uint8_t status = i2c_read(0xF3);
// Check if sensor is busy
bool is_measuring = status & (1 << 3);
bool is_updating = status & (1 << 0);
if (is_measuring || is_updating) {
delay(10); // Wait before reading data
}
// More efficient: check both bits at once
if (status & 0b00001001) { // Mask for bits 3 and 0
delay(10);
}
Real-World Example: LoRaWAN Device Status Byte
Scenario: A battery-powered agricultural soil sensor transmits data via LoRaWAN (51-byte payload limit). The device needs to report multiple status flags with every reading.
Traditional approach (wasteful):
bool temp_sensor_ok = true; // 1 byte
bool moisture_ok = true; // 1 byte
bool battery_low = false; // 1 byte
bool calibration_needed = false; // 1 byte
bool valve_open = true; // 1 byte
bool network_joined = true; // 1 byte
bool gps_fix = false; // 1 byte
bool rain_detected = true; // 1 byte
// Total: 8 bytes for status flagsOptimized approach (bitwise packing):
// Pack 8 status flags into 1 byte
uint8_t status = 0; // Start with all flags cleared
// Set individual flags using bitwise OR
status |= (1 << 0); // Bit 0: temp_sensor_ok = 1
status |= (1 << 1); // Bit 1: moisture_ok = 1
// Bit 2: battery_low = 0 (keep cleared)
// Bit 3: calibration_needed = 0
status |= (1 << 4); // Bit 4: valve_open = 1
status |= (1 << 5); // Bit 5: network_joined = 1
// Bit 6: gps_fix = 0
status |= (1 << 7); // Bit 7: rain_detected = 1
// Result: status = 0b10110011 = 0xB3
// Total: 1 byte for all status flagsSavings:
- Payload size: 8 bytes to 1 byte (87.5% reduction)
- Energy: Fewer bytes = faster transmission = 3.36 mAh saved per day (7 fewer bytes × 96 transmissions/day × 5 µA/byte × 250 ms airtime)
- Battery life: ~7 extra days per year on a 2,000 mAh battery
- Cost: $2 battery replacement avoided annually per device (×1,000 devices = $2,000/year savings)
Reading flags:
if (status & (1 << 2)) {
printf("Battery low - replace soon\n");
}Why this matters at scale:
For a LoRaWAN network with 1,000 agricultural sensors transmitting every 15 minutes (96 msgs/day):
Bandwidth savings:
- Unpacked (8 bytes status): 1,000 × 96 × 365 × 8 = 280.32 GB/year
- Packed (1 byte status): 1,000 × 96 × 365 × 1 = 35.04 GB/year
- Savings: 245.28 GB/year (87.5% reduction)
- At $0.001/KB gateway costs: $245/year saved
Energy and maintenance:
- 7 bytes × 5 µA/byte × 250 ms × 96 msgs/day = 3.36 mAh/day per device
- Battery life extension: ~7 days/year per device
- Avoided replacements: 1,000 devices × $25/visit = $25,000/year savings
Why bitwise operations excel in IoT:
- Fast: Single CPU instruction (2-4 cycles)
- Low power: No multiplication/division operations
- Compact: Pack 8 boolean flags into 1 byte instead of 8 bytes
7.4.5 Python Struct Module for Binary Data
When IoT devices communicate with Python servers/scripts, use the struct module:
import struct
# Pack 32-bit integer as big-endian (network byte order)
timestamp = 1609459200
packed = struct.pack('>I', timestamp) # '>' = big-endian, 'I' = uint32
# Result: b'\x5f\xee\x66\x00'
# Pack multiple values into binary message
# Format: '>H f B' = big-endian, uint16, float32, uint8
sensor_id = 42
temperature = 23.5
status = 0b10010001
message = struct.pack('>H f B', sensor_id, temperature, status)
# Result: 7 bytes (2 + 4 + 1) - Python struct doesn't add padding
# Unpack received binary data
data = b'\x00\x2a\x41\xbc\x00\x00\x91'
sensor_id, temp, status = struct.unpack('>H f B', data)
print(f"Sensor {sensor_id}: {temp}C, Status: {status:08b}")7.5 Knowledge Check
Order the Steps: Decoding a Multi-Byte Sensor Value
Quiz 1: IoT Memory Address Decoding
Scenario: You’re programming an ESP32 microcontroller for a smart agriculture sensor. The datasheet shows the I2C peripheral control register at memory address 0x3FF53000. During debugging, you need to set bit 7 (the I2C enable bit) of the byte at offset 0x10 from this base address.
Think about:
- What is the complete memory address in hexadecimal where you’ll write? (Base + offset)
- What binary value should you write to enable I2C while keeping other bits unchanged? (Use bitwise OR)
- If you read back
0x8Ffrom that address, is I2C successfully enabled?
Key Insights:
- Address calculation: 0x3FF53000 + 0x10 = 0x3FF53010 (hex addition)
- Enable bit 7: Write
0x80(binary10000000) using bitwise OR to preserve other bits - Verification:
0x8F=10001111in binary - bit 7 is 1, so yes, I2C is enabled! - Real impact: Incorrect bit manipulation could disable other peripherals or crash the sensor, requiring a field technician visit costing $200+ per device
This is why every IoT developer must master hexadecimal and bitwise operations - a single bit error can mean the difference between a working $50 sensor and a $250 warranty replacement.
Scenario 1: Debugging a Sensor Reading
You’re debugging a temperature sensor connected to an ESP32. The serial monitor shows: Received bytes: 01 90. The sensor uses 16-bit big-endian format where the value represents temperature in tenths of a degree Celsius.
Question: What is the actual temperature reading?
Scenario 1 Answer
Temperature: 40.0C
Step-by-step solution:
- Combine bytes (big-endian): 0x01 is MSB, 0x90 is LSB
- Hexadecimal value: 0x0190
- Convert to decimal: 0x0190 = 1 x 256 + 9 x 16 + 0 = 256 + 144 = 400
- Apply scaling: 400 tenths = 40.0C
Key insight: Big-endian means the most significant byte comes first. If this were little-endian (like Intel/ARM), you’d read it as 0x9001 = 36,865 tenths = 3686.5C (obviously wrong!). Always check the datasheet for byte order.
Scenario 2: Packing Sensor Status Flags
You’re designing a battery-powered environmental sensor that needs to transmit 6 status flags over LoRaWAN (where every byte matters):
- Bit 0: Temperature sensor OK
- Bit 1: Humidity sensor OK
- Bit 2: Battery low warning
- Bit 3: Calibration needed
- Bit 4: Wi-Fi connected
- Bit 5: SD card present
Question: If the temperature sensor is OK, humidity sensor is OK, battery is low, and Wi-Fi is connected, what single byte value should you transmit?
Scenario 2 Answer
Byte value: 0x17 (decimal 23, binary 00010111)
Step-by-step solution:
Identify set bits:
- Bit 0: Temperature OK = 1
- Bit 1: Humidity OK = 1
- Bit 2: Battery low = 1
- Bit 3: Calibration needed = 0
- Bit 4: Wi-Fi connected = 1
- Bit 5: SD card present = 0
Build binary value: 00010111 (reading right to left: bits 0,1,2,4 are set)
Convert to hex: 0001 0111 = 0x17
Code implementation:
uint8_t status = 0; status |= (1 << 0); // Temp OK status |= (1 << 1); // Humidity OK status |= (1 << 2); // Battery low status |= (1 << 4); // Wi-Fi connected // Result: status = 0x17
Key insight: Bitwise packing saves 5 bytes compared to sending 6 separate boolean bytes. Over thousands of transmissions, this dramatically extends battery life.
Scenario 3: Cross-Platform Communication
Your ESP32 sensor (little-endian ARM) sends a 32-bit Unix timestamp to a cloud server. The timestamp value is 1704067200 (January 1, 2024, 00:00:00 UTC).
Question:
- What bytes will the ESP32 send if you just write the raw memory bytes (wrong approach)?
- What bytes should be sent for correct network byte order (big-endian)?
- What C function converts between these formats?
Scenario 3 Answer
1. Raw little-endian bytes (WRONG):
First, convert 1704067200 to hexadecimal: - 1704067200 = 0x65920080
Little-endian stores LSB first: 80 00 92 65
2. Network byte order (CORRECT):
Big-endian stores MSB first: 65 92 00 80
3. Conversion function:
#include <arpa/inet.h> // or <netinet/in.h>
uint32_t timestamp = 1704067200;
uint32_t network_order = htonl(timestamp); // Host TO Network LongWhat happens with wrong byte order:
- Server receives: 80 00 92 65 (little-endian)
- Server interprets as big-endian: 0x80009265 = 2,147,521,125
- Wrong date: January 2038 instead of January 2024!
Key insight: Always use htonl() (32-bit), htons() (16-bit), ntohl(), and ntohs() when sending multi-byte values over networks. This ensures correct communication regardless of processor architecture.
Scenario 4: Reading a Sensor Datasheet
You’re reading the datasheet for an I2C accelerometer. It says: - “X-axis acceleration is stored in registers 0x32 (low byte) and 0x33 (high byte)” - “Resolution: 10-bit, range: +/-2g, left-justified in 16-bit format”
You read register 0x32 = 0x40 and register 0x33 = 0x01.
Question: What is the actual acceleration value in g?
Scenario 4 Answer
Acceleration: +0.0195g (approx 0.02g)
Step-by-step solution:
- Combine bytes (little-endian since low byte is at lower address):
- Low byte 0x32 = 0x40
- High byte 0x33 = 0x01
- Combined 16-bit value: 0x0140
- Handle left-justification:
- “Left-justified in 16-bit” means the 10-bit value occupies the upper 10 bits
- Shift right by 6 to get the actual 10-bit value: 0x0140 >> 6 = 5
- Interpret as signed value:
- 10-bit range: -512 to +511 (two’s complement)
- Value 5 is positive (MSB = 0)
- Convert to g:
- +/-2g range means: -2g to +2g over 1024 levels
- Resolution: 4g / 1024 = 0.00390625 g/LSB
- Acceleration: 5 x 0.00390625 = 0.0195g (or about 0.02g)
Alternative calculation:
- Full scale: +/-2g = 4g total range
- 10-bit resolution: 2^10 = 1024 levels
- Value of 5: (5/512) x 2g = 0.0195g
Key insight: Datasheets often describe “left-justified” or “right-justified” data. Left-justified means you need to shift right to extract the actual value. Always check the bit layout diagram in the datasheet!
Knowledge Check: Bitwise Operations Quick Check
Concept: Using bitwise operations for hardware control.
7.6 Visual Reference Gallery
Packet Encapsulation Layers (Alternative View)
Each network layer adds its own header to the data payload, creating a layered encapsulation structure. This visualization shows how a small sensor reading (perhaps 4 bytes) grows as it passes through the protocol stack: application layer adds topic/QoS information, transport adds port numbers and checksums, network adds IP addresses, and data link adds MAC addresses and frame delimiters. Understanding encapsulation helps explain why LoRaWAN’s 13-byte overhead can dwarf a 2-byte temperature reading.
Worked Example: Decoding a Sensor Status Register
Scenario: You’re integrating the BME680 environmental sensor (temperature, humidity, pressure, gas). The datasheet shows a status register at address 0x1D with the following bit layout:
Bit 7: gas_measuring (1 = gas measurement in progress)
Bit 6: measuring (1 = conversion in progress)
Bit 5: Reserved
Bit 4: new_data (1 = new data available)
Bit 3: gas_valid (1 = gas measurement valid)
Bits 2-0: Reserved
You read the register via I2C and get: 0x98 (binary: 10011000)
Question: What is the sensor’s current state?
7.6.1 Step 1: Convert Hex to Binary
0x98 = 10011000 (binary)
7.6.2 Step 2: Extract Each Bit Using Masks
Bit 7 (gas_measuring):
uint8_t status = 0x98;
bool gas_measuring = (status & (1 << 7)) != 0; // Mask: 0b10000000 = 0x80
// Result: 0x98 & 0x80 = 0x80 (non-zero) → trueBit 6 (measuring):
bool measuring = (status & (1 << 6)) != 0; // Mask: 0b01000000 = 0x40
// Result: 0x98 & 0x40 = 0x00 (zero) → falseBit 4 (new_data):
bool new_data = (status & (1 << 4)) != 0; // Mask: 0b00010000 = 0x10
// Result: 0x98 & 0x10 = 0x10 (non-zero) → trueBit 3 (gas_valid):
bool gas_valid = (status & (1 << 3)) != 0; // Mask: 0b00001000 = 0x08
// Result: 0x98 & 0x08 = 0x08 (non-zero) → true7.6.3 Step 3: Interpret the Status
| Flag | Value | Meaning |
|---|---|---|
| gas_measuring | true | Gas measurement in progress |
| measuring | false | NOT doing temperature/pressure conversion |
| new_data | true | New data is available to read |
| gas_valid | true | Gas measurement is valid |
Interpretation: The sensor has completed a measurement cycle, new data is ready, and the gas reading is valid. You should: 1. Read the data registers now (before next conversion overwrites them) 2. Wait for gas_measuring to become 0 before triggering next measurement
7.6.4 Step 4: Practical Code
#include <Wire.h>
#define BME680_ADDR 0x76
#define BME680_STATUS_REG 0x1D
void check_sensor_status() {
Wire.beginTransmission(BME680_ADDR);
Wire.write(BME680_STATUS_REG);
Wire.endTransmission();
Wire.requestFrom(BME680_ADDR, 1);
uint8_t status = Wire.read();
// Extract individual flags
bool gas_measuring = status & 0x80; // Bit 7
bool measuring = status & 0x40; // Bit 6
bool new_data = status & 0x10; // Bit 4
bool gas_valid = status & 0x08; // Bit 3
Serial.print("Status: 0x");
Serial.print(status, HEX);
Serial.print(" | Gas measuring: ");
Serial.print(gas_measuring ? "Yes" : "No");
Serial.print(" | New data: ");
Serial.print(new_data ? "Yes" : "No");
Serial.print(" | Gas valid: ");
Serial.println(gas_valid ? "Yes" : "No");
// Decision logic
if (new_data && !measuring && !gas_measuring) {
Serial.println("→ Ready to read sensor data!");
// read_sensor_data();
} else {
Serial.println("→ Sensor busy or no new data yet");
}
}7.6.5 Key Takeaways
- Always check the datasheet for the exact bit positions
- Use masks with AND (
&) to extract specific bits - Shift operators (
1 << n) create masks dynamically - Bitwise operations are fast - single CPU cycle vs multiple comparisons
- Meaningful names (like
gas_measuring) are clearer thanbit7
Real-world debugging tip: When a sensor isn’t working, print the status register in both hex and binary to see which flags are unexpected.
7.7 How It Works: Bit-Level Data Manipulation
Understanding how microcontrollers manipulate data at the bit level is fundamental to efficient IoT programming. Let’s walk through a complete example of reading, modifying, and transmitting sensor status.
Step 1: Sensor Reports 8 Status Flags
A BME680 environmental sensor has a status register with 8 boolean flags: - Bit 7: Gas measurement valid - Bit 6: Heater stable - Bit 5: Temperature valid - Bit 4: Pressure valid - Bit 3: Humidity valid - Bit 2: New data available - Bit 1: Measuring in progress - Bit 0: Reserved (always 0)
The sensor returns the status byte: 0b11111000 (hex: 0xF8)
Step 2: MCU Reads Status Byte via I2C
uint8_t status = read_i2c_register(BME680_STATUS_REG);
// status = 0b11111000 = 0xF8 = 248 decimalStep 3: Check Individual Flags with Bitwise AND
To check if “new data available” (bit 2) is set:
bool has_new_data = (status & 0b00000100) != 0; // AND with mask
// 0b11111000 & 0b00000100 = 0b00000000
// Result: false (bit 2 is NOT set in this status byte)Wait, that doesn’t look right. Let me recalculate: - Status: 0b11111000 (bits 7-3 are set) - Bit 2 mask: 0b00000100 - AND result: 0b00000000 (bit 2 is 0 in status) - Correct result: No new data available
Step 4: Wait for New Data (Poll Until Bit 2 Sets)
while (!(status & 0b00000100)) { // Loop until bit 2 is set
delay(10);
status = read_i2c_register(BME680_STATUS_REG);
}
// Eventually status becomes 0b11111100 (bit 2 now set)Step 5: Pack Multiple Sensors’ Status into One Byte
Your system has 4 sensors. Instead of sending 4 separate bytes, pack their “data ready” flags:
uint8_t multi_sensor_status = 0; // Start with 0b00000000
multi_sensor_status |= (sensor1_ready << 0); // Set bit 0 if sensor 1 ready
multi_sensor_status |= (sensor2_ready << 1); // Set bit 1 if sensor 2 ready
multi_sensor_status |= (sensor3_ready << 2); // Set bit 2 if sensor 3 ready
multi_sensor_status |= (sensor4_ready << 3); // Set bit 3 if sensor 4 ready
// Result: 0b00001101 if sensors 1,3,4 are ready (bits 0,2,3 set)Step 6: Transmit via LoRaWAN (1 Byte vs 4 Bytes)
Without bit packing: - 4 separate bytes: [1, 0, 1, 1] = 4 bytes
With bit packing: - Single byte: 0b00001101 = 1 byte - Savings: 75% bandwidth reduction
Step 7: Cloud Decodes the Packed Byte
status_byte = 0b00001101 # Received from sensor
sensor1_ready = (status_byte & 0b0001) != 0 # True
sensor2_ready = (status_byte & 0b0010) != 0 # False
sensor3_ready = (status_byte & 0b0100) != 0 # True
sensor4_ready = (status_byte & 0b1000) != 0 # TrueKey Insight: A single bitwise operation (&) extracts individual boolean flags from a packed byte. This is MUCH more efficient than sending multiple bytes over bandwidth-constrained networks.
Real-World Endianness Issue:
If you pack a 16-bit sensor ID and status byte into a 3-byte payload:
uint16_t sensor_id = 0x1234;
uint8_t status = 0xF8;
// Method 1: Little-endian (ARM/Intel default)
uint8_t payload[3];
payload[0] = sensor_id & 0xFF; // 0x34 (LSB first)
payload[1] = (sensor_id >> 8) & 0xFF; // 0x12
payload[2] = status; // 0xF8
// Transmitted: [0x34, 0x12, 0xF8]
// Method 2: Big-endian (network byte order)
payload[0] = (sensor_id >> 8) & 0xFF; // 0x12 (MSB first)
payload[1] = sensor_id & 0xFF; // 0x34
payload[2] = status; // 0xF8
// Transmitted: [0x12, 0x34, 0xF8]If sensor sends little-endian but cloud expects big-endian, sensor ID 0x1234 (4660 decimal) is decoded as 0x3412 (13330 decimal) – completely wrong!
What to Observe:
- Bitwise operations work on binary representation, not decimal values
- Packing flags into bytes is a fundamental IoT optimization
- Endianness matters when multi-byte values cross systems (MCU → cloud)
7.8 Incremental Example Set: Bitwise Operations in Practice
Scenario: A motion sensor has a status byte where bit 3 indicates “motion detected”. You need to check if motion was detected.
Given:
- Status register value:
0b01001100(hex: 0x4C, decimal: 76) - Motion detection flag: bit 3
Task: Write code to check if motion was detected.
Beginner Solution (Step-by-Step):
uint8_t status = 0b01001100; // Read from sensor
// Step 1: Create a mask for bit 3
uint8_t motion_mask = 0b00001000; // Only bit 3 is set
// Step 2: AND the status with the mask
uint8_t result = status & motion_mask;
// 0b01001100 & 0b00001000 = 0b00001000
// Step 3: Check if result is non-zero
if (result != 0) {
Serial.println("Motion detected!");
} else {
Serial.println("No motion.");
}Visual Breakdown:
Status: 0 1 0 0 1 1 0 0 (0x4C)
Mask: 0 0 0 0 1 0 0 0 (0x08)
---------------
AND result: 0 0 0 0 1 0 0 0 (0x08 = non-zero, so motion detected!)
Beginner Pattern:
- Create a mask with only the target bit set
- AND the value with the mask
- Check if result is non-zero
Common Beginner Mistake:
// WRONG: Checking if result == mask
if ((status & motion_mask) == motion_mask) { // This works, but...
// More complex than needed
}
// CORRECT: Just check if non-zero
if (status & motion_mask) { // Simpler!
// Bit is set
}Scenario: You’re configuring a sensor’s control register. You need to: - Enable bits 2, 4, and 7 (turn on features) - Disable bits 1 and 5 (turn off features) - Leave other bits unchanged
Given:
- Current control register:
0b10010110(0x96) - Enable: bits 2, 4, 7
- Disable: bits 1, 5
Intermediate Solution:
uint8_t control = 0b10010110; // Current state
// Step 1: Enable bits 2, 4, 7 using OR
control |= 0b10010100; // Set these bits to 1
// 0b10010110 | 0b10010100 = 0b10010110 (bits already set, no change yet)
// Step 2: Clear bits 1, 5 using AND with inverted mask
control &= ~0b00100010; // Clear these bits
// ~0b00100010 = 0b11011101
// 0b10010110 & 0b11011101 = 0b10010100
// Final: 0b10010100 (bits 1,5 cleared, bits 2,4,7 set)Visual Breakdown:
Original: 1 0 0 1 0 1 1 0 (0x96)
Enable mask: 1 0 0 1 0 1 0 0 (OR with this)
After OR: 1 0 0 1 0 1 1 0 (no change, bits already had those values)
Clear mask: 0 0 1 0 0 0 1 0 (NOT this)
Inverted: 1 1 0 1 1 1 0 1 (AND with inverted)
After AND: 1 0 0 1 0 1 0 0 (bits 1,5 now cleared)
^ ^ ^ ^
7 6 2 1 (bit numbers)
Intermediate Pattern:
- Use
|=(OR-assign) to set bits to 1 - Use
&= ~(AND-assign with inverted mask) to clear bits to 0 - Combine operations when you need to both set and clear
Why Invert the Mask for Clearing?
- To clear bit 5, you want all OTHER bits to stay the same
- AND with 1 preserves bits, AND with 0 clears them
~0b00100010gives0b11011101– all 1s except the bits you want to clear
Scenario: You’re designing a LoRaWAN payload (11 bytes max) for a weather station that reports: - Wind direction (0-359 degrees) – needs 9 bits (2^9 = 512 max) - Wind speed (0-50 m/s, 0.1 m/s precision) – needs 9 bits (0-500) - Temperature (-40 to +60 C, 0.1 C precision) – needs 10 bits (0-1000 after offset) - Humidity (0-100%, 0.5% precision) – needs 8 bits (0-200) - Battery voltage (2.0-4.2V, 0.01V precision) – needs 8 bits (0-220)
Total bits needed: 9 + 9 + 10 + 8 + 8 = 44 bits = 5.5 bytes (round up to 6 bytes)
Advanced Solution (Bit-Field Packing):
// Pack 5 sensor fields into 6 bytes using bit-field packing
void pack_weather_data(uint8_t *buf,
uint16_t wind_dir, // 0-359 (9 bits)
uint16_t wind_spd_x10, // 0-500 (9 bits)
uint16_t temp_x10_off, // 0-1000 (10 bits, (temp+40)*10)
uint8_t hum_x2, // 0-200 (8 bits)
uint8_t batt_x100) // 0-220 (8 bits)
{
buf[0] = (wind_dir >> 1) & 0xFF; // dir[8:1]
buf[1] = ((wind_dir & 0x01) << 7) // dir[0]
| ((wind_spd_x10 >> 2) & 0x7F); // spd[8:2]
buf[2] = ((wind_spd_x10 & 0x03) << 6) // spd[1:0]
| ((temp_x10_off >> 4) & 0x3F); // temp[9:4]
buf[3] = ((temp_x10_off & 0x0F) << 4) // temp[3:0]
| ((hum_x2 >> 4) & 0x0F); // hum[7:4]
buf[4] = ((hum_x2 & 0x0F) << 4) // hum[3:0]
| ((batt_x100 >> 4) & 0x0F); // bat[7:4]
buf[5] = (batt_x100 & 0x0F) << 4; // bat[3:0]
}
// Result: 44 bits of data in 6 bytes vs 35+ bytes as JSON textExample Encoding:
Input values: - Wind direction: 247 degrees = 0b011110111 (9 bits) - Wind speed: 12.5 m/s = 125 (×10) = 0b001111101 (9 bits) - Temperature: 23.7 C = (23.7+40)×10 = 637 = 0b1001111101 (10 bits) - Humidity: 68.5% = 137 (×2) = 0b10001001 (8 bits) - Battery: 3.24V = (324-200) = 124 = 0b01111100 (8 bits)
Packed binary (6 bytes):
Byte 0: 01111011 (wind_dir >> 1)
Byte 1: 10011111 (wind_dir[0] + wind_speed[8:2])
Byte 2: 01100111 (wind_speed[1:0] + temp[9:4])
Byte 3: 11011000 (temp[3:0] + humidity[7:4])
Byte 4: 10010111 (humidity[3:0] + battery[7:4])
Byte 5: 11000000 (battery[3:0] left-aligned)
Hex: 0x7B 0x9F 0x67 0xD8 0x97 0xC0 = 6 bytes
Comparison with JSON:
{"dir":247,"spd":12.5,"tmp":23.7,"hum":68.5,"bat":3.24}= 53 bytes (8.8× larger!)
Advanced Patterns:
- Use bit shifts and masks to pack fields that don’t align to byte boundaries
- Document the bit layout clearly (essential for debugging)
- Validate field ranges BEFORE packing (overflow causes silent corruption)
- Use the same bit order in pack/unpack (MSB-first is conventional)
When to Use Advanced Bit-Packing:
- Extreme bandwidth constraints (Sigfox 12-byte limit, LoRaWAN SF12 51-byte limit)
- Ultra-low-power devices (transmission energy >> CPU energy, so complex packing is worth it)
- High message volume (1M+ messages/day, where byte savings multiply dramatically)
When NOT to Use:
- WiFi/Ethernet (bandwidth is cheap, debugging difficulty isn’t worth it)
- Prototyping (use JSON until you have proven bandwidth constraints)
- When CBOR or Protobuf provide sufficient compression with better tooling
Common Pitfalls
1. Sending Raw Memory Bytes Across Mixed-Endianness Systems
Writing a uint16_t or uint32_t buffer directly to the network works only when both sides already agree on byte order. The safe default is to convert multi-byte values explicitly with htons(), htonl(), ntohs(), or ntohl() before they cross MCU, gateway, server, or file boundaries.
2. Overwriting Neighboring Register Bits
Assigning a new byte to a control register often clears unrelated enable bits, interrupt flags, or mode settings. Use named masks plus |=, &= ~, and read-modify-write patterns so one feature bit changes without destabilizing the rest of the peripheral.
3. Masking or Shifting the Wrong Field Width
Bit-packed payloads fail silently when you forget sign extension, use the wrong shift distance, or decode left-justified sensor data as if it were right-justified. Verify each field with a worked example from the datasheet and test the exact boundary values that should fit in the allotted bits.
7.9 Summary
Endianness and bitwise operations are essential for IoT programming:
- Endianness: Big-endian (network byte order, MSB first) vs little-endian (Intel/ARM, LSB first)
- Conversion Functions:
htonl(),htons(),ntohl(),ntohs()for cross-platform communication - Bitwise AND: Extract bits, check flags, mask values
- Bitwise OR: Set bits, enable features, combine flags
- Bitwise XOR: Toggle bits, simple checksums
- Bit Shifts: Pack/unpack data, multiply/divide by powers of 2
Key Takeaways:
- Always check protocol specs for endianness - network protocols use big-endian
- Bitwise operations are fast (single CPU instruction) and memory-efficient
- Pack multiple boolean flags into single bytes to save bandwidth and battery
- Use Python’s
structmodule for binary data parsing in server-side code - A single bit error can cause 51-year timestamp errors or crash sensors
7.10 What’s Next
Now that you can apply bitwise operations and convert between byte orders, explore how these skills connect to broader IoT topics:
| Next Chapter | What You Will Explore |
|---|---|
| Packet Structure and Framing | Discover how bytes are packaged into protocol frames for network transmission |
| Data Formats for IoT | Compare JSON, CBOR, and Protocol Buffers for IoT messaging efficiency |
| Data Representation Fundamentals | Return to the complete data representation topic map |
| Sensor Circuits and Signals | Apply register manipulation skills to real sensor hardware interfaces |
| Prototyping Hardware | Practice bitwise operations on ESP32 and Arduino microcontrollers |
For Kids: Meet the Sensor Squad!
Sammy the Sensor is confused. “I have eight things to report: Am I warm? Am I wet? Is it bright? Is it windy? But I can only send ONE byte!”
Max the Microcontroller smiles. “No problem! Think of a byte like a row of eight light switches. Each switch is ON or OFF. We can pack all eight answers into one byte!”
Max draws a picture:
Switch: [warm] [wet] [bright] [windy] [moving] [loud] [dark] [cold]
Value: [ 1 ] [ 0 ] [ 1 ] [ 0 ] [ 1 ] [ 0 ] [ 0 ] [ 1 ]
“That’s 10100101 in binary – which is just ONE byte!” Max explains.
Lila the LED adds: “And to check just ONE switch, we use a trick called AND. It’s like putting a mask over everything else so we can peek at just one switch!”
Bella the Battery cheers: “Instead of sending 8 separate letters (8 bytes), we pack everything into 1 byte. That saves me 87.5% of my energy!”
“But wait,” Sammy asks, “what about big numbers? Like my reading of 1,234?”
Max explains: “Big numbers need multiple bytes. The tricky part is which byte goes first! Some computers put the big part first (big-endian), others put the small part first (little-endian). It’s like writing a number left-to-right or right-to-left. Always check!”
The Squad’s Tip: Think of bytes as rows of light switches. Bitwise operations flip, check, and read those switches one at a time!