The Quick Answer: Implementing Binary to BCD Code
If you need to convert a pure binary value (like 0001 1111 for 31) into Binary-Coded Decimal (BCD) to drive 7-segment displays or decimal readouts, you have two primary paths. The most efficient software method for microcontrollers is the Double Dabble algorithm (also known as shift-and-add-3). The most reliable hardware method is offloading the math to a dedicated BCD-to-7-segment decoder IC like the CD4511BE or 74LS47.
Converting binary to BCD code isn't just about math; it's about managing GPIO limits, CPU cycles, and display multiplexing. Use the decision tree below to lock in your approach before cutting any wires.
Decision Tree: Which Conversion Method Should You Pick?
| If your project requires... | Then choose... | Concrete Part / Method |
|---|---|---|
| 1 or 2 digits, minimal BOM cost | Software Double Dabble | ESP32 direct GPIO driving |
| 3 or 4 digits, low CPU overhead | SPI LED Driver | MAX7219 module |
| Hardware logic, no MCU code | Hardware BCD Decoder | CD4511BE or 74LS47 IC |
Default Pick: For a standard 2-digit DIY sensor readout, use the Software Double Dabble method directly on the ESP32. It costs $0 in extra BOM, requires no SPI/I2C libraries, and keeps your codebase self-contained.
Hardware vs. Software: Choosing Your Conversion Method
Before we wire the breadboard, let's look at the trade-offs between doing the binary to BCD code conversion in silicon versus doing it in C++.
| Criteria | Software (Double Dabble) | Hardware Decoder (CD4511BE) | LED Driver IC (MAX7219) |
|---|---|---|---|
| GPIO Pins Used (2 digits) | 14 pins (direct drive) | 8 pins (4 per IC) | 3 pins (SPI) |
| Component Cost | ~$0.50 (resistors) | ~$1.80 (2x ICs) | ~$3.50 (module) |
| CPU Overhead | Moderate (multiplexing required) | Low (latched outputs) | Very Low (hardware handles it) |
| Current Sourcing | Limited by MCU (max 40mA/pin) | High (up to 25mA per segment) | High (constant current sinks) |
Project Build: ESP32 Binary Counter to BCD Display
We are going to build a 2-digit binary counter that converts its internal 8-bit state to BCD code and displays it on two common-cathode 7-segment displays using direct GPIO multiplexing.
Target Board Variant: ESP32 DevKit V1 (30-pin, ESP32-WROOM-32 module). Note: Code and pinouts are specific to the 30-pin variant; 38-pin variants have shifted GPIO mappings.
Parts List
- 1x ESP32 DevKit V1 (30-pin, ESP32-WROOM-32)
- 2x Common Cathode 7-Segment Displays (e.g., Kingbright SC56-11GWA)
- 14x 220Ω 1/4W Resistors (for current limiting, targeting ~15mA at 2.1V forward voltage)
- 2x 2N2222 NPN Transistors (for digit multiplexing common cathodes)
- 2x 1kΩ Base Resistors (for the 2N2222 transistors)
- 1x Half-size Breadboard and jumper wires
Pin Mapping Table
CRITICAL ESP32 WARNING: Never use GPIO 0, 2, or 12 for display outputs. These are strapping pins. Pulling them high or low during boot will put the ESP32 into flash mode or cause a brownout reset.
| 7-Segment Pin | ESP32 GPIO | Notes |
|---|---|---|
| Segment A | GPIO 16 | Via 220Ω resistor |
| Segment B | GPIO 17 | Via 220Ω resistor |
| Segment C | GPIO 18 | Via 220Ω resistor |
| Segment D | GPIO 19 | Via 220Ω resistor |
| Segment E | GPIO 21 | Via 220Ω resistor |
| Segment F | GPIO 22 | Via 220Ω resistor |
| Segment G | GPIO 23 | Via 220Ω resistor |
| Digit 1 Cathode | GPIO 25 | To 2N2222 Collector |
| Digit 2 Cathode | GPIO 26 | To 2N2222 Collector |
The Code: Double Dabble Algorithm in C++
The Double Dabble algorithm converts binary to BCD code by shifting the binary number left one bit at a time. Before each shift, if any BCD digit is 5 or greater, we add 3 to it. This forces the base-10 carry to happen correctly in base-2 logic.
Copy and paste the complete, compilable code below into your Arduino IDE. Ensure your board manager is set to ESP32 Dev Module.
// Binary to BCD Code Converter for ESP32 DevKit V1 (30-pin)
// Target: 2x Common Cathode 7-Segment Displays via Multiplexing
// --- PIN DEFINITIONS ---
const uint8_t SEG_PINS[7] = {16, 17, 18, 19, 21, 22, 23}; // A, B, C, D, E, F, G
const uint8_t DIGIT_PINS[2] = {25, 26}; // Digit 1 (Tens), Digit 2 (Ones)
// 7-segment lookup for common cathode (1 = ON, 0 = OFF)
// Order: A, B, C, D, E, F, G
const uint8_t BCD_TO_SEG[10][7] = {
{1,1,1,1,1,1,0}, // 0
{0,1,1,0,0,0,0}, // 1
{1,1,0,1,1,0,1}, // 2
{1,1,1,1,0,0,1}, // 3
{0,1,1,0,0,1,1}, // 4
{1,0,1,1,0,1,1}, // 5
{1,0,1,1,1,1,1}, // 6
{1,1,1,0,0,0,0}, // 7
{1,1,1,1,1,1,1}, // 8
{1,1,1,1,0,1,1} // 9
};
volatile uint8_t current_tens = 0;
volatile uint8_t current_ones = 0;
volatile uint8_t active_digit = 0;
hw_timer_t * timer = NULL;
// --- DOUBLE DABBLE ALGORITHM ---
void binaryToBCD(uint8_t binary, uint8_t &tens, uint8_t &ones) {
if (binary > 99) {
Serial.printf("ERR: BCD_CONV_OVERFLOW - Input %d exceeds 2-digit limit.\n", binary);
tens = 9; ones = 9; // Failsafe display
return;
}
uint8_t bcd = 0;
for (int i = 0; i < 8; i++) {
// Add 3 to BCD zones that are >= 5
if ((bcd & 0x0F) >= 5) bcd += 3;
if ((bcd & 0xF0) >= 0x50) bcd += 0x30;
// Shift BCD left and bring in the MSB of the binary input
bcd = (bcd << 1) | ((binary >> 7) & 1);
binary <<= 1;
}
tens = (bcd >> 4) & 0x0F;
ones = bcd & 0x0F;
}
// --- MULTIPLEXING INTERRUPT ---
void IRAM_ATTR onTimer() {
// Turn off both digits
digitalWrite(DIGIT_PINS[0], LOW);
digitalWrite(DIGIT_PINS[1], LOW);
// Get current BCD value for the active digit
uint8_t val = (active_digit == 0) ? current_tens : current_ones;
// Write segments
for (int i = 0; i < 7; i++) {
digitalWrite(SEG_PINS[i], BCD_TO_SEG[val][i]);
}
// Turn on active digit (NPN transistor requires HIGH to sink cathode)
digitalWrite(DIGIT_PINS[active_digit], HIGH);
// Toggle for next interrupt
active_digit = !active_digit;
}
void setup() {
Serial.begin(115200);
Serial.println("Binary to BCD Code Converter Initialized.");
// Initialize GPIOs
for (int i = 0; i < 7; i++) pinMode(SEG_PINS[i], OUTPUT);
for (int i = 0; i < 2; i++) pinMode(DIGIT_PINS[i], OUTPUT);
// Setup hardware timer for multiplexing at 500Hz (2ms per digit)
timer = timerBegin(0, 80, true); // 80MHz / 80 = 1MHz tick
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 2000, true); // 2000us = 2ms
timerAlarmEnable(timer);
}
void loop() {
// Simulate a binary counter incrementing every second
static uint8_t binary_counter = 0;
binaryToBCD(binary_counter, current_tens, current_ones);
Serial.printf("Binary: %03d | BCD Tens: %d | BCD Ones: %d\n",
binary_counter, current_tens, current_ones);
binary_counter++;
if (binary_counter > 99) binary_counter = 0;
delay(1000);
}
Debugging Guide: When Your BCD Output Shows Garbage
When working with direct-drive multiplexing and binary to BCD code conversions, things often go wrong on the first upload. If your display shows random segments, flickers violently, or stays stuck on 88, follow this diagnostic path.
The First Three Things to Check
- Common Cathode vs. Common Anode Mismatch: The code above assumes Common Cathode (segments HIGH to illuminate, digit pin HIGH to ground via NPN). If you are using Common Anode displays, the logic is inverted. Your display will show the exact opposite of what you intend (e.g., showing '8' when it should be blank).
- Strapping Pin Conflicts: Did you accidentally wire a segment to GPIO 12? If GPIO 12 is pulled high during boot, the ESP32-WROOM-32 will fail to boot and throw a continuous brownout loop. Check your physical wiring against the pin mapping table above.
- Missing Base Resistors on Transistors: If you connected the ESP32 GPIO directly to the base of the 2N2222 without a 1kΩ resistor, you are pulling excessive current from the ESP32's internal 3.3V regulator, causing a brownout reset.
Ranked Causes for Specific Error Symptoms
E (1452) gpio: gpio_set_level(226): GPIO output gpio_num errorCause 1 (Most Likely): You defined a GPIO pin in the
SEG_PINS array that is input-only on the ESP32 (like GPIO 34, 35, 36, or 39). Fix: Restrict outputs to GPIO 13-33.Cause 2: The array index in the interrupt is exceeding the bounds of
SEG_PINS. Fix: Ensure your for loop limit matches the array size (7).
ERR: BCD_CONV_OVERFLOW - Input 100 exceeds 2-digit limit.Cause: The binary input to the
binaryToBCD() function exceeded 99. The Double Dabble implementation here is strictly optimized for 8-bit inputs mapped to two BCD digits. Fix: Add a modulo operator binary_counter % 100 before passing the value, or expand the algorithm to handle a third BCD digit (hundreds).
Extending and Simplifying the Build
Once you have the base binary to BCD code conversion working, you'll likely want to scale the project. Here is how to adapt the architecture based on your new requirements.
How to Extend (Scaling to 4 Digits)
Multiplexing four discrete 7-segment displays directly from an ESP32 requires 11 GPIOs (7 segments + 4 digit controls) and heavily taxes the CPU interrupt routine.
The Upgrade Path: Switch to a MAX7219 LED driver IC. The MAX7219 handles the BCD decoding, current limiting, and multiplexing in hardware. You will replace the Double Dabble C++ code with the MD_Parola or LedControl Arduino library, communicating via SPI (using just 3 GPIOs: DIN, CLK, CS). Expect to pay about $3.50 for a pre-wired 4-digit MAX7219 module.
How to Simplify (The I2C Shortcut)
If you realize you don't actually need to learn the Double Dabble algorithm and just want a decimal readout for a sensor project, abandon bare 7-segment displays entirely.
The Shortcut: Use a TM1637 4-digit display module. It uses a proprietary 2-wire I2C-like protocol. You send it raw integers (e.g., display.showNumberDec(1024)), and the module's internal chip handles the binary to BCD code conversion and multiplexing. It costs roughly $2.00, uses only 2 GPIOs, and requires zero interrupt service routines in your main sketch.






