Signed binary is a base-2 numbering system that reserves the most significant bit (MSB) to indicate polarity, allowing microcontrollers to natively represent and calculate negative integers. In practical embedded C++, this changes everything about how you declare variables and parse I2C/SPI payloads; if you assign a signed sensor reading to an unsigned variable, a simple -10°C temperature reading will wrap around and report as 65526, silently breaking your control logic. Most hobbyists confuse signed binary with 'sign-magnitude' representation (where a single bit acts as a simple plus/minus sign), but modern microcontrollers like the ATmega328P and ESP32 exclusively use a mathematical format called two's complement.
The Core Mechanism: Two's Complement vs. Sign-Magnitude
Humans write negative numbers with a minus sign in front. CPUs do not have a 'minus' symbol in their hardware logic gates. Instead, they use two's complement to represent signed binary numbers. In this system, positive numbers look exactly like standard unsigned binary. Negative numbers are created by flipping all the bits of the positive equivalent (one's complement) and adding one.
What people commonly confuse this with is sign-magnitude, where the MSB is just a flag (0 for positive, 1 for negative) and the remaining bits hold the absolute value. If microcontrollers used sign-magnitude, you would have two different binary representations for zero (+0 and -0), which breaks hardware equality checks. Two's complement ensures there is only one zero, making hardware math efficient.
Worked Numeric Example: Parsing a 16-Bit I2C Sensor
Let us look at a real-world failure mode. You are reading the X-axis accelerometer data from an MPU6050 IMU over I2C. The sensor outputs a 16-bit signed integer split across two 8-bit registers: a High Byte and a Low Byte.
Suppose the sensor is tilted slightly backward, generating a negative G-force. The I2C bus returns the following raw bytes:
- High Byte:
0xE0(Decimal 224) - Low Byte:
0x00(Decimal 0)
To combine them, you shift the high byte left by 8 bits and bitwise-OR it with the low byte:
uint8_t high = 0xE0;
uint8_t low = 0x00;
uint16_t raw_unsigned = (high << 8) | low; // Results in 0xE000 (57344)
int16_t raw_signed = (high << 8) | low; // Results in 0xE000 (-8192)
If you declared your variable as uint16_t (unsigned), the microcontroller reads 0xE000 as 57,344. Your code will think the sensor is experiencing massive, impossible physical forces. By declaring it as int16_t (signed), the compiler recognizes that the MSB (the 1 in 0xE0) is the sign bit. It applies two's complement decoding and correctly yields -8,192.
You can verify this math manually: the two's complement of 0xE000 is found by flipping the bits (0x1FFF) and adding 1 (0x2000), which equals 8,192 in decimal. Because the MSB was 1, the final value is -8,192.
Where You Meet Signed Binary in Practice
You will rarely write raw two's complement math from scratch, but you must understand signed binary to configure the correct data types in the following hardware scenarios:
- Differential ADCs (e.g., ADS1115): When measuring voltages that can swing below your ground reference (like AC waveforms or shunt resistors in H-bridges), the ADC outputs signed 16-bit or 24-bit integers. Using an unsigned type will map negative voltages to massive positive numbers.
- Quadrature Encoders: Motor encoders track relative position. If a motor reverses, the encoder count must decrement into negative numbers. Signed 32-bit integers (
int32_t) are mandatory here to prevent underflow wrapping when the motor changes direction near the zero mark. - Temperature Sensors Below Freezing: Digital sensors like the DS18B20 output signed 12-bit data. A reading of -5°C is transmitted as
0xFFB0. If parsed as unsigned, your weather station will report 65456°C. - PID Control Loops: The 'Error' term in a PID controller (Setpoint - Process Variable) is inherently signed. If your process variable exceeds the setpoint, the error must go negative to tell the actuator to back off.
Decision Path: Picking the Right C++ Data Type
Choosing between int, long, and int16_t is a common source of cross-platform bugs. An int is 16 bits on an Arduino Uno (AVR), but 32 bits on an ESP32 (Xtensa). To guarantee consistent signed binary parsing, you must use fixed-width integers from the <stdint.h> library.
| If your sensor/math outputs... | Then use this C++ Type... | Why this pick? |
|---|---|---|
| 8-bit I2C/SPI registers (e.g., LIS3DH in 8-bit mode) | int8_t |
Maps exactly to one byte; handles -128 to +127 natively. |
| 16-bit I2C/SPI registers (e.g., MPU6050, ADS1115) | int16_t |
Prevents AVR/ESP32 'int' size mismatch; handles -32,768 to +32,767. |
| 24-bit Delta-Sigma ADCs (e.g., HX711 load cells) | int32_t |
24-bit signed data must be stored in a 32-bit container with sign-extension. |
| Accumulating encoder ticks or PID error sums | int32_t |
Prevents overflow when adding multiple 16-bit sensor readings together. |
int or long in your sensor code. Standardize on int16_t for all raw 16-bit hardware register reads, and immediately cast to int32_t before performing any multiplication, division, or accumulation. This single habit eliminates 99% of signed binary overflow bugs across both 8-bit and 32-bit microcontrollers.
Three Fatal Bitwise Mistakes (And How to Fix Them)
Even with the right data type, manipulating signed binary data at the bit level can trigger undefined behavior or silent math errors in C++.
1. The 8-to-16 Bit Sign Extension Trap
If you read an 8-bit signed value (0xFF, which is -1) and assign it directly to a 16-bit unsigned variable, it becomes 0x00FF (+255). The sign is lost. You must cast it to a signed 8-bit integer first, so the compiler performs sign extension (copying the MSB into the new upper bits), turning it into 0xFFFF (-1 in 16-bit).
int8_t raw_8bit = 0xFF; // -1
int16_t correct_16bit = raw_8bit; // Compiler sign-extends to 0xFFFF (-1)
uint16_t wrong_16bit = (uint8_t)raw_8bit; // Truncates sign, becomes 0x00FF (255)
2. Right-Shifting Signed Integers
In C++, right-shifting (>>) a negative signed integer is implementation-defined. On most compilers, it performs an 'arithmetic shift' (filling the left side with 1s to preserve the negative sign), but you should never rely on this for critical math. If you need to divide a signed number by a power of two, use the division operator (/). If you must bit-shift, cast to unsigned, shift, and cast back.
3. Overflowing the Absolute Minimum
An 8-bit signed integer (int8_t) ranges from -128 to +127. Notice that the negative side holds one extra value. If you try to run abs(-128) or multiply -128 by -1, the result (+128) cannot fit in an int8_t. It will overflow and wrap back to -128. Always cast to a wider data type before applying absolute value functions to signed binary sensor floors.
Frequently Asked Questions
Why does my ESP32 print negative I2C sensor data as massive positive numbers?
The ESP32 is a 32-bit architecture. If you use the generic int data type, it allocates 32 bits. If you bitwise-OR two 8-bit bytes into a 32-bit unsigned container without explicitly casting to int16_t, the upper 16 bits remain zero. The compiler sees a positive 32-bit number. Always use int16_t for the intermediate combination step.
Can I just use floating-point (float) for all sensor math to avoid signed binary issues?
No. While float handles negatives natively, converting raw ADC integers to floats immediately after reading wastes CPU cycles and memory bandwidth. Parse the raw signed binary into an int16_t or int32_t first, apply any digital filters or accumulations using integer math, and only cast to float at the very end when calculating the final physical unit (like Celsius or G-force).






