Binary is a base-2 number system where every digit represents a power of two, using only 0s and 1s to map directly to the off/on voltage states of digital logic gates. When you move from wiring simple LEDs to configuring microcontroller registers or debugging serial buses, understanding binary values and number systems fundamentally changes how you interact with physical hardware. It dictates how you format memory addresses, set pin states in bulk, and interpret logic analyzer traces. If you treat hex and binary as abstract math rather than physical voltage maps, you will eventually misconfigure a peripheral or short a driver IC.
The Core Concept: Mapping Voltage to Math
Digital logic does not understand the number '5' or '170'. It only understands voltage thresholds. In standard 3.3V CMOS logic (like the GPIO pins on an ESP32-WROOM-32), a voltage between 0V and 0.8V is read as a logical 0 (LOW), and a voltage between 2.3V and 3.3V is read as a logical 1 (HIGH). The space between those thresholds is the undefined region, where the microcontroller might read either state unpredictably.
Because a single pin can only hold one of two states (0 or 1), we group them into buses or registers (usually 8, 16, or 32 pins wide) to represent larger numbers. The rightmost bit is the Least Significant Bit (LSB), representing 2^0 (1). The next bit is 2^1 (2), then 2^2 (4), and so on. This direct mapping between physical pins and mathematical powers of two is why base-2 is the native language of every digital circuit you will ever build.
Translating the Languages: Binary, Decimal, and Hexadecimal
While the hardware speaks binary, humans are terrible at reading long strings of 1s and 0s. We use decimal (Base-10) for counting and hexadecimal (Base-16) as a compact shorthand for binary. Because 16 is a power of 2 (2^4), exactly four binary bits map to one hexadecimal character.
| Decimal (Base-10) | Binary (Base-2) | Hexadecimal (Base-16) | Hardware Meaning (8-bit port) |
|---|---|---|---|
| 0 | 00000000 | 0x00 | All pins LOW (0V) |
| 85 | 01010101 | 0x55 | Alternating pins HIGH/LOW |
| 170 | 10101010 | 0xAA | Alternating pins LOW/HIGH |
| 255 | 11111111 | 0xFF | All pins HIGH (3.3V/5V) |
Worked Numeric Example: Decoding 170
Let's break down the decimal value 170 into binary. We check which powers of two add up to 170:
- 128 (2^7) fits. (170 - 128 = 42 remaining). Bit 7 =
1 - 64 (2^6) does not fit. Bit 6 =
0 - 32 (2^5) fits. (42 - 32 = 10 remaining). Bit 5 =
1 - 16 (2^4) does not fit. Bit 4 =
0 - 8 (2^3) fits. (10 - 8 = 2 remaining). Bit 3 =
1 - 4 (2^2) does not fit. Bit 2 =
0 - 2 (2^1) fits. (2 - 2 = 0 remaining). Bit 1 =
1 - 1 (2^0) does not fit. Bit 0 =
0
Reading from Bit 7 down to Bit 0, we get 10101010. In hex, we split this into two nibbles: 1010 (10 in decimal, which is A in hex) and 1010 (A). Therefore, 170 decimal = 0b10101010 binary = 0xAA hex.
Where You Meet Binary Values and Number Systems in Practice
You will encounter number system conversions constantly when working with embedded systems. Here are the three most common bench scenarios:
- Direct Register Manipulation: When you need to toggle multiple pins on an ESP32 simultaneously without the overhead of
digitalWrite(), you write directly to the GPIO registers. For example, writing0x00000004to theGPIO_OUT_W1TS_REGsets GPIO 2 HIGH. You can find the exact memory addresses in the ESP32 Technical Reference Manual. - Shift Registers: When expanding your I/O using a chip like the NXP 74HC595, you clock in 8 bits serially. The datasheet specifies the order in which bits are latched, requiring you to format your binary masks correctly to route data to the correct output pin (Q0 through Q7).
- I2C Bus Addressing: Every sensor on an I2C bus has a unique address, usually printed in the datasheet as a 7-bit hex value (e.g.,
0x3Cfor an SSD1306 OLED). Your microcontroller's I2C library handles the translation to the 8-bit bus format, but your logic analyzer will display the shifted values.
Real-World Scenario Walkthrough: The Shift Register Wiring Trap
Abstract definitions don't fry components, but number system typos do. Here is a real-world failure mode that happens when developers forget how compilers interpret numeric literals.
The Setup: You want to turn on Relay 1 (connected to Q0, Bit 0) and Relay 8 (connected to Q7, Bit 7) simultaneously. All other relays must remain off. The target binary pattern is 10000001.
The Numbers: You write the Arduino code to push this data to the shift register:
shiftOut(dataPin, clockPin, MSBFIRST, 10000001);
The Outcome: The relays click erratically. Relay 3 and Relay 4 engage simultaneously. These two relays control the forward and reverse contacts of a motor H-bridge. Engaging both at once creates a direct shoot-through short across the 12V power supply, instantly destroying the L298N motor driver and tripping your bench power supply's overcurrent protection.
What Went Wrong: You forgot the 0b prefix. The C++ compiler did not read 10000001 as binary; it read it as the decimal number ten million and one. The shiftOut function expects an 8-bit byte (0-255). The compiler silently truncated the massive decimal number to its lowest 8 bits.
Ten million and one in binary is 100110001001011010000001. The lowest 8 bits are 11001000 (Decimal 200, Hex 0xC8). This turned on bits 7, 6, 5, and 2 (Relays 8, 7, 6, and 3), causing the catastrophic short.
The Fix: Always explicitly declare your number system using standard prefixes. Use 0b10000001 for binary, or 0x81 for hexadecimal. Never type a raw string of 1s and 0s without the 0b prefix.
What People Commonly Confuse (And How It Breaks Circuits)
The most dangerous confusion in digital electronics is mixing up arithmetic operations with bitwise operations.
If you want to turn on Bit 3 (value 8) and Bit 4 (value 16) on a port register, beginners often try to add them: 8 + 16 = 24. This works only if the bits were previously zero. If Bit 3 was already HIGH (8), and you add 8 to it, the result is 16, which carries over into the next bit, turning off Bit 3 and turning on Bit 4. The hardware state is now wrong.
Instead, you must use the Bitwise OR operator (|). 8 | 16 evaluates the bits independently. If you OR a register with 0x18 (binary 00011000), Bits 3 and 4 are forced HIGH, and every other bit remains exactly as it was. Conversely, to turn a bit off without disturbing the others, you use Bitwise AND (&) combined with a NOT (~) mask.
The I2C Address Shifting Trap
Another frequent point of confusion involves I2C addresses. A sensor datasheet will list the I2C address as 0x3C. However, when you hook up a logic analyzer, the decoded I2C packet shows the address byte as 0x78. Developers often think the sensor is defective or the library is wrong.
The reality is that Texas Instruments' I2C specification defines the address as 7 bits. The 8th bit is the Read/Write flag. To send the 7-bit address 0x3C (binary 0111100) over the 8-bit bus, the hardware shifts it left by one position and appends a 0 for a Write command. 01111000 in binary is 0x78 in hex. Your library handles this automatically, but your logic analyzer shows the raw 8-bit bus reality.
FAQ: Quick Bench Reference for Number Systems
How do I quickly convert hex to binary in my head?
Memorize the hex values for the nibbles 0 through F. Break the hex number into individual characters. For example, 0xE4 becomes E (14 in decimal, which is 1110 in binary) and 4 (0100 in binary). Put them together: 11100100.
Why do some datasheets use octal (Base-8)?
Octal (digits 0-7) was heavily used in early computing systems with 12-bit or 36-bit word lengths because 3 bits perfectly map to one octal digit. In modern 8-bit, 16-bit, and 32-bit microcontroller architectures, octal is virtually obsolete. You will almost exclusively use binary, decimal, and hexadecimal on the workbench today.
What does the '0x' prefix actually mean?
The 0x prefix is a syntactic convention inherited from the C programming language to tell the compiler that the following characters are a hexadecimal number. The 0 originally indicated a numeric constant, and the x stood for 'hexadecimal'. Similarly, 0b denotes binary, and a leading 0 (like 077) historically denotes octal, though you should avoid leading zeros on decimal numbers to prevent accidental octal interpretation.






