The Short Answer: What is Binary Code Decimal (BCD)?

Binary Code Decimal (BCD) is a digital encoding method where each decimal digit (0-9) is represented by its own distinct 4-bit binary sequence. In the standard 8421 BCD weighting system, the binary values 0000 through 1001 represent the decimal numbers 0 through 9. The remaining six 4-bit combinations (1010 through 1111, representing decimal 10-15) are invalid states in pure BCD.

In embedded systems, BCD bridges the gap between human-readable decimal inputs and machine-level binary processing. While pure binary is more storage-efficient, BCD prevents rounding errors in financial calculations and simplifies hardware interfacing with decimal displays and thumbwheel switches. If you are reading a 1-digit BCD switch, you only need 4 microcontroller GPIO pins, reading a maximum value of 9, rather than dealing with the full 0-255 range of an 8-bit register.

Project Build: Reading a BCD Thumbwheel Switch

This project reads a 10-position BCD thumbwheel switch, decodes the 8421 nibble in software, handles transient invalid states caused by mechanical switch bounce, and displays the clean decimal output on an I2C OLED.

Difficulty Rating: Intermediate (Requires understanding of parallel digital inputs, pull-up resistors, and bitwise operations).
Estimated Time: 45 minutes for wiring, 20 minutes for firmware upload and debugging.
Target Board Variant: ESP32 DevKit V1 (30-pin, ESP32-WROOM-32 module).

Exact Parts List

  • Microcontroller: ESP32 DevKit V1 (30-pin variant, ESP32-WROOM-32E). Do not use the 38-pin variant without adjusting GPIO mappings.
  • Input Device: CTS Electrocomponents 256-410-010 (or equivalent 10-position BCD thumbwheel switch with common ground).
  • Display: 0.96-inch SSD1306 I2C OLED (128x64 resolution, 4-pin interface: VCC, GND, SCL, SDA).
  • Passives: 4x 10kΩ through-hole resistors (for GPIO pull-ups).
  • Wiring: 22 AWG solid core jumper wires.

Wiring and Pin Mapping Matrix

Mechanical BCD switches require pull-up resistors. The switch connects the common pin to ground for the active bits. When a bit is '1', the switch leaves the line floating (pulled high to 3.3V by the resistor). When a bit is '0', the switch shorts the line to ground. According to the Espressif ESP32 datasheet, internal pull-ups are roughly 45kΩ, which is too weak for noisy environments; external 10kΩ pull-ups are mandatory for clean logic transitions.

BCD Switch Pin (Weight) Function ESP32 GPIO External Pull-Up
Pin 1 (Weight 1) LSB (2^0) GPIO 32 10kΩ to 3.3V
Pin 2 (Weight 2) Bit 1 (2^1) GPIO 33 10kΩ to 3.3V
Pin 4 (Weight 4) Bit 2 (2^2) GPIO 25 10kΩ to 3.3V
Pin 8 (Weight 8) MSB (2^3) GPIO 26 10kΩ to 3.3V
Pin C (Common) Ground Reference GND N/A

Note: OLED I2C wiring uses standard ESP32 defaults: SDA to GPIO 21, SCL to GPIO 22.

Complete ESP32 BCD Decoding Firmware

This C++ code targets the Arduino IDE. It includes bitwise shifting to assemble the nibble, explicit error handling for invalid BCD states (10-15), and debouncing logic to prevent screen flicker.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- PIN DEFINITIONS ---
#define BCD_BIT0 32  // Weight 1
#define BCD_BIT1 33  // Weight 2
#define BCD_BIT2 25  // Weight 4
#define BCD_BIT3 26  // Weight 8

// --- OLED DEFINITIONS ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// --- STATE VARIABLES ---
int lastValidBCD = 0;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50ms debounce

void setup() {
  Serial.begin(115200);
  
  // Configure BCD pins as inputs (external pull-ups used)
  pinMode(BCD_BIT0, INPUT);
  pinMode(BCD_BIT1, INPUT);
  pinMode(BCD_BIT2, INPUT);
  pinMode(BCD_BIT3, INPUT);

  // Initialize OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Halt execution
  }
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 20);
  display.println("BCD READY");
  display.display();
}

void loop() {
  // Read raw GPIO states (Active LOW logic due to pull-ups and common ground)
  int b0 = !digitalRead(BCD_BIT0);
  int b1 = !digitalRead(BCD_BIT1);
  int b2 = !digitalRead(BCD_BIT2);
  int b3 = !digitalRead(BCD_BIT3);

  // Assemble 4-bit nibble
  int rawNibble = (b3 << 3) | (b2 << 2) | (b1 << 1) | b0;

  // ERROR HANDLING: Check for invalid BCD states (10 through 15)
  if (rawNibble > 9) {
    // This occurs during the physical rotation of the switch (break-before-make)
    Serial.printf("ERR: BCD INVALID STATE 0x%02X\n", rawNibble);
    return; // Ignore transient invalid states, hold last valid value
  }

  // Debounce logic
  if (rawNibble != lastValidBCD) {
    if ((millis() - lastDebounceTime) > debounceDelay) {
      lastValidBCD = rawNibble;
      lastDebounceTime = millis();
      updateDisplay(lastValidBCD);
    }
  }
}

void updateDisplay(int value) {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.setTextSize(1);
  display.println("BCD Decimal Value:");
  
  display.setTextSize(4);
  display.setCursor(40, 20);
  display.print(value);
  display.display();
  Serial.printf("Valid BCD Read: %d\n", value);
}

Decision Tree: Choosing Your BCD Decoding Approach

When integrating BCD inputs, you must decide between hardware decoding (using a dedicated logic IC) and software decoding (reading raw bits into a microcontroller). Use this decision path to select your architecture.

Criteria Hardware Decoder (e.g., TI CD4511B) Software Decoding (MCU GPIO)
MCU Pin Count Required 4 pins (for BCD) or 1 pin (if using serial/shift register) 4 pins per BCD digit
Invalid State Handling IC blanks display automatically on inputs 10-15 Requires explicit 'if > 9' logic in firmware
Switch Bounce Filtering Requires external RC snubber or capacitor Handled easily in software (millis() debounce)
Cost & Board Space +$0.60 per IC + passives, takes up DIP-16 footprint $0.00 extra, utilizes existing MCU silicon
Decision Termination: If your switch input frequency is below 100Hz (human-operated thumbwheels) and you have 4 spare GPIO pins, choose Software Decoding. It eliminates the CD4511 IC, reduces BOM cost, and allows you to handle invalid transition states gracefully in code without analog RC filtering.

Debugging BCD Read Errors: The 'Invalid State' Problem

The most common failure mode when reading mechanical BCD switches is receiving erratic, jumping numbers on your display or serial monitor. This is caused by the physical wiper inside the switch transitioning between contacts.

First Three Things to Check When It Fails

  1. Verify Pull-Up Voltage: Measure the voltage at the ESP32 GPIO pin with the switch in the 'open' (1) state. It must read a stable 3.2V to 3.3V. If it reads 1.5V or floats, your 10kΩ pull-up resistor is missing or tied to the wrong rail.
  2. Check Common Ground: Ensure the switch's Common (C) pin is tied to the ESP32's GND, not the 3.3V rail. Reversing this will invert your logic and cause immediate invalid states.
  3. Inspect Bit Weighting Order: Confirm that GPIO 32 is reading the '1' weight, not the '8' weight. Swapping MSB and LSB will cause the switch to output 1, 8, 2, 4 instead of 1, 2, 3, 4.

Ranked Causes for the 'ERR: BCD INVALID STATE' Serial Output

If your serial monitor is flooded with ERR: BCD INVALID STATE 0x0A (or 0x0B through 0x0F), here is the ranked list of causes:

  • Cause 1 (90% likelihood): Normal Mechanical Transition. Most thumbwheel switches are 'break-before-make'. As you turn the dial from 3 (0011) to 4 (0100), the wiper breaks contact with the 1 and 2 pins before making contact with the 4 pin. For a few milliseconds, all pins read 0 (0000), or intermediate states like 0010 (2) or 0110 (6). If the wiper makes partial contact, you can read 1010 (0x0A). Fix: The provided code ignores these states, holding the last valid number. This is the correct software approach.
  • Cause 2 (8% likelihood): Missing Pull-Up Resistor. Without a pull-up, an open switch contact acts as an antenna, picking up EMI and floating high. Fix: Install 10kΩ resistors from each data line to 3.3V.
  • Cause 3 (2% likelihood): Dirty Switch Contacts. Carbon buildup inside an older switch causes high resistance, preventing the GPIO from pulling fully to ground (reading a logic 1 instead of 0). Fix: Spray DeoxIT D5 into the switch mechanism and cycle it 20 times.

Extending and Simplifying the Build

Once you have a single digit reading reliably, you can scale the system based on your project requirements.

How to Extend (Multi-Digit BCD)

To read a 3-digit BCD switch (000-999), you will need 12 GPIO pins. Instead of duplicating the logic, create an array of pin definitions and loop through them. Multiply the hundreds digit by 100, the tens digit by 10, and add the units digit. Warning: Ensure your ESP32 has enough exposed GPIOs; avoid using strapping pins (GPIO 0, 2, 12, 15) for BCD inputs, as external pull-ups on these pins will prevent the ESP32 from booting into flash mode.

How to Simplify (Hardware Offloading)

If you are running out of GPIO pins or want to eliminate software debouncing entirely, simplify the build by adding a CD4511BE BCD-to-7-Segment Latch. Wire the BCD switch directly to the CD4511 inputs, and wire the CD4511 outputs to a standard 7-segment display. This removes the ESP32 from the input loop entirely, functioning as a pure hardware digital circuit. Use the MCU only if you need to log the data to an SD card or transmit it over MQTT.