Understanding Binary Coded Decimal Code in Hardware

Binary Coded Decimal (BCD) is a digital encoding method where each decimal digit (0-9) is represented by its own distinct 4-bit binary sequence. Unlike pure binary, which converts an entire multi-digit number into a single base-2 value, BCD isolates each digit. The most common weighting scheme is 8421, where the four bits represent the values 8, 4, 2, and 1 from most significant bit (MSB) to least significant bit (LSB).

In embedded systems and industrial control panels, binary coded decimal code is heavily used in rotary thumbwheel switches, absolute encoders, and legacy HVAC dials. It allows a microcontroller to read human-readable decimal inputs directly without performing complex base-16 to base-10 math. However, because 4 bits can represent 16 states (0x0 to 0xF) and BCD only uses 10 of them, the remaining 6 states (10 through 15) are invalid. Handling these invalid states is the primary source of debugging headaches in BCD projects.

8421 BCD Truth Table and Invalid States

Below is the definitive mapping for a standard 4-bit BCD switch. When reading hardware, your firmware must explicitly reject the highlighted invalid states to prevent logic faults.

Decimal8 (B3)4 (B2)2 (B1)1 (B0)HexState Validity
000000x0Valid
100010x1Valid
200100x2Valid
300110x3Valid
401000x4Valid
501010x5Valid
601100x6Valid
701110x7Valid
810000x8Valid
910010x9Valid
1010100xAINVALID
1110110xBINVALID
1211000xCINVALID
1311010xDINVALID
1411100xEINVALID
1511110xFINVALID

Project Build: Reading a BCD Thumbwheel Switch with ESP32

This build targets the ESP32 DevKit V1 (ESP32-WROOM-32 module). We will interface a 4-bit BCD thumbwheel switch, decode the binary coded decimal code in software, validate the state, and output the result to the Serial Monitor.

Difficulty Rating: Beginner/Intermediate | Time to Build: 30 Minutes

Parts List

  • MCU: ESP32 DevKit V1 (30-pin or 38-pin variant, ESP32-WROOM-32)
  • Switch: CTS 4610 Series 4-bit BCD Thumbwheel Switch (or equivalent 10-position BCD rotary)
  • Resistors: 4x 10kΩ through-hole resistors (for external pull-ups)
  • Wiring: 22 AWG solid core jumper wires, standard solderless breadboard

Pin Mapping Table

The ESP32-WROOM-32 has specific GPIO strapping pins that can cause boot failures if pulled high or low during startup. We avoid GPIO 0, 2, 5, 12, and 15 for switch inputs. See the ESP32 Datasheet for strapping pin details.

BCD Switch PinFunctionESP32 GPIOExternal Pull-up
Common (COM)Ground ReferenceGNDN/A
Bit 0 (1s)LSB OutputGPIO 1310kΩ to 3.3V
Bit 1 (2s)OutputGPIO 1410kΩ to 3.3V
Bit 2 (4s)OutputGPIO 2710kΩ to 3.3V
Bit 3 (8s)MSB OutputGPIO 2610kΩ to 3.3V
Callout Tip: Why use external 10kΩ pull-ups instead of the ESP32's internal pull-ups? The internal pull-ups on the ESP32 are roughly 45kΩ. In electrically noisy environments (like near relays or motors), 45kΩ is too weak to hold the line firmly HIGH, leading to phantom BCD state changes. A 10kΩ external resistor provides a much stiffer logic HIGH.

Complete ESP32 BCD Decoding Firmware

The following Arduino C++ code is fully compilable for the ESP32 DevKit V1. It reads the GPIO pins, assembles the 4-bit nibble using bitwise shift operations, and includes strict error handling for the 6 invalid BCD states.

#include <Arduino.h>

// Pin Definitions (Avoid ESP32 strapping pins)
#define BCD_PIN_0 13  // LSB (1s)
#define BCD_PIN_1 14  // 2s
#define BCD_PIN_2 27  // 4s
#define BCD_PIN_3 26  // MSB (8s)

// Debounce timing in milliseconds
const unsigned long DEBOUNCE_DELAY = 50;
unsigned long lastReadTime = 0;
int lastValidBCD = 0;

void setup() {
  Serial.begin(115200);
  delay(1000); // Allow serial port to initialize
  Serial.println("BCD Decoder Initialized...");

  // Configure pins as inputs
  pinMode(BCD_PIN_0, INPUT);
  pinMode(BCD_PIN_1, INPUT);
  pinMode(BCD_PIN_2, INPUT);
  pinMode(BCD_PIN_3, INPUT);
  
  // Note: External 10k pull-ups are used. 
  // If using internal pull-ups, change INPUT to INPUT_PULLUP.
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - lastReadTime >= DEBOUNCE_DELAY) {
    lastReadTime = currentMillis;

    // Read individual bits
    uint8_t b0 = digitalRead(BCD_PIN_0);
    uint8_t b1 = digitalRead(BCD_PIN_1);
    uint8_t b2 = digitalRead(BCD_PIN_2);
    uint8_t b3 = digitalRead(BCD_PIN_3);

    // Assemble the 4-bit BCD value using bitwise shift
    uint8_t bcdVal = (b3 << 3) | (b2 << 2) | (b1 << 1) | b0;

    // Validate BCD State (Must be 0-9)
    if (bcdVal > 9) {
      // Exact error string for debugging
      Serial.print("[ERR] BCD Fault: Read 0x");
      Serial.print(bcdVal, HEX);
      Serial.print(" (");
      Serial.print(bcdVal);
      Serial.println(") - Invalid State");
    } else {
      // Only update and print if the value has actually changed
      if (bcdVal != lastValidBCD) {
        lastValidBCD = bcdVal;
        Serial.print("Valid BCD Decimal: ");
        Serial.println(bcdVal);
      }
    }
  }
}

Debugging BCD Read Errors and Invalid States

When working with physical mechanical switches, you will inevitably encounter invalid states. If your serial monitor outputs the exact error string: [ERR] BCD Fault: Read 0x0C (12) - Invalid State, do not assume the switch is broken. This is a standard transient or wiring fault.

Here are the first three things to check when it fails, ranked from most likely to least likely:

  1. Switch Transition Bounce (Mid-Rotation Read): Mechanical BCD thumbwheel switches do not transition all 4 bits simultaneously. As you rotate the dial from 3 (0011) to 4 (0100), the contacts break and make at slightly different microsecond intervals. The MCU might read a transient state like 0111 (7) or 1100 (12/0x0C) during the physical sweep. Fix: Increase the DEBOUNCE_DELAY in the code to 100ms, or implement a software filter that requires three consecutive identical reads before accepting a new value.
  2. Missing or Failed Pull-Up Resistors: If the switch is in a position where a bit is OPEN (logic HIGH expected), but the pull-up resistor is missing or broken, the ESP32 GPIO pin is left floating. It will pick up ambient electromagnetic noise and read a phantom LOW or oscillate. Fix: Measure the voltage at the ESP32 GPIO pin with a multimeter while the switch is open. It must read a solid 3.2V to 3.3V. If it reads 0.5V to 1.5V, your pull-up circuit is compromised.
  3. LSB/MSB Wiring Swap: If you consistently read invalid states like 0x0A (1010) when the dial is set to 5 (0101), your bit weighting is inverted. Fix: Verify your wiring against the pin mapping table. Ensure the switch pin labeled '1' or 'A' goes to BCD_PIN_0, and '8' or 'D' goes to BCD_PIN_3.

Extending and Simplifying the BCD Build

Once you have a single-digit BCD switch working, you will likely need to scale the project. You have two distinct architectural paths: scaling in software, or offloading to hardware logic.

Comparison: Software Decoding vs. Hardware Logic ICs

CriteriaSoftware Decoding (Direct GPIO)Hardware Logic (e.g., 74HC147)
GPIO Usage4 pins per digit (16 pins for 4 digits)4 pins total (using multiplexing/encoding)
Invalid State HandlingMust be programmed in firmwareHandled automatically by IC priority logic
BOM Cost (2026 Pricing)~$0.05 (resistors only)~$0.65 per 74HC147 IC
PCB Routing ComplexityHigh (many traces to MCU)Low (centralized logic routing)

How to Extend: Multi-Digit Multiplexing

To read a 3-digit BCD switch bank (000-999) without using 12 GPIO pins, use a multiplexer IC like the 74HC4052 or a shift register like the 74HC165. You wire the BCD outputs of all three switches together, and use the MCU to toggle the common ground of each switch one at a time. Read the 4 BCD lines, store the value, switch to the next digit, and repeat. This reduces the GPIO requirement from 12 down to 7 (4 data lines + 3 control lines).

How to Simplify: The 74HC147 Priority Encoder

If you want to eliminate software debouncing and invalid state checking entirely, use a TI SN74HC147 10-line to 4-line BCD priority encoder. This IC takes standard decimal inputs and outputs clean, hardware-debounced BCD code. Furthermore, it features priority logic: if multiple inputs are triggered simultaneously (a common mechanical fault), it automatically outputs the highest-value digit, preventing the invalid states that plague direct-switch wiring.

By mastering binary coded decimal code at both the bitwise firmware level and the hardware logic level, you can reliably integrate legacy industrial dials and precision rotary inputs into modern ESP32 IoT architectures without falling victim to transient state errors.