Negative numbers in binary are represented in modern digital systems using the two's complement format, where the most significant bit (MSB) indicates the sign and the remaining bits encode the inverted magnitude plus one. If you have ever wired a thermocouple or a bidirectional current shunt to an ESP32 and watched a -10°C reading suddenly report on your serial monitor as 65,526°C, you have just collided with binary sign representation. Understanding how microcontrollers process values below zero is not just a computer science exercise; it dictates how you cast variables in C++, how you configure hardware registers, and whether your over-temperature shutoff logic will actually protect your circuit.
The Core Mechanism: How Two's Complement Actually Works
In digital logic, the Arithmetic Logic Unit (ALU) needs a way to perform subtraction using only addition circuits. Two's complement solves this elegantly by wrapping the number line. In an 8-bit signed integer (int8_t), the range is -128 to +127. The MSB (bit 7) carries a negative weight (-128), while the remaining bits carry positive weights (64, 32, 16, 8, 4, 2, 1).
Let's encode a sensor offset of -14 using the standard two's complement algorithm:
- Start with positive 14:
0000 1110(Hex:0x0E) - Invert all bits (One's Complement):
1111 0001 - Add 1 (Two's Complement):
1111 0010(Hex:0xF2)
When the ALU reads 1111 0012, it evaluates the MSB as -128, and the remaining bits as +64 +32 +16 +2 = +114. The sum is exactly -14. This mathematical symmetry is why two's complement is the universal standard for integer math in every microcontroller from the ATmega328P to the ESP32-S3.
What People Commonly Confuse It With
When debugging sensor arrays, makers frequently misinterpret binary sign data because they confuse two's complement with other encoding schemes or data types.
- Sign-Magnitude Encoding: This is the intuitive but flawed method where you simply flip the MSB to indicate a negative sign (e.g.,
1000 1110for -14). While this is used for the sign bit in IEEE 754 floating-point standards, it is virtually never used for integer ALU math because it creates two distinct zeros (+0 and -0) and breaks basic addition circuits. - Unsigned Integer Overflow: The most common bench mistake is reading a signed sensor register into an unsigned variable (e.g.,
uint16_t). If an ADC outputs -1 (which is1111 1111 1111 1111in 16-bit two's complement), casting it to auint16_tforces the microcontroller to read all bits as positive weights, yielding 65,535. Your code will interpret a slight negative offset as a massive positive spike. - Floating-Point Underflow: Confusing binary integer representation with floating-point mantissa limits. Two's complement applies strictly to integer types (
int8_t,int16_t,int32_t). If you are usingfloatordouble, the binary structure relies on a sign bit, an exponent, and a mantissa, entirely bypassing two's complement rules.
Where You Meet This in Practice: Sensors, ADCs, and Microcontrollers
What negative binary representation changes in a real installation is how you interface with external Analog-to-Digital Converters (ADCs) when measuring bidirectional signals. The internal 12-bit SAR ADC on a standard ESP32 DevKit v1 is strictly unipolar; it only reads 0V to 3.3V, mapping to unsigned integers 0 to 4095. You will never see a negative binary number from analogRead().
However, when you need to measure bidirectional current (e.g., a solar charge controller monitoring battery charge vs. discharge) or differential temperature, you use an external I2C or SPI ADC like the Texas Instruments ADS1115. The ADS1115 features a programmable gain amplifier (PGA) and outputs a 16-bit two's complement word when the differential voltage drops below your baseline reference.
readADC_SingleEnded(), the function returns a uint16_t. If your shunt voltage drops to -0.05V, the raw I2C register holds a two's complement negative, but the library's return type forces an unsigned cast. You must explicitly cast the result to int16_t in your sketch to restore the negative sign before applying your voltage scaling multiplier.
Decision Path: Choosing the Right Data Type and Bit-Width
Selecting the wrong C++ data type for your sensor payload will silently corrupt your data pipeline. Use this decision matrix to lock in the correct variable type and hardware pairing for your next PCB or breadboard build.
| Sensor Output Type | Expected Physical Range | C++ Data Type to Use | Concrete Hardware / Library Pick |
|---|---|---|---|
| Unipolar Analog (0-5V) | 0 to 1023 (10-bit) | uint16_t |
Arduino analogRead() (ATmega328P) |
| Bipolar Analog (-2.5V to +2.5V) | -32,768 to +32,767 | int16_t |
TI ADS1115 with explicit (int16_t) cast |
| High-Res Bipolar (24-bit Sigma-Delta) | -8,388,608 to +8,388,607 | int32_t |
Analog Devices AD7124 (RTD/Load Cell) |
| Unipolar High-Current (0-50A) | 0 to 65,535 | uint32_t |
Allegro ACS71240 Hall Effect Sensor |
int16_t for any 16-bit sensor that can physically read below its zero-baseline, and int32_t for 24-bit ADCs. Never use uint variants for bidirectional current shunts, differential thermocouples, or H-bridge motor current sensing. The memory cost of stepping up to a 32-bit signed integer on an ESP32 is negligible, and it prevents catastrophic overflow errors during bitwise sign-extension.
Debugging Binary Sign Errors on the Bench
When your serial monitor prints massive, seemingly random numbers instead of a small negative value, follow this diagnostic path to isolate the binary sign error.
- Check the Hex Dump: Print the raw ADC register value in hexadecimal using
Serial.println(rawVal, HEX). If the first nibble is between 8 and F (e.g.,0xFFFA), the hardware is correctly outputting a negative two's complement number. - Calculate the Offset: If your serial output is exactly
65536 - X(where X is the small positive number you expected to see as negative), you have an unsigned cast error in your C++ code. Change your variable declaration fromuint16_ttoint16_t. - Verify 24-Bit Sign Extension: If you are reading a 24-bit ADC (like the AD7124 or HX711) into a 32-bit integer, the microcontroller will pad the upper 8 bits with zeros. If the 24-bit value is negative (MSB is 1), padding with zeros turns it into a massive positive 32-bit number. You must perform an arithmetic bitwise shift to force the sign bit to propagate.
Here is the exact C++ snippet to correctly sign-extend a 24-bit two's complement value packed into a 32-bit unsigned integer:
// Assuming 'raw_24bit' is a uint32_t holding the 24-bit ADC data
int32_t signed_val = (raw_24bit << 8) >> 8;
This shifts the 24-bit value to the MSB of the 32-bit container, then performs an arithmetic right shift, which automatically fills the upper 8 bits with the correct sign bit (1s for negative, 0s for positive).
Frequently Asked Questions
Why don't microcontrollers just use a dedicated sign bit for integers like they do for floats?
Using a dedicated sign bit (sign-magnitude) requires the ALU to have separate, complex logic circuits for addition and subtraction, and it results in two distinct binary representations for zero (+0 and -0). Two's complement allows the ALU to use a single, simple adder circuit for both addition and subtraction, and ensures there is only one zero, saving silicon die space and clock cycles.
Can I read negative voltages directly into an Arduino Uno's analog pins?
No. The ATmega328P's internal ADC is strictly unipolar (0V to VCC). Feeding a negative voltage into an Arduino analog pin will forward-bias the internal ESD protection diodes, potentially destroying the microcontroller's GPIO bank. You must use an external differential ADC like the ADS1115 or an op-amp level-shifter circuit to offset the negative voltage into a positive range before it reaches the Arduino pin.
Does two's complement apply to I2C and SPI communication protocols?
The protocols themselves are agnostic; they just shift bits down the wire. However, the sensor's datasheet will specify how those bits are formatted. If the datasheet states the data register is 'two's complement', the master microcontroller must interpret the received byte array using signed integer casting, regardless of whether it arrived via I2C, SPI, or UART.






