To decipher an 8-bit binary code in hardware, you map physical high/low voltages to a byte variable using bitwise shift operations, converting base-2 physical states into base-10 decimal values. For reliable parallel bus decoding on the ESP32 without exhausting its limited GPIO pins, the most robust method is using an I2C I/O expander like the PCF8574. This isolates the I/O, handles parallel-to-serial conversion, and allows you to read an entire 8-bit binary word in a single I2C transaction.

This guide bridges the fundamental theory of binary mathematics with a practical, decision-forward embedded build. We will wire an 8-bit parallel input, write the bitwise C++ logic to decipher it, and establish a strict debugging protocol for when the I2C bus inevitably throws a NACK error.

Hardware Spec Sheet and Pin Mapping

Before writing code, we must define the physical layer. Direct-wiring 8 pins to an ESP32 is possible, but it consumes valuable strapping pins (like GPIO 0, 2, and 12) which can cause boot failures if pulled to the wrong state during power-on. Using an I2C expander solves this.

Bill of Materials (BOM) & Component Variants
Component Exact Variant / Model Estimated Cost (2026) Role in Circuit
Microcontroller ESP32-DevKitC V4 (ESP32-WROOM-32E module) $5.50 Main processor, I2C master, bitwise math engine
I/O Expander PCF8574T (SOIC-16 IC or pre-built breakout board) $1.80 Parallel-to-serial conversion, 8-bit I/O buffering
Input Source 8-Position DIP Switch (2.54mm pitch) $0.50 Simulates an 8-bit parallel binary bus or sensor array
Pull-up Resistors 4.7kΩ through-hole (for I2C) & 10kΩ (for inputs) $0.10 Bus stabilization and floating-pin prevention

Pin Mapping Table

The PCF8574 communicates via I2C. Ensure your ESP32 variant uses the standard default I2C pins, or redefine them in software.

ESP32-DevKitC V4 Pin PCF8574 Breakout Pin Function
3V3VCCPower (3.3V logic level)
GNDGNDCommon Ground
GPIO 21SDAI2C Data Line
GPIO 22SCLI2C Clock Line
N/A (Tie to GND)A0, A1, A2I2C Address Selection (Sets address to 0x20)
N/A (Read via I2C)P0 - P78-Bit Parallel Binary Input Lines

The Theory of Deciphering Binary Code

Deciphering binary code is fundamentally an exercise in base-2 to base-10 conversion. Each physical wire in our 8-bit bus represents a single bit (0 or 1). The rightmost bit (P0 on the PCF8574) is the Least Significant Bit (LSB) and represents $2^0$ (1). The leftmost bit (P7) is the Most Significant Bit (MSB) and represents $2^7$ (128).

If P7 is HIGH, P2 is HIGH, and all others are LOW, the physical binary code is 10000100. To decipher this into a human-readable decimal number, we sum the powers of two for every HIGH bit:

$2^7 + 2^2 = 128 + 4 = 132$

In embedded C++, we do not manually add these powers. Instead, we read the 8 bits as a single raw byte (an uint8_t variable) and use bitwise masking and shifting to isolate specific flags or combine bytes. For example, if you only want to decipher the lower 4 bits (a nibble) representing a BCD (Binary Coded Decimal) digit, you apply a bitwise AND mask: uint8_t lower_nibble = raw_byte & 0x0F;. This zeroes out the top four bits, leaving only the 0-15 value intact.

Compilable ESP32 Decoder Code

The following code targets the ESP32-DevKitC V4 (ESP32-WROOM-32E) using the Arduino core. It initializes the I2C bus, requests one byte from the PCF8574, handles I2C transmission errors, and deciphers the raw binary into both decimal and hexadecimal formats.

#include <Wire.h>

// --- Pin Definitions & Constants ---
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define PCF8574_ADDR 0x20  // A0, A1, A2 tied to GND
#define I2C_CLOCK_SPEED 100000 // 100kHz standard mode

// Variables to hold deciphered data
uint8_t raw_binary_byte = 0;
uint8_t previous_byte = 0;

void setup() {
  Serial.begin(115200);
  while(!Serial) { delay(10); }
  
  Serial.println("[SYS] Initializing I2C Bus...");
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN, I2C_CLOCK_SPEED);
  
  // PCF8574 inputs are quasi-bidirectional. 
  // Writing 0xFF sets all pins HIGH, enabling internal weak pull-ups.
  Wire.beginTransmission(PCF8574_ADDR);
  Wire.write(0xFF); 
  uint8_t init_error = Wire.endTransmission();
  
  if (init_error != 0) {
    Serial.printf("[ERR] I2C_NACK_ADDR: Wire.endTransmission() returned %d\n", init_error);
    Serial.println("[SYS] Halting. Check wiring and address pins.");
    while(1) { delay(1000); }
  }
  Serial.println("[SYS] PCF8574 Online. Deciphering binary stream...");
}

void loop() {
  // Request 1 byte from the PCF8574
  uint8_t bytes_read = Wire.requestFrom(PCF8574_ADDR, (uint8_t)1);
  
  // Error Handling: Check if the I2C transaction actually succeeded
  if (bytes_read != 1) {
    Serial.printf("[ERR] I2C_NACK_DATA: Expected 1 byte, received %d\n", bytes_read);
    delay(500);
    return;
  }
  
  raw_binary_byte = Wire.read();
  
  // State-change detection to avoid flooding the serial monitor
  if (raw_binary_byte != previous_byte) {
    decipher_and_print(raw_binary_byte);
    previous_byte = raw_binary_byte;
  }
  
  delay(50); // 50ms debounce / poll rate
}

void decipher_and_print(uint8_t byte_val) {
  // Decipher to Decimal
  uint16_t decimal_val = byte_val; // Implicit cast
  
  // Decipher specific flags using Bitwise AND masking
  bool is_parity_bit_set = (byte_val & 0x80) != 0; // Check MSB (Bit 7)
  uint8_t lower_nibble = byte_val & 0x0F;          // Isolate Bits 0-3
  
  Serial.println("--- Binary Deciphered ---");
  Serial.printf("Raw Binary : %08b\n", byte_val);
  Serial.printf("Hexadecimal: 0x%02X\n", byte_val);
  Serial.printf("Decimal    : %u\n", decimal_val);
  Serial.printf("Bit 7 Flag : %s\n", is_parity_bit_set ? "HIGH" : "LOW");
  Serial.printf("Lower Nibble (0-15): %u\n", lower_nibble);
  Serial.println("-------------------------");
}

Debugging the I2C Bus: Error Strings and Ranked Causes

When reading parallel binary data over an I2C expander, the physical bus is your most common point of failure. If your serial monitor outputs an error, follow this decision path.

The First 3 Things to Check When It Fails:
  1. I2C Pull-up Resistors: Verify 4.7kΩ resistors are physically present between SDA/SCL and 3.3V. The ESP32 internal pull-ups are too weak (~45kΩ) for reliable I2C communication at 100kHz.
  2. Address Pin Hard-Ties: Ensure PCF8574 pins A0, A1, and A2 are physically wired to GND (for address 0x20). Floating address pins will cause the chip to randomly shift its I2C address due to ambient EMI.
  3. SDA/SCL Swap: Confirm GPIO 21 is SDA and GPIO 22 is SCL. Swapping them yields a silent failure or a NACK.

Exact Error Strings and Ranked Causes

Error String: [ERR] I2C_NACK_ADDR: Wire.endTransmission() returned 2

Meaning: The ESP32 sent the address (0x20), but no device acknowledged it (NACK on address).

  • Cause 1 (Most Likely): A0, A1, A2 pins are not all tied to GND, changing the hardware address.
  • Cause 2: SDA/SCL wires are swapped or broken.
  • Cause 3: The PCF8574 chip is unpowered (VCC missing).

Error String: [ERR] I2C_NACK_DATA: Expected 1 byte, received 0

Meaning: The address was acknowledged, but the data request failed (NACK on data).

  • Cause 1 (Most Likely): I2C bus capacitance is too high (wires too long, >30cm), causing signal degradation.
  • Cause 2: Clock speed is too high for the wire length. Drop I2C_CLOCK_SPEED to 50000 (50kHz).

Decision Tree: Choosing Your Binary Decoding Hardware

Not every binary deciphering task requires an I2C expander. Use this decision matrix to select the correct hardware interface for your specific bit-width and speed requirements.

Input Condition Hardware Pick Technical Justification
Reading 1 to 4 pins, speed < 1kHz Direct ESP32 GPIO Zero overhead. Use digitalRead() or direct port manipulation. Avoid strapping pins (0, 2, 12).
Reading 8 pins, speed < 5kHz, need pin isolation PCF8574 I2C Expander (Default Pick) Frees up ESP32 GPIOs, provides quasi-bidirectional buffering, and requires only 2 MCU pins. Ideal for DIP switches and slow parallel buses.
Reading 8+ pins, speed > 10kHz (e.g., ADC bus) 74HC165 SPI Shift Register I2C is too slow for high-frequency parallel sampling. SPI shift registers can clock in 8 bits in microseconds with deterministic timing.
Reading 16 to 32 pins simultaneously MCP23017 (I2C) or 74HC165 Cascade MCP23017 offers true 16-bit I/O with interrupt pins; cascading 74HC165s is better for strict timing.

The Concrete Pick: For 90% of hobbyist and industrial-control binary deciphering tasks (reading sensor arrays, DIP switches, or slow parallel protocols), default to the PCF8574. It costs under $2, requires minimal wiring, and the I2C bus overhead is negligible for human-speed or slow-machine-speed inputs.

Extending and Simplifying the Build

How to Extend (Scaling to 16 or 24 Bits)

If you need to decipher a 16-bit binary code, do not switch to a microcontroller with more pins. Instead, cascade a second PCF8574 on the same I2C bus.

  1. Change the address of the second chip by tying its A0 pin to VCC (3.3V) while keeping A1 and A2 at GND. This changes its I2C address to 0x21.
  2. In your code, execute a second Wire.requestFrom(0x21, 1).
  3. Combine the bytes using bitwise shifting: uint16_t full_word = (byte_high << 8) | byte_low;
You can cascade up to 8 PCF8574 chips (addresses 0x20 to 0x27) for a massive 64-bit parallel input bus using only two ESP32 GPIO pins.

How to Simplify (Reducing BOM and Wiring)

If you are prototyping and lack a pre-built PCF8574 breakout board with integrated pull-ups, you can simplify the physical build by relying on the ESP32's internal I2C pull-ups. While not recommended for production or wire runs over 10cm, you can enable them in software by modifying the setup:

Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN, I2C_CLOCK_SPEED);
pinMode(I2C_SDA_PIN, INPUT_PULLUP);
pinMode(I2C_SCL_PIN, INPUT_PULLUP);

Additionally, the PCF8574 has internal weak pull-ups on its I/O pins when written HIGH. If your DIP switches are wired directly between the P0-P7 pins and GND, you can omit the external 10kΩ input resistors entirely, reducing your component count and breadboard clutter.

For deeper hardware specifications, refer to the NXP PCF8574 Datasheet for exact I2C timing diagrams, and the Espressif ESP32-WROOM-32E Datasheet for GPIO current limits and strapping pin warnings. For software implementation details, consult the Arduino Wire Library Reference.