Two's complement is a binary numbering system used to represent signed integers, where the most significant bit (MSB) acts as a negative weight and negative values are calculated by inverting all bits of the positive number and adding one. If you have ever wired up an I2C temperature sensor or a 3-axis accelerometer to an ESP32, only to see your serial monitor spit out 65436 when you put the sensor in the freezer, you have collided with this exact concept. Understanding the two's complement of binary number is not just a computer science trivia question; it is a mandatory skill for anyone writing firmware for hardware that measures the physical world.

The Core Math: How the Two's Complement of Binary Number Works

In standard unsigned binary, an 8-bit register can hold values from 0 to 255. The bit weights from right to left are 1, 2, 4, 8, 16, 32, 64, and 128. But physical phenomena like temperature, altitude, and motor direction require negative numbers. Silicon designers use two's complement because it allows the Arithmetic Logic Unit (ALU) to use the exact same logic gates for both addition and subtraction, eliminating the need for separate subtractor circuits.

In an 8-bit two's complement system, the MSB (bit 7) changes from a weight of +128 to -128. The remaining bits keep their positive weights. This gives a range of -128 to +127.

The Odometer Analogy: Think of a mechanical car odometer with six digits. When it rolls backward from 000000, it doesn't show a minus sign; it rolls over to 999999. In a computer, 11111111 is the binary equivalent of rolling backward past zero.

To manually calculate the two's complement of a binary number (e.g., finding the 8-bit representation of -5), follow these numbered steps:

  1. Start with the positive binary value: +5 in 8-bit binary is 0000 0101.
  2. Invert all bits (Bitwise NOT): Change every 1 to 0, and every 0 to 1. This gives 1111 1010 (this is the one's complement).
  3. Add 1 to the result: 1111 1010 + 0000 0001 = 1111 1011.
  4. Verify the math: Apply the two's complement weights: -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -5.

Where You Meet This in Practice: Embedded Sensors and ADCs

What does this change in a real circuit or installation? It fundamentally changes how you must declare variables and parse data registers in your microcontroller firmware. When a sensor measures a bipolar signal (like an AC current via a Hall-effect sensor, or sub-zero temperatures), it transmits the data over I2C or SPI as a raw stream of 1s and 0s. The sensor does not send a "minus sign" character; it sends a two's complement bit pattern.

Here is how the bit-width dictates your firmware variable types when reading these registers:

Register WidthC++ Unsigned TypeC++ Signed TypeMin Signed ValueMax Signed Value
8-bituint8_tint8_t-128127
16-bituint16_tint16_t-32,76832,767
24-bituint32_t*int32_t*-8,388,6088,388,607
32-bituint32_tint32_t-2,147,483,6482,147,483,647

*Note: 24-bit ADCs (like the ADS1220) require manual sign-extension in firmware to map into a 32-bit signed integer.

Real-World Debugging Walkthrough: The -10.0°C Bug

Let's look at a worked real-world scenario walkthrough to see what happens when you ignore this math on the workbench.

The Setup: You are building a cold-storage monitor using an ESP32-WROOM-32 and a Texas Instruments TMP117 digital temperature sensor. The TMP117 communicates over I2C and outputs a 16-bit two's complement register, where 1 LSB equals 0.0078125 °C.

The Numbers: You place the sensor in a freezer at exactly -10.0 °C.
To find the expected raw decimal value: -10.0 / 0.0078125 = -1280.
The hex representation of +1280 is 0x0500.
Applying the two's complement of binary number rules: invert 0x0500 to get 0xFAFF, then add 1. The sensor transmits 0xFB00 over the I2C bus.

The Outcome: Your ESP32 reads the two bytes, combines them into a 16-bit variable, and prints the result to the serial monitor. But instead of -1280, your screen prints 64256. When you multiply by the LSB resolution, your dashboard reports the freezer is at +502.0 °C.

What Went Wrong: You declared your variable as an unsigned integer.
uint16_t raw_temp = (msb << 8) | lsb;
Because uint16_t cannot hold negative numbers, the C++ compiler interprets 0xFB00 as a pure positive value (64256). The fix is a simple cast to a signed 16-bit integer, which tells the compiler to treat the MSB as a negative weight:

// The Fix: Cast to signed 16-bit integer
int16_t raw_temp = (int16_t)((msb << 8) | lsb);
float temp_c = raw_temp * 0.0078125; // Correctly yields -10.0

Common Confusions: Sign-Magnitude vs. Two's Complement

What do people commonly confuse this with? Beginners often assume binary handles negative numbers using Sign-Magnitude representation. In sign-magnitude, the MSB is simply a flag (0 for positive, 1 for negative), and the remaining bits hold the absolute value.

For example, in 8-bit sign-magnitude, +5 is 0000 0101 and -5 is 1000 0101. While this looks intuitive to humans, it creates a massive headache in silicon: it results in two distinct representations for zero (0000 0000 for +0, and 1000 0000 for -0). It also requires complex, slow logic gates to handle addition when the signs differ. Two's complement eliminates the dual-zero problem and makes subtraction identical to addition, which is why every modern microcontroller from the ATmega328P to the ARM Cortex-M4 relies on it exclusively.

FAQ: Two's Complement on the Workbench

Q: How do I manually sign-extend a 12-bit ADC reading to a 16-bit integer in C++?
A: If your 12-bit ADC outputs a negative number, the 12th bit (bit 11) is your sign bit. You must check if bit 11 is HIGH. If it is, you bitwise-OR the value with 0xF000 to fill the upper four bits with 1s, preserving the negative weight when cast to an int16_t.

Q: Why does my oscilloscope show the MSB transmitted first on I2C?
A: Most digital sensors (and standard protocols like I2C and SPI) transmit the Most Significant Bit first. This is highly convenient for two's complement because the receiving microcontroller can evaluate the sign bit immediately as the first bit clocks in, before the rest of the byte is even received.

Q: Can I just use the abs() function to fix negative sensor readings?
A: No. If your variable is declared as an unsigned type, the compiler never recognizes the number as negative in the first place. Applying abs() to 64256 just returns 64256. You must fix the data type (int16_t) before applying any math functions. For deeper protocol mechanics, refer to the Espressif I2C API documentation regarding byte ordering.

Mastering the two's complement of binary number bridges the gap between raw electrical signals and usable firmware data. The next time your sensor reports a physically impossible positive spike in a negative environment, skip the hardware debugging, check your variable casts, and let the ALU do the math it was designed to do.