Binary is a base-2 numbering system using only 0s and 1s to represent physical electrical states (low/high voltage), while hexadecimal is a base-16 system using 0-9 and A-F to compactly group those binary bits so humans can read memory dumps and register masks without counting long strings of digits. In a physical circuit, these number systems do not change the copper traces or the silicon die, but they fundamentally change how you configure microcontroller registers, decode logic analyzer traces, and calculate pull-up resistor networks. People commonly confuse hexadecimal notation (like 0xFF) with a separate mathematical value, rather than realizing it is just a human-friendly shorthand for the exact same binary voltages toggling on a silicon die.
The Physical Reality Behind the Math
When you write a 1 or a 0 in your firmware, you are not doing abstract math; you are commanding a physical transistor to connect a pin to either the voltage rail or ground. For an ESP32-WROOM-32 operating on 3.3V LVCMOS logic, a binary 0 is not just 'zero'—it is any voltage below the V_IL maximum of 0.8V. A binary 1 is any voltage above the V_IH minimum of 2.0V. Anything between 0.8V and 2.0V is an undefined state that can cause erratic behavior, excessive current draw, or oscillation.
Hexadecimal exists purely because binary is exhausting for humans to read. Because 16 is a power of 2 (2^4), exactly four binary bits (a nibble) map to one hexadecimal character. An 8-bit register written as 11010010 in binary is instantly recognizable as 0xD2 in hex. This compact notation is why memory addresses, MAC addresses, and I2C registers are universally documented in hex.
Translating the Languages: A Worked Numeric Example
Let's look at a concrete example of mapping a physical microcontroller port to binary and hex. Suppose you are configuring an 8-bit GPIO port expander (like the MCP23008) and you need to set pins 7, 4, and 2 as HIGH (outputs), while keeping pins 6, 5, 3, 1, and 0 LOW.
| Pin Number | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Binary State | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
| Hex Nibble | 1001 (9) | 0100 (4) | ||||||
Reading the binary string 10010100 directly tells you the pin states, but converting it to hex (0x94) makes it easier to type into your C++ code. If you were forced to use decimal, this same configuration is 148 (128 + 16 + 4). Looking at the decimal number 148 tells you absolutely nothing about which physical pins are HIGH without doing mental division. This is why decimal is rarely used for hardware register configuration.
Where You Meet Binary and Hexadecimal Numbers in Practice
You will encounter these number systems constantly when working with embedded hardware. Here are the most common bench scenarios:
- I2C Addresses: Sensors and displays use 7-bit hex addresses. An SSD1306 OLED display typically lives at
0x3Cor0x3D. - SPI Configuration: Setting clock polarity (CPOL) and phase (CPHA) requires flipping specific bits in a control register, almost always documented in hex.
- Addressable LEDs: WS2812B NeoPixels accept 24-bit color data. A pure green command is sent as the hex value
0x00FF00, which the microcontroller shifts out as 24 distinct binary 1s and 0s. - Memory Mapping: When debugging a crash on an ESP32, the backtrace will dump hex memory addresses (e.g.,
0x400815a2) that you must feed intoesp-insightsoraddr2lineto find the faulty C++ function.
Bench Scenario: When a Hex Typo Derails an I2C Bus
To understand why confusing decimal, binary, and hex causes real hardware failures, let's walk through a common bench mistake involving the PCA9685 16-channel PWM driver, a chip frequently used to drive servos and high-power LEDs.
0x00) to enable Auto-Increment (Bit 5) and ensure the chip is awake (Bit 4 must be 0). The target binary is 00100000, which is 0x20 in hex.The Numbers: In your Arduino sketch, you use the Wire library to send the configuration byte. You type:Wire.write(20);
The Outcome: The code compiles perfectly. The I2C scanner sees the device at 0x40. But when you command the servos to move, nothing happens. The PCA9685 is completely unresponsive to PWM commands.
What Went Wrong: You wrote decimal 20 instead of hex 0x20.
Decimal 20 is 00010100 in binary. By sending this, you accidentally set Bit 4 to 1. In the PCA9685 datasheet, Bit 4 is the SLEEP bit. You just commanded the chip to go to sleep, disabling all PWM outputs. Furthermore, you set Bit 2 (SUB2), enabling a sub-address you don't need.
The Fix: Always use explicit base prefixes in your firmware to prevent compiler assumptions.
- Use
0xfor hexadecimal:Wire.write(0x20); - Use
0bfor binary (supported in modern GCC/Arduino):Wire.write(0b00100000); - Use bit-shift macros for readability:
Wire.write((1 << 5));(Shifts a 1 into the 5th bit position).
Common Confusions and Debugging FAQ
Why does my I2C scanner show 0x78, but the sensor datasheet says the address is 0x3C?
This is the most common I2C confusion. The I2C specification defines addresses as 7 bits. However, the 8th bit transmitted on the bus is the Read/Write (R/W) flag. 0x3C is the 7-bit address shifted left by one (binary 0111100 becomes 01111000, which is 0x78 in hex). If your scanner reads the raw 8-bit bus traffic, it shows 0x78 for a write and 0x79 for a read. Always use the 7-bit format (0x3C) in your Arduino Wire.beginTransmission() calls, as the library handles the R/W bit shifting automatically.
Is 0xFF the same as 255, or -1?
In an unsigned 8-bit integer (uint8_t), 0xFF is exactly 255. All 8 bits are HIGH. However, if you store 0xFF in a signed 8-bit integer (int8_t), the processor uses two's complement representation, making it -1. This causes massive bugs when comparing sensor readings. Always use explicitly sized unsigned types (uint8_t, uint16_t) when handling hex register data.
How do I read a hex memory dump from a logic analyzer?
Logic analyzers (like the Saleae Logic Pro 8) capture physical voltage transitions and decode them into hex bytes. If you see 0x55 on a UART line, that is binary 01010101. In serial communications, 0x55 is a classic training byte because its alternating 1s and 0s allow the receiver's clock recovery circuits to perfectly lock onto the baud rate before the actual data payload arrives.






