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 costSoftware Double DabbleESP32 direct GPIO driving
3 or 4 digits, low CPU overheadSPI LED DriverMAX7219 module
Hardware logic, no MCU codeHardware BCD DecoderCD4511BE 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++.

CriteriaSoftware (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 OverheadModerate (multiplexing required)Low (latched outputs)Very Low (hardware handles it)
Current SourcingLimited 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.

Difficulty Rating: Intermediate | Estimated Time: 45 minutes
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 PinESP32 GPIONotes
Segment AGPIO 16Via 220Ω resistor
Segment BGPIO 17Via 220Ω resistor
Segment CGPIO 18Via 220Ω resistor
Segment DGPIO 19Via 220Ω resistor
Segment EGPIO 21Via 220Ω resistor
Segment FGPIO 22Via 220Ω resistor
Segment GGPIO 23Via 220Ω resistor
Digit 1 CathodeGPIO 25To 2N2222 Collector
Digit 2 CathodeGPIO 26To 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

  1. 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).
  2. 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.
  3. 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

Symptom: ESP32 continuously reboots, Serial monitor shows:
E (1452) gpio: gpio_set_level(226): GPIO output gpio_num error
Cause 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).
Symptom: Serial monitor outputs:
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.