Signed binary to decimal conversion is the mathematical process of translating a binary number where the most significant bit (MSB) indicates polarity into a standard base-10 integer that includes negative values. In a real circuit, this conversion changes how your microcontroller scales bidirectional analog signals—like a shunt resistor measuring both charge and discharge current—preventing a reverse flow from registering as a massive positive spike. Makers commonly confuse two's complement arithmetic with sign-magnitude encoding (where only the MSB flips) or fail to recognize unsigned overflow when a negative sensor value is forced into a uint16_t variable.

The Core Concept: Translating Polarity in Firmware

Microcontrollers do not natively understand negative numbers; they only see sequences of 1s and 0s. To represent negative values, modern silicon relies almost exclusively on two's complement notation. In this system, the MSB acts as a sign bit (0 for positive, 1 for negative), but the remaining bits are not a simple magnitude. Instead, the entire binary word is weighted such that the MSB carries a negative weight.

Why Two's Complement?
Two's complement is used because it allows the ALU (Arithmetic Logic Unit) to use the exact same addition circuitry for both positive and negative numbers. Subtracting 5 from 10 is mathematically identical to adding -5 to 10, eliminating the need for separate subtraction hardware.

When you read a raw register from an I2C or SPI sensor, you are pulling an unsigned integer (e.g., uint16_t). If the sensor measures a bidirectional physical property (temperature below zero, reverse current, audio waveforms), the raw value for a negative measurement will appear as a massive positive number. Converting this binary to decimal signed format requires instructing your compiler to interpret that MSB as a negative weight.

Worked Numeric Example: Decoding a 16-Bit Bidirectional Sensor

Let's look at a real-world scenario using the Texas Instruments INA219 high-side current sensor. The INA219 measures shunt voltage, which can be positive (current flowing to the load) or negative (current flowing back to the source, like regenerative braking).

Suppose your ESP32 reads the Shunt Voltage Register over I2C and returns the raw 16-bit hexadecimal value 0xFFE0.

  1. Raw Binary: 1111 1111 1110 0000
  2. Check the MSB: The leftmost bit is 1, meaning this is a negative value.
  3. Invert the bits (One's Complement): 0000 0000 0001 1111 (Hex: 0x001F)
  4. Add 1 (Two's Complement): 0000 0000 0010 0000 (Hex: 0x0020, Decimal: 32)
  5. Apply the Sign: The decimal magnitude is 32, so the signed decimal value is -32.

According to the datasheet, the INA219 shunt voltage LSB is 10µV. Multiplying our signed decimal result (-32) by 10µV gives us a real-world shunt voltage of -320µV. If you had cast this to an unsigned integer, you would have read 65504, resulting in a calculated voltage of +655,040µV—a catastrophic error for your control loop.

Where You Meet This in Practice

You will encounter the need for binary to decimal signed conversion whenever a sensor crosses a zero-boundary. Common hardware scenarios include:

  • Bidirectional Current Sensors: ICs like the INA219, INA226, or ACS712 (when biased at VCC/2 and read via an ADC).
  • Thermocouple Amplifiers: The MAX31855 outputs 14-bit signed data to accommodate sub-zero Celsius temperatures.
  • Precision ADCs: The ADS1115 (16-bit) and ADS1015 (12-bit) output two's complement data when configured for differential input modes.
  • Audio DACs/ADCs: AC-coupled audio signals oscillate above and below a DC bias point, requiring signed integer math for DSP filtering.
Bench Tip: If your serial monitor suddenly prints values like 65432 when you expect -104, you haven't broken your sensor. You are simply viewing a negative two's complement number through an unsigned variable type.

Decision Tree: Handling Signed Data in Embedded C++

How you handle the conversion in C++ (Arduino/ESP-IDF) depends entirely on how the sensor aligns its data within the 16-bit register. Use this decision path to select the correct firmware implementation.

Sensor Data FormatCondition / CheckAction / Code Implementation
16-Bit Native Signed Sensor outputs full 16-bit two's complement (e.g., INA219, ADS1115) Direct Cast:
int16_t signed_val = (int16_t)raw_uint16;
12-Bit Left-Aligned 12-bit data occupies bits 15-4; bits 3-0 are zero (e.g., ADS1015) Shift then Cast:
int16_t signed_val = (int16_t)raw_uint16 >> 4;
(Arithmetic right-shift preserves the sign bit in C++)
12-Bit Right-Aligned 12-bit data occupies bits 11-0; bits 15-12 are zero (e.g., MCP3208) Manual Sign Extension:
if (raw & 0x0800) raw |= 0xF000;
int16_t signed_val = (int16_t)raw;
Arbitrary Bit-Width Data is packed in a non-standard width (e.g., 14-bit MAX31855) Bitwise Mask & Extend:
Shift to align MSB to bit 15, cast to int16_t, then arithmetic shift back down.

Default Recommendation: For 90% of modern I2C sensors, the data is 16-bit native signed. Your concrete pick should always be to read the two bytes into a uint16_t, combine them respecting endianness, and execute a direct C-style cast to int16_t. Let the compiler's ALU handle the two's complement math.

Common Pitfalls: Endianness and Sign Extension

Even when you understand two's complement theory, hardware communication protocols can sabotage your conversion.

The Endianness Trap

I2C and SPI sensors transmit data byte-by-byte. A 16-bit register consists of a Most Significant Byte (MSB) and Least Significant Byte (LSB). If your sensor sends the MSB first (Big-Endian, like the INA219), but your C++ code combines them assuming Little-Endian, you will swap the bytes. A raw value of 0x00FF (255) becomes 0xFF00 (-256 in signed decimal). Always verify the byte order in the sensor's datasheet timing diagrams.

Logical vs. Arithmetic Shifting

When dealing with left-aligned sub-16-bit data (like a 12-bit ADC), you must shift the bits to the right to center them. In C++, if your variable is declared as uint16_t (unsigned), a right shift (>>) is a logical shift, padding the left side with zeros. This destroys your negative sign bit. You must cast the variable to a signed type before shifting, forcing an arithmetic shift that pads the left side with 1s, preserving the negative polarity.

Frequently Asked Questions

Can I just use the map() function in Arduino for signed data?
No. The Arduino map() function uses standard integer math and will overflow or produce wildly inaccurate results if fed raw unsigned representations of negative two's complement numbers. Always cast to a signed integer type before passing the value into map().

Why does my 12-bit right-aligned sensor read positive when it should be negative?
Because bits 15-12 are zero, the compiler sees a standard positive number. You must manually check if bit 11 (the sign bit for a 12-bit number) is high. If it is, you must bitwise-OR the value with 0xF000 to 'extend' the sign into the upper 4 bits before casting to int16_t.

Does this apply to floating-point numbers?
No. Floating-point numbers (IEEE 754) use an entirely different architecture (sign bit, exponent, mantissa). Two's complement and binary to decimal signed conversion rules apply strictly to fixed-point integers.