Sign in binary is the system of representing positive and negative integers in digital logic, almost universally implemented using the two's complement format where the most significant bit (MSB) acts as a negative weight. If you are reading a negative voltage from an ADC, parsing an I2C accelerometer, or processing audio waves on an ESP32, the raw hex bytes mean nothing unless you understand how the hardware encodes that minus sign. Misinterpreting this encoding is the number one reason hobbyists see massive, seemingly random positive numbers when a sensor should be reading a value below zero.

The Core Mechanism: Two's Complement vs. Sign-Magnitude

When humans write '-5', we use a minus sign and a magnitude. Early computer engineers tried to mimic this with sign-magnitude encoding, where the leftmost bit is simply a 1 for negative and 0 for positive, while the remaining bits hold the absolute value. It failed in practice because it created two zeros (+0 and -0) and required complex, separate subtraction logic in the ALU (Arithmetic Logic Unit).

Modern microcontrollers—from the ATmega328P in an Arduino Uno to the Xtensa LX6 cores in an ESP32—use two's complement. In this system, the MSB doesn't just flag a negative state; it carries a negative mathematical weight. In an 8-bit signed integer, the bits represent weights of -128, 64, 32, 16, 8, 4, 2, and 1. This elegant trick means subtraction is performed using the exact same hardware adder circuits as addition.

Decimal Value Unsigned (8-bit) Sign-Magnitude Two's Complement (Standard)
+50000 01010000 01010000 0101
-5N/A (Reads as 251)1000 01011111 1011
+1270111 11110111 11110111 1111
-127N/A (Reads as 129)1111 11111000 0001
-128N/A (Reads as 128)N/A (No -0)1000 0000

Notice the asymmetry in the two's complement column: an 8-bit signed integer ranges from -128 to +127. There is one more negative value than positive because zero occupies the 'positive' side of the boundary. What this changes in a real circuit is how you define your variables in C/C++. If you assign a 16-bit sensor reading to a uint16_t instead of an int16_t, a reading of -1 will wrap around to 65,535, completely breaking your control loop.

Worked Example: Reading Negative Voltages on an ADS1115 ADC

Let's look at a real bench scenario. You are using a Texas Instruments ADS1115 16-bit ADC to measure a bipolar signal (like a current shunt reading) that swings between +4.096V and -4.096V. The ADS1115 outputs data in 16-bit two's complement format.

Target Measurement: -1.250V
ADC Range: ±4.096V
Resolution: 4.096V / 32,768 = 0.125mV per LSB

First, we calculate the expected raw decimal value. Divide the target voltage by the LSB resolution: -1.250V / 0.000125V = -10,000. The ADC needs to output the two's complement binary equivalent of -10,000.

  1. Start with positive 10,000: In 16-bit binary, this is 0010 0111 0001 0000 (Hex: 0x2710).
  2. Invert all bits (One's Complement): 1101 1000 1110 1111 (Hex: 0xD8EF).
  3. Add 1: 1101 1000 1111 0000 (Hex: 0xD8F0).

When your microcontroller reads the I2C bus, it receives two bytes: 0xD8 (high byte) and 0xF0 (low byte). If you bitwise-shift and combine them into an unsigned 16-bit integer (uint16_t), the microcontroller calculates: (0xD8 * 256) + 0xF0 = 55,536.

The 55,536 Bug: I see this constantly on forums. A user prints '55536' to the serial monitor and assumes their ADC is fried or returning noise. The hardware is fine; the software is treating the negative weight of the MSB as a positive +32,768.

To fix this, you must explicitly cast the combined raw bytes to a signed integer in your embedded C++ code:

uint16_t raw_unsigned = (Wire.read() << 8) | Wire.read();
int16_t signed_value = (int16_t)raw_unsigned; // Forces two's complement interpretation
float actual_voltage = signed_value * 0.000125; // Yields -1.250V

Where You Meet Sign in Binary in Practice

Understanding signed binary isn't just an academic exercise; it dictates how you wire and code several common embedded systems.

1. I2S Audio Processing on the ESP32

Audio waveforms are AC signals that oscillate above and below a zero-crossing baseline. When you configure the ESP-IDF I2S driver to read from an INMP441 MEMS microphone, the data arrives as 32-bit signed integers. Silence is represented by values hovering tightly around 0x00000000. If you mistakenly configure the I2S DMA buffer to use unsigned integers, the 'silence' baseline shifts to 2,147,483,648, and any DSP filtering you apply will instantly overflow and crash the audio stream.

2. Quadrature Encoders and Motor Control

When tracking the position of a DC motor with a quadrature encoder, you need to know direction. Clockwise increments a counter; counter-clockwise decrements it. Using a signed 32-bit integer (int32_t) for your step counter allows the position to seamlessly cross from +1 down through 0 into -1 without requiring complex conditional logic to track direction states.

3. Sensor Fusion (MPU6050 / BNO055)

Accelerometers and gyroscopes measure vectors in 3D space. Gravity pulling down on the Z-axis might read +16,384, while flipping the board upside down yields -16,384. The sign bit tells your Kalman filter which way is 'down'.

Troubleshooting Signed Math Bugs in Embedded C

Even when you correctly define your variables as signed, the C/C++ compiler can introduce subtle bugs if you mix signed and unsigned math. Here is a decision path for the most common signed binary errors.

Symptom: Bit-shifting a negative number yields unexpected results

The Cause: You are using the right-shift operator (>>) on a signed negative integer. According to C++ arithmetic operator standards, right-shifting a signed negative value is implementation-defined. Some compilers perform an arithmetic shift (filling the left side with 1s to preserve the negative sign), while others perform a logical shift (filling with 0s, turning your negative number into a massive positive one).

The Fix: Never use bitwise shifts for division on signed integers. If you need to divide a signed sensor reading by 4, use the division operator (val / 4). The compiler will optimize it to an arithmetic shift automatically, guaranteeing safe behavior across AVR, ARM, and Xtensa architectures.

Symptom: A signed calculation suddenly wraps to a massive positive number

The Cause: Implicit type promotion. If you multiply an int16_t sensor reading by an int scaling factor, the compiler promotes the 16-bit value to a 32-bit signed integer. However, if you multiply it by a uint16_t, the compiler promotes both to an unsigned 32-bit integer. The sign bit is instantly lost.

The Fix: Explicitly cast your operands before the math operation. Write (int32_t)signed_sensor * (int32_t)unsigned_scale to force the compiler to maintain the two's complement weight throughout the calculation.

Bench Rule of Thumb: Default to unsigned (uint8_t, uint16_t, uint32_t) for everything in embedded C—pin states, raw ADC counts, PWM duty cycles, and timers. Only switch to signed (int16_t, int32_t) at the exact boundary where a physical measurement crosses zero, like a bipolar voltage, an audio sample, or a bidirectional motor speed.