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.
| Decimal | 8 (B3) | 4 (B2) | 2 (B1) | 1 (B0) | Hex | State Validity |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0x0 | Valid |
| 1 | 0 | 0 | 0 | 1 | 0x1 | Valid |
| 2 | 0 | 0 | 1 | 0 | 0x2 | Valid |
| 3 | 0 | 0 | 1 | 1 | 0x3 | Valid |
| 4 | 0 | 1 | 0 | 0 | 0x4 | Valid |
| 5 | 0 | 1 | 0 | 1 | 0x5 | Valid |
| 6 | 0 | 1 | 1 | 0 | 0x6 | Valid |
| 7 | 0 | 1 | 1 | 1 | 0x7 | Valid |
| 8 | 1 | 0 | 0 | 0 | 0x8 | Valid |
| 9 | 1 | 0 | 0 | 1 | 0x9 | Valid |
| 10 | 1 | 0 | 1 | 0 | 0xA | INVALID |
| 11 | 1 | 0 | 1 | 1 | 0xB | INVALID |
| 12 | 1 | 1 | 0 | 0 | 0xC | INVALID |
| 13 | 1 | 1 | 0 | 1 | 0xD | INVALID |
| 14 | 1 | 1 | 1 | 0 | 0xE | INVALID |
| 15 | 1 | 1 | 1 | 1 | 0xF | INVALID |
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.
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 Pin | Function | ESP32 GPIO | External Pull-up |
|---|---|---|---|
| Common (COM) | Ground Reference | GND | N/A |
| Bit 0 (1s) | LSB Output | GPIO 13 | 10kΩ to 3.3V |
| Bit 1 (2s) | Output | GPIO 14 | 10kΩ to 3.3V |
| Bit 2 (4s) | Output | GPIO 27 | 10kΩ to 3.3V |
| Bit 3 (8s) | MSB Output | GPIO 26 | 10kΩ to 3.3V |
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:
- 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_DELAYin the code to 100ms, or implement a software filter that requires three consecutive identical reads before accepting a new value. - 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.
- 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
| Criteria | Software Decoding (Direct GPIO) | Hardware Logic (e.g., 74HC147) |
|---|---|---|
| GPIO Usage | 4 pins per digit (16 pins for 4 digits) | 4 pins total (using multiplexing/encoding) |
| Invalid State Handling | Must be programmed in firmware | Handled automatically by IC priority logic |
| BOM Cost (2026 Pricing) | ~$0.05 (resistors only) | ~$0.65 per 74HC147 IC |
| PCB Routing Complexity | High (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.






