Fractional to binary conversion is the mathematical process of translating the decimal portion of a base-10 number into a base-2 sequence by repeatedly multiplying the fraction by two and recording the integer carry bits. When you configure an Analog-to-Digital Converter (ADC) on an ESP32, calculate a precise PWM duty cycle, or debug a PID controller, the microcontroller doesn't understand "1.25 Volts" or "0.1". It only understands binary weights. If you don't grasp how decimal fractions map to binary bits, you will inevitably run into quantization errors, steady-state jitter, and inexplicable floating-point math failures in your C++ code.

What this changes in a real circuit is your fundamental limit of precision: it dictates the exact voltage step size your ADC can resolve, the true duty cycle your PWM pin outputs, and whether your sensor readings will suffer from truncation errors. Let's break down the math, look at the real-world voltage equivalents, and cover where this trips up embedded developers.

The Core Algorithm and Worked Examples

Converting the integer part of a decimal number to binary uses repeated division by 2. Converting the fractional part uses repeated multiplication by 2. This is the most common stumbling block for beginners who try to apply the same algorithm to both sides of the decimal point.

The Golden Rule: Multiply the fraction by 2. The integer part (0 or 1) becomes your next binary bit. Keep the new fractional part and repeat until the fraction reaches zero or you hit your bit-depth limit.

Worked Example 1: A Clean Termination (0.6875)

Let's convert the decimal fraction 0.6875 to binary. This is a "clean" number that terminates, much like 0.5 or 0.25 in decimal.

  1. 0.6875 × 2 = 1.375 → Integer is 1. Carry over 0.375.
  2. 0.375 × 2 = 0.75 → Integer is 0. Carry over 0.75.
  3. 0.75 × 2 = 1.5 → Integer is 1. Carry over 0.5.
  4. 0.5 × 2 = 1.0 → Integer is 1. Fraction is 0. Stop.

Reading the integers top-to-bottom, the binary fraction is 0.1011. In a 4-bit fractional system, this perfectly represents $2^{-1} + 2^{-3} + 2^{-4}$ (0.5 + 0.125 + 0.0625 = 0.6875).

Worked Example 2: The Repeating Nightmare (0.1)

Now try converting 0.1 (one-tenth). In decimal, this is a clean, single-digit fraction. In binary, it is an infinitely repeating sequence.

  • 0.1 × 2 = 0.2
  • 0.2 × 2 = 0.4
  • 0.4 × 2 = 0.8
  • 0.8 × 2 = 1.6 (carry 0.6)
  • 0.6 × 2 = 1.2 (carry 0.2)
  • 0.2 × 2 = 0.4 (pattern repeats...)

The binary result is 0.0001100110011... repeating infinitely. This is exactly why 0.1 + 0.2 == 0.3 evaluates to false in C++ on an Arduino or ESP32. The hardware must truncate the infinite binary fraction to fit it into a 32-bit IEEE 754 float, introducing a tiny rounding error that breaks exact equality checks.

Binary Fraction Weights and Real-World ADC Voltages

To understand how these fractions impact your hardware, you need to map binary bit weights to physical voltages. The table below shows the decimal weight of each fractional bit position and translates it to the exact voltage step it represents on a standard 12-bit ADC with a 3.3V reference (like the SAR ADC found on the ESP32-S3).

Bit Position Binary Weight Decimal Fraction Voltage Step (3.3V 12-bit ADC) Cumulative Voltage (if all lower bits = 1)
MSB ($2^{-1}$) 0.1 0.5000 1.6500 V 3.2992 V
Bit 2 ($2^{-2}$) 0.01 0.2500 0.8250 V 1.6492 V
Bit 3 ($2^{-3}$) 0.001 0.1250 0.4125 V 0.8242 V
Bit 4 ($2^{-4}$) 0.0001 0.0625 0.2062 V 0.4117 V
Bit 8 ($2^{-8}$) 0.00000001 0.0039 0.0129 V 0.0258 V
LSB ($2^{-12}$) 0.000000000001 0.000244 0.0008 V (0.8 mV) 0.0008 V

Note: The "Cumulative Voltage" column shows the maximum voltage representable if the MSB is 0 and all subsequent lower bits are 1. This highlights why binary fractions can never perfectly equal the next highest bit weight—they always fall short by exactly 1 LSB (Least Significant Bit).

Where You Meet This in Practice

You don't do this math by hand on the bench, but the consequences of fractional binary limits show up in three specific areas of embedded design.

1. ADC Quantization Error and Sensor Resolution

When an ADC converts an analog voltage to a digital number, it is essentially finding the closest binary fraction that matches the input. According to Texas Instruments' foundational data converter theory, the maximum quantization error is always ±0.5 LSB. If you are using a 10-bit ADC on an Arduino Uno R3 (5V reference), your LSB is $5.0 / 1024 = 4.88$ mV. If your sensor outputs 2.501V, the ADC cannot distinguish it from 2.505V. They both map to the exact same binary integer (512). Understanding the binary fraction weights tells you exactly when you need to upgrade to a 16-bit external ADC like the ADS1115 to resolve smaller physical changes.

2. PWM Duty Cycle Mapping

When you call analogWrite(pin, 127) on an 8-bit PWM timer, you are setting the duty cycle to $127/255$, which is roughly 49.8%. You cannot achieve exactly 50.0% on an 8-bit timer because 127.5 is not a valid integer register value. The binary fraction limit forces a hardware-level rounding error. If your motor driver requires a precise 50.00% duty cycle to prevent drift, you must use a timer with higher bit-depth (like the 16-bit timers on the ESP32) to push the binary fraction error below the physical threshold of the motor's inertia.

3. Floating-Point Math in C++ (IEEE 754)

Microcontrollers store fractional numbers using the IEEE 754 floating-point standard. A 32-bit float dedicates 23 bits to the fractional mantissa. Because numbers like 0.1 result in infinitely repeating binary fractions, the 23-bit limit forces truncation.

The Fix: Never use == to compare floats in Arduino/ESP32 code. Instead of if (voltage == 3.3), use an epsilon threshold: if (abs(voltage - 3.3) < 0.001). Alternatively, do all your math in integers (e.g., measure in millivolts instead of volts) to completely bypass binary fraction truncation.

Common Confusions and Debugging Mistakes

When troubleshooting digital logic or writing firmware, makers frequently conflate binary fractions with other concepts. Here is what people commonly confuse it with:

  • Confusing the Integer Method with the Fractional Method: Makers often try to convert 0.625 by dividing by 2 and tracking remainders. Division is strictly for the left side of the decimal point (the integer). Multiplication is strictly for the right side (the fraction).
  • Confusing Binary Fractions with Fixed-Point Data Types: A binary fraction (like 0.101) is a mathematical concept. Fixed-point arithmetic (like the Q15 format used in DSPs) is a programming technique where you designate a specific bit in a standard integer to act as an implicit decimal point to speed up math on CPUs lacking a Floating Point Unit (FPU).
  • Assuming More Bits Eliminate Rounding: Adding more bits (moving from a 12-bit to a 24-bit ADC) reduces the size of the quantization error, but it does not eliminate the fundamental nature of binary fractions. Irrational numbers and repeating decimals will still require truncation at the 24th bit.

Frequently Asked Questions

Why does my 12-bit ESP32 ADC read 4095 when I apply 3.3V?
A 12-bit ADC has $2^{12} = 4096$ distinct states, numbered 0 to 4095. The maximum binary integer (all 12 bits high) represents the reference voltage minus 1 LSB. It can never output "4096" because that would require a 13th bit.

How do I convert a mixed number like 5.25 to binary?
Split it. Convert the integer (5) using repeated division to get 101. Convert the fraction (0.25) using repeated multiplication to get .01. Combine them: 101.01.

Is it better to use float or double on an ESP32?
On the ESP32 (Xtensa LX6 architecture), both float and double are typically treated as 32-bit single-precision floats in standard Arduino C++ environments unless you specifically invoke 64-bit math libraries, which will severely slow down your loop execution. Stick to 32-bit floats or integer math.