Binary is a base-2 numbering system where each digit (bit) represents a power of two, using only 0 (low voltage/off) and 1 (high voltage/on) to encode all data in digital electronics. When you are representing numbers in binary on a microcontroller, this mathematical reality directly dictates your analog-to-digital converter (ADC) resolution, your RAM footprint, and the physical width of your data buses.
In a real circuit or installation, the binary representation you choose changes how much physical memory your variables consume and the exact maximum threshold your sensors can read before clipping or overflowing. The most common mistake hobbyists make is confusing binary numeric values (the abstract math of 1s and 0s) with binary logic levels (the physical voltage on the wire, such as 3.3V TTL versus 5V CMOS). A secondary, equally destructive confusion is conflating unsigned integers with signed two's complement representations, which routinely leads to mysterious negative numbers and inverted motor directions in sensor logs.
The Core Math: Powers of Two and Bit Weight
Unlike the base-10 (decimal) system where each column represents powers of 10 (ones, tens, hundreds), binary columns represent powers of 2. The rightmost bit is the Least Significant Bit (LSB) representing 2^0 (1), and the bits scale up to the left: 2, 4, 8, 16, 32, 64, 128, and so on.
Worked Example: ADC Resolution and Register Overflow
Let's look at a real-world scenario using the 12-bit ADC on an ESP32 microcontroller. A 12-bit ADC can output binary values from 000000000000 (0) to 111111111111 (4095).
Suppose your analog light sensor outputs a reading of 3482. Let's convert this to binary by subtracting the largest powers of two that fit:
- 3482 - 2048 (2^11) = 1434 (Bit 11 = 1)
- 1434 - 1024 (2^10) = 410 (Bit 10 = 1)
- 410 - 256 (2^8) = 154 (Bit 8 = 1)
- 154 - 128 (2^7) = 26 (Bit 7 = 1)
- 26 - 16 (2^4) = 10 (Bit 4 = 1)
- 10 - 8 (2^3) = 2 (Bit 3 = 1)
- 2 - 2 (2^1) = 0 (Bit 1 = 1)
The resulting 12-bit binary number is 110110011010.
The Failure Mode: If you declare your variable as an 8-bit unsigned integer (uint8_t) in your Arduino or ESP-IDF code, the compiler only allocates 8 bits of memory. When the 12-bit ADC result is written to this variable, the top four bits (1101) are permanently discarded. The microcontroller stores only the lower 8 bits: 10011010.
When your code later reads this variable, it evaluates 10011010 as 154. Your PID control loop or data logger will suddenly think the light level dropped from 3482 to 154, triggering a massive, uncommanded system response. This is why understanding binary width is a safety-critical skill in embedded systems.
Where You Meet Binary in Practice
You will encounter binary representation constraints constantly on the workbench, particularly in three areas:
1. I2C Address Shifting
I2C uses a 7-bit addressing scheme, meaning there are 128 possible addresses (0 to 127). However, when you look at an I2C transaction on a logic analyzer, the address byte is 8 bits long. Why? Because the master shifts the 7-bit address left by one position and inserts the Read/Write bit into the LSB. If your sensor datasheet lists the I2C address as 0x3C (binary 0111100), the actual byte sent on the wire for a Write operation is 01111000 (0x78). The NXP I2C-bus specification explicitly details this shift, yet it remains a top reason makers fail to initialize sensors.
2. Direct Port Manipulation
When you need to toggle multiple pins simultaneously without the overhead of digitalWrite(), you write directly to the microcontroller's hardware registers. On an ATmega328P (Arduino Uno), writing PORTD = B10101010; instantly sets pins D7, D5, D3, and D1 high, and D6, D4, D2, and D0 low. The 'B' prefix tells the compiler to interpret the following digits as a binary literal rather than decimal.
3. Bitwise Masking for Status Registers
Sensors often pack multiple flags into a single 8-bit status register. If bit 3 indicates a 'Data Ready' flag, you cannot just read the whole byte and check if it equals 8. You must use a bitwise AND mask: if (status_byte & (1 << 3)). This isolates the binary representation of that specific bit. For a deeper dive into these operators, review the official Arduino bitwise documentation.
Decision Tree: Picking the Right Data Type
Choosing the correct C/C++ data type ensures your binary numbers have enough physical bits to live in without wasting precious SRAM. Use this decision path to select your variable type:
| If your binary data... | And the maximum absolute value is... | Then pick this exact type |
|---|---|---|
| Is strictly positive (e.g., ADC counts, PWM duty cycle) | < 256 (8 bits) | uint8_t |
| Is strictly positive (e.g., 10/12-bit ADC, millisecond timers) | < 65,536 (16 bits) | uint16_t |
| Requires negative numbers (e.g., IMU acceleration, temperature) | Between -32,768 and 32,767 | int16_t |
Requires large positive counters (e.g., millis(), pulse counts) |
> 65,535 | uint32_t |
| Requires large negative/positive ranges (e.g., GPS coordinates) | > 32,767 or < -32,768 | int32_t |
int16_t using bitwise shifts: int16_t val = (msb << 8) | lsb;. If you use a standard unsigned integer, negative physical movements will wrap around to massive positive numbers (e.g., -1 becomes 65535).
FAQ: Binary Edge Cases on the Workbench
What is Two's Complement and why does it matter?
Two's complement is the standard method microcontrollers use to represent negative numbers in binary. Instead of wasting a bit just for a 'minus sign', the most significant bit (MSB) acts as a negative weight. In an 8-bit signed integer (int8_t), the MSB represents -128, not +128. Therefore, 11111111 is not 255; it is -1. If you cast a signed 8-bit sensor reading to an unsigned 16-bit integer without explicitly casting it through a signed type first, the compiler will pad the high byte with zeros instead of ones, destroying the negative value.
Does the order of binary bytes matter (Endianness)?
Yes. When a 16-bit or 32-bit binary number is transmitted over a serial bus like UART, SPI, or I2C, it must be broken into 8-bit chunks. 'Big-Endian' sends the Most Significant Byte (MSB) first, while 'Little-Endian' sends the Least Significant Byte (LSB) first. ARM Cortex-M chips (like the STM32 or RP2040) are typically Little-Endian, while network protocols and many I2C sensors are Big-Endian. If your 16-bit sensor reads exactly 256 when you expect 1, you have an endianness mismatch and need to swap the byte order in your code.
Why do we use Hexadecimal if the hardware only uses Binary?
Because reading a 32-bit binary string like 11011001101011110000101011001110 is impossible for human debugging. Hexadecimal (base-16) is simply a shorthand for binary. Every single hex digit perfectly represents exactly four binary bits (a 'nibble'). 0xD9AF0ACE is mathematically identical to the binary string above, but it maps directly to the physical byte boundaries in your microcontroller's memory.
When writing embedded C/C++, never rely on the compiler's default int type, as its binary width changes depending on whether you are compiling for an 8-bit AVR or a 32-bit ESP32. Always default to explicitly sized types like uint16_t for 10/12-bit ADCs and int16_t for physical sensors, explicitly casting down to 8-bit variables only when SRAM is critically constrained and you have mathematically verified the maximum bounds of your data.






