A binary fraction is a base-2 number that uses negative powers of two to represent decimal values less than one, separated from the integer part by a binary point. If you are programming an ESP32 to output a precise analog voltage via a DAC, configuring a high-resolution PWM dimmer, or reading an ADC, you are directly manipulating these base-2 fractional values. Understanding how a fraction binary expansion maps to real-world voltages is the difference between a clean, accurate signal and a noisy, offset-ridden mess.
The Core Concept: How Base-2 Fractions Work
In the decimal system, numbers to the right of the decimal point represent negative powers of 10 (tenths, hundredths, thousandths). In digital electronics, hardware registers operate in base-2. Therefore, the digits to the right of the binary point represent negative powers of 2.
What this changes in a real circuit is the absolute minimum voltage step—known as the Least Significant Bit (LSB)—that your digital system can resolve or output. The hardware cannot output a voltage that falls between these binary fraction steps.
2^-1 = 0.5
2^-2 = 0.25
2^-3 = 0.125
2^-4 = 0.0625
2^-8 = 0.00390625
Just like 0.1 in decimal means 1/10, a 1 in the first position after the binary point means 1/2. A 1 in the second position means 1/4. When dealing with a fraction binary representation in fixed-point math, the microcontroller simply shifts bits to multiply or divide by powers of two, making it incredibly fast but inherently limited to values that can be constructed from these specific fractions.
Worked Numeric Example: Converting Decimal 0.6875
Let's convert a base-10 decimal into a binary fraction using the standard multiplication-by-2 method. This is the exact algorithm a compiler uses when you cast a float to a fixed-point register.
- Step 1: Multiply 0.6875 by 2. Result: 1.375. The integer part is 1. Keep the fractional part (0.375).
- Step 2: Multiply 0.375 by 2. Result: 0.75. The integer part is 0. Keep the fractional part (0.75).
- Step 3: Multiply 0.75 by 2. Result: 1.5. The integer part is 1. Keep the fractional part (0.5).
- Step 4: Multiply 0.5 by 2. Result: 1.0. The integer part is 1. The fractional part is 0, so we stop.
Reading the integer parts from top to bottom, the binary fraction is 0.1011.
Verification:
(1 × 2^-1) + (0 × 2^-2) + (1 × 2^-3) + (1 × 2^-4)
= 0.5 + 0 + 0.125 + 0.0625 = 0.6875.
This is a terminating binary fraction. It maps perfectly to a 4-bit hardware register. However, as we will see in the real-world scenario, most common decimal voltages do not terminate so cleanly.
Where You Meet Binary Fractions in Practice
You will encounter base-2 fractional math whenever digital logic interfaces with analog physics. According to fundamental digital-to-analog conversion principles, the mapping is always a ratio of the reference voltage.
- DACs (Digital-to-Analog Converters): When you write a 12-bit value to a Microchip MCP4725 over I2C, the chip uses that integer to construct an analog voltage using an internal resistor ladder weighted by binary fractions.
- ADC Quantization: A 10-bit ADC reading a 5V rail divides the voltage into 1024 steps. Each step is exactly 5/1024 (0.00488V). The ADC rounds the true analog voltage to the nearest binary fraction it can represent.
- PWM Duty Cycles: An 8-bit PWM timer has 256 discrete steps. A 50% duty cycle is exactly 0.10000000 in binary (128/256). But a 33% duty cycle requires a repeating binary fraction, forcing the timer to alternate between 84 and 85 to approximate the average.
Real-World Scenario: The 1.22mV DAC Offset on a Teensy 4.1
Here is what happens when developers assume base-10 math maps cleanly to base-2 hardware registers without checking the fraction binary expansion.
The Setup:
You are building a precision constant-current sink for charging lithium cells. You need the microcontroller (a Teensy 4.1) to output exactly 1.000V to the feedback pin of an op-amp. The Teensy is using an external 12-bit DAC referenced to a precise 3.300V rail. You calculate the required DAC register value in your C++ code.
The Numbers:
Target voltage ratio: 1.000V / 3.300V = 0.30303030... (decimal).
A 12-bit DAC has 4096 steps (0 to 4095).
Required register value: 0.30303030... × 4095 = 1240.9090...
Because the DAC register only accepts integers, the microcontroller truncates the value to 1240.
The Outcome:
The DAC outputs the voltage corresponding to 1240.
Actual Output = (1240 / 4095) × 3.300V = 0.99878V.
Your target was 1.000V. You have an error of 1.22mV. In a precision 4-20mA industrial loop or a sensitive battery charging circuit, this 1.22mV offset translates to a noticeable current drift.
What Went Wrong:
The developer assumed the decimal ratio 1/3.3 could be perfectly represented in hardware. In reality, the fraction binary expansion of 0.303030... is a non-terminating, repeating sequence (similar to how 1/3 is 0.333... in decimal). The 12-bit hardware truncated the infinite binary fraction at the 12th bit, introducing a quantization error. To fix this, you must either calibrate the 3.3V reference slightly higher, use a 16-bit DAC (like the MCP4728) to push the truncation error into the microvolt range, or implement a software calibration offset.
Common Confusions: Binary Fractions vs. IEEE 754 Floating Point
What people commonly confuse binary fractions with is floating-point representation. While both deal with non-integer values, they are fundamentally different in how they are stored and executed in silicon.
| Feature | Fixed-Point Binary Fraction | IEEE 754 Floating Point |
|---|---|---|
| Structure | Integer + implicit binary point | Sign bit + Exponent + Mantissa |
| Hardware Cost | Extremely low (basic ALU shifts) | High (requires FPU or complex software emulation) |
| Precision Behavior | Uniform step size (LSB is constant) | Variable step size (denser near zero, sparse at high values) |
| Primary Use Case | DAC/ADC registers, PWM, motor control | Scientific math, PID controllers, sensor fusion |
When you write float voltage = 1.2; in Arduino IDE, the compiler uses the IEEE 754 standard to pack that number into 32 bits. But the moment you push that value to an analog output pin using analogWrite(), the hardware strips away the IEEE 754 packaging and converts it back into a raw binary fraction mapped to the PWM timer's bit-depth.
FAQ: Binary Fraction Edge Cases in Embedded Systems
Why can't my microcontroller represent 0.1 exactly in binary?
Just as 1/3 results in an infinite repeating decimal (0.333...) in base-10, the decimal value 0.1 results in an infinite repeating binary fraction (0.0001100110011...). Because hardware registers have a fixed number of bits (e.g., 32-bit or 64-bit), the sequence must be truncated, resulting in a value like 0.10000000149. This is why you should never use == to compare floats in C++ embedded code; always use an epsilon threshold.
How do I calculate the exact LSB voltage of my ADC?
Divide your reference voltage by 2^n, where n is the bit-depth. For a 12-bit ADC on a 3.3V rail, the binary fraction step size is 3.3 / 4096 = 0.0008056V (805.6 µV). Any analog signal change smaller than this LSB will be completely invisible to the microcontroller.
Does a higher bit-depth DAC eliminate binary fraction truncation errors?
It reduces them, but rarely eliminates them entirely unless the target voltage is an exact power-of-two ratio of the reference. Moving from a 12-bit to a 16-bit DAC reduces the LSB step size from ~800µV to ~50µV, pushing the truncation error below the noise floor of most standard op-amps and PCB traces.






