Two's complement is a binary mathematical operation used to represent negative numbers in digital systems by inverting the bits of a positive number and adding one, allowing microcontrollers to perform subtraction using standard addition circuits. If you are reading raw bytes from an I2C accelerometer or parsing a signed ADC value, this is the underlying format your silicon is using to distinguish a positive voltage from a negative one. Think of a mechanical car odometer that rolls backward past 000000 to 999999; the system inherently understands that 999999 represents -1 mile without needing a separate 'negative' symbol.
The Core Mechanism: A Worked Numeric Example
To see how this works on the bench, let's look at an 8-bit system. In 8-bit binary, you have 256 possible states (0 to 255). Two's complement splits this range down the middle: 0 to 127 for positive numbers, and -1 to -128 for negative numbers.
Let's convert +42 to -42 using the standard two's complement algorithm.
- Start with the positive binary value: +42 in 8-bit binary is
0010 1010. - Invert all bits (One's Complement): Flip every 1 to 0, and every 0 to 1. This gives
1101 0101. - Add 1 to the result:
1101 0101+0000 0001=1101 0110.
The binary value 1101 0110 is the two's complement representation of -42. If you add the binary for +42 and -42 together, you get 1 0000 0000. Because we are in an 8-bit system, the 9th bit (the carry-out) is simply discarded, leaving 0000 0000. The math works perfectly without the Arithmetic Logic Unit (ALU) needing to know whether it was adding or subtracting.
What Two's Complement Changes in Real Hardware
In a physical microcontroller, two's complement fundamentally changes how the ALU is wired and how the Most Significant Bit (MSB) is interpreted.
Furthermore, two's complement changes the mathematical weight of the MSB. In standard unsigned binary, the MSB (bit 7 in an 8-bit byte) has a positive weight of $2^7$ (128). In two's complement, the MSB acts as the sign bit, but it carries a negative weight of $-2^7$ (-128).
Let's evaluate 1101 0110 using this hardware logic:
- Bit 7 (MSB): 1 × (-128) = -128
- Bit 6: 1 × 64 = +64
- Bit 4: 1 × 16 = +16
- Bit 2: 1 × 4 = +4
- Bit 1: 1 × 2 = +2
- Sum: -128 + 64 + 16 + 4 + 2 = -42.
Where You Meet This in Practice
You will encounter two's complement constantly when moving past simple digital outputs and into analog measurement and spatial sensing. Common encounters include:
- Bidirectional Current Sensing: Shunt monitors like the INA219 output two's complement values to indicate current flowing in reverse (e.g., regenerative braking or battery charging).
- Signed ADCs: High-resolution ADCs like the ADS1115 use 16-bit two's complement to represent voltages below the reference ground when configured for differential inputs.
- IMUs and Accelerometers: Chips like the MPU6050 report G-forces and rotational velocities. A tilt to the left might yield a positive integer, while a tilt to the right yields its two's complement negative equivalent.
- Motor Encoders: Quadrature encoders tracking position relative to a home index will output negative tick counts when the motor reverses direction.
Bench Scenario: The 65,036 Milliamp Bug
Understanding this concept isn't just academic; failing to handle it correctly in your firmware will result in catastrophic logic errors. Here is a real-world scenario from the bench.
The Numbers: The battery begins charging. The physical current is -0.500A. The ADS1115 correctly samples the negative voltage drop and outputs the raw 16-bit hex value 0xFE0C.
The Outcome: Your serial monitor prints a massive current spike: 65036 mA. Your overcurrent protection routine panics and disconnects the contactor, halting the charge cycle.
What Went Wrong: The I2C library you used to read the ADC returns a 16-bit unsigned integer (uint16_t) by default. The raw bytes 0xFE0C equal 65,036 in unsigned decimal. Because you stored the result in an unsigned variable, the compiler never applied the two's complement sign-extension logic.
The Fix: You must explicitly cast the raw register read to a signed 16-bit integer (int16_t) before doing any floating-point math. In C/C++, casting 0xFE0C to an int16_t tells the compiler to treat the MSB as a negative weight, instantly resolving the value to -500. As noted in the Arduino data type documentation, standard 16-bit ints are inherently signed and store values from -32,768 to 32,767 using this exact format.
Common Confusions: Sign-Magnitude vs. Two's Complement
Beginners often confuse two's complement with other binary signing methods. While two's complement is the undisputed standard in modern microcontrollers (from 8-bit ATmegas to 32-bit ARM Cortex chips), older systems or specific data formats sometimes use alternatives.
| Format | How it Works | 8-Bit Representation of -1 | Major Drawback |
|---|---|---|---|
| Two's Complement | Invert bits, add 1. MSB has negative weight. | 1111 1111 |
Asymmetric range (-128 to +127). |
| Sign-Magnitude | MSB is strictly a sign flag (1=negative). Remaining bits are absolute value. | 1000 0001 |
Requires complex ALU logic; has two zeros (+0 and -0). |
| One's Complement | Simply invert the bits of the positive number. | 1111 1110 |
Has two zeros; requires 'end-around carry' for addition. |
Notice that in Sign-Magnitude, 1000 0001 means -1. If you feed that byte into an ALU expecting two's complement, it will interpret it as -127. Always check the sensor datasheet to confirm the data format; while 99% of modern silicon uses two's complement, some specialized optical sensors or legacy protocols still use offset-binary or sign-magnitude.
Frequently Asked Questions
Why does an 8-bit two's complement system go down to -128 but only up to +127?
This is the 'asymmetric range' quirk of two's complement. Zero occupies one of the positive states (0000 0000). Because there are 256 total states, splitting them leaves 127 positive states, zero, and 128 negative states. The value -128 is represented as 1000 0000. If you try to invert it and add 1 to find its positive counterpart, it overflows back to 1000 0000, which is why +128 cannot exist in an 8-bit signed integer.
How do I manually check if a hex value is negative in two's complement?
Look at the most significant hex digit. In a 16-bit number, if the first hex character is 8, 9, A, B, C, D, E, or F, the MSB (bit 15) is a 1. This means the number is negative. For example, 0x8000 is -32,768, while 0x7FFF is +32,767. For a deeper mathematical breakdown of this boundary, refer to the comprehensive two's complement documentation on Wikipedia.
Does two's complement apply to floating-point numbers?
No. Floating-point numbers (like the float data type in C++) use the IEEE 754 standard, which relies on a dedicated sign bit, an exponent, and a mantissa. Two's complement is strictly used for fixed-point integers. When you cast a signed integer to a float, the compiler's FPU (Floating Point Unit) handles the translation out of two's complement and into IEEE 754 format automatically.






