Binary signed numbers are digital representations that use a dedicated most significant bit (MSB) to indicate whether a value is positive or negative, allowing microcontrollers to process data below zero. When you interface with bipolar sensors, motor encoders, or audio DACs, this format changes exactly how you must cast, bit-shift, and scale raw I2C or SPI register bytes before applying them to physical outputs. Most beginners confuse two's complement with sign-magnitude (where the MSB is just a minus sign and the remaining bits are absolute value), leading to catastrophic math errors and inverted motor directions when a value crosses zero.
The Core Mechanism: Two's Complement vs. Sign-Magnitude
In digital logic, we don't have a dedicated '+' or '-' symbol. We only have 1s and 0s. To represent negative numbers, hardware engineers rely almost universally on two's complement. In an 8-bit system, the MSB (bit 7) carries a negative weight (-128) instead of a positive weight (+128). The remaining bits carry their standard positive weights (64, 32, 16, 8, 4, 2, 1).
11111111 (255 in unsigned) acts exactly like 99,999—it represents -1. This mathematical trick allows the microcontroller's ALU (Arithmetic Logic Unit) to use the exact same addition circuitry for both positive and negative numbers without needing a separate subtraction circuit.
What people commonly confuse this with is sign-magnitude, where the MSB simply acts as a flag (1 = negative, 0 = positive) and the other 7 bits hold the absolute value. In sign-magnitude, 10000001 means -1. In two's complement, 10000001 means -127. If you treat a two's complement sensor reading as sign-magnitude in your firmware, your negative values will map to massive, incorrect positive spikes.
Worked Example: Decoding Negative Voltage from an ADS1115 ADC
Let's look at a real-world bench scenario. You are using a Texas Instruments ADS1115 16-bit I2C ADC to measure a bipolar voltage (e.g., reading the shunt resistor on a motor controller that can spin in both directions). The ADC is configured for the ±4.096V full-scale range.
LSB Size: 0.125 mV (Calculated as 4.096V / 32,768 steps).
Suppose the motor reverses, and the actual voltage at the ADC input drops to -2.048V. Here is how the math and the code resolve it:
- Decimal Target: -2.048V / 0.000125V = -16,384 in decimal.
- Binary Representation: The 16-bit two's complement hex value for -16,384 is
0xC000. - I2C Transmission: The ADS1115 sends this as two bytes over the I2C bus: MSB =
0xC0(192), LSB =0x00(0).
If you just read these bytes into standard 8-bit unsigned integers and combine them without proper casting, the compiler will treat 0xC000 as the unsigned decimal 49152. Your firmware will think the voltage is +6.144V, completely missing the reversal. Here is the exact C++ implementation to handle this correctly on an ESP32 or Arduino:
// Read the two bytes from the I2C conversion register
uint8_t msb = Wire.read(); // Returns 0xC0
uint8_t lsb = Wire.read(); // Returns 0x00
// CRITICAL: Cast to int16_t to force the compiler to treat the MSB as a sign bit
int16_t raw_signed = (int16_t)((msb << 8) | lsb);
// raw_signed is now correctly evaluated as -16384
float voltage = raw_signed * 0.000125; // Yields -2.048V
int16_t and int32_t from <stdint.h> rather than generic int. On an 8-bit AVR (Arduino Uno), an int is 16 bits, but on a 32-bit ESP32, an int is 32 bits. Using generic types will cause silent bit-shifting bugs when you migrate your code between microcontroller families.
Where You Meet Binary Signed Numbers in Practice
You will encounter binary signed numbers whenever a physical quantity can cross a zero-boundary in your circuit. The most common jobsite and bench encounters include:
- Bipolar ADCs: Measuring split-rail analog signals (e.g., ±12V audio waveforms or bidirectional current shunts) using chips like the ADS1115 or ADS131E08.
- IMU Accelerometers & Gyros: Sensors like the MPU6050 or BNO055 output signed 16-bit integers. A positive Z-axis acceleration means gravity is pulling down; a negative Z-axis means the board is upside down.
- Quadrature Encoders: When tracking motor position, an encoder counter must increment when spinning clockwise and decrement when spinning counter-clockwise, requiring signed 32-bit integers (
int32_t) to prevent overflow on long travel distances. - Bipolar DACs: Generating negative control voltages for op-amps or audio synthesis using DACs like the TI DAC8568, which accept two's complement SPI payloads to output negative voltages.
Decision Tree: Choosing the Right Signed Format for Your Hardware
While two's complement dominates modern silicon, you will occasionally encounter legacy hardware or specific DSP (Digital Signal Processing) chips that use alternative formats. Use this decision path to configure your firmware and hardware registers correctly.
| Hardware Scenario | Data Format Required | Concrete Implementation / Pick |
|---|---|---|
| Reading standard I2C/SPI sensors (IMUs, Temp, ADCs) | Two's Complement | Cast raw bytes to int16_t or int32_t. |
| Interfacing with legacy 12-bit audio ADCs (e.g., older PCM chips) | Offset Binary (Straight Binary) | Subtract 2048 (half-scale) from the unsigned uint16_t reading. |
| Driving high-speed FPGA DSP cores | Sign-Magnitude | Extract MSB as boolean sign, mask remaining bits for absolute value. |
| Default / Fallback for 99% of Maker & Pro Projects | Two's Complement | Pick: Use int16_t/int32_t and configure DACs for two's complement mode. |
The Final Verdict: Unless you are explicitly reading a datasheet that mandates offset binary for a specific audio codec, always default to two's complement. Configure your DAC control registers to accept two's complement, and rely on C++'s native signed integer types to handle the math. Do not attempt to write custom bit-masking functions to calculate negative values manually; you will introduce edge-case bugs at the zero-crossing boundary.
FAQ: Debugging Signed Integer Bugs on the Bench
Q: My signed variable is stuck at -32768 and won't go lower. What happened?
A: You've hit signed integer underflow. An int16_t has a minimum value of -32,768. If you subtract 1 from it, it wraps around to +32,767. If you are accumulating encoder ticks or integrating gyro drift, upgrade your variable to an int32_t immediately.
Q: I'm bit-shifting a negative number right (>>) and it's filling with 1s instead of 0s. Is my chip broken?
A: No, this is correct behavior called 'arithmetic shift'. In C++, right-shifting a signed negative integer preserves the sign bit by filling the left side with 1s. If you need a logical shift (filling with 0s), cast the variable to an unsigned type (uint16_t) before shifting.
Q: How do I verify my I2C sensor is actually sending two's complement and not sign-magnitude?
A: Put the sensor in an environment where you know the value should be slightly below zero (e.g., an accelerometer tilted slightly past 90 degrees, or an ADC measuring a small negative shunt voltage). If the raw hex output looks like 0xFFFF... (e.g., 0xFFFE), it's two's complement. If the raw hex output looks like 0x8002, it's sign-magnitude.






