Fractions in binary represent decimal values less than one using negative powers of two (like 2^-1 for 0.5 and 2^-2 for 0.25) placed to the right of a binary radix point. In a real microcontroller circuit, how you handle these fractional representations dictates whether your motor controller stutters, your ADC readings drift, or your DAC outputs the exact reference voltage you need. Makers commonly confuse binary fractions with Binary-Coded Decimal (BCD)—which just maps base-10 digits to 4-bit nibbles—or falsely assume that because a number like 0.1 is exact in base-10, it has a clean, finite representation in base-2.
The Math: Converting Decimal Fractions to Binary
To convert a decimal fraction to binary, we use the 'multiply by 2' method. Instead of dividing by 2 as we do for whole numbers, we repeatedly multiply the fractional part by 2 and record the integer portion (which will always be 0 or 1).
Let's run a worked numeric example converting the decimal value 0.6875 into binary.
- Multiply 0.6875 by 2. The result is 1.375. Record the integer (1). Keep the fraction (0.375).
- Multiply 0.375 by 2. The result is 0.75. Record the integer (0). Keep the fraction (0.75).
- Multiply 0.75 by 2. The result is 1.5. Record the integer (1). Keep the fraction (0.5).
- Multiply 0.5 by 2. The result is 1.0. Record the integer (1). The fraction is now 0.0, so we stop.
Reading the recorded integers from top to bottom, we get 0.1011.
To verify, map the bits to their negative power positions: (1 × 2^-1) + (0 × 2^-2) + (1 × 2^-3) + (1 × 2^-4). This equals 0.5 + 0 + 0.125 + 0.0625, which perfectly sums back to 0.6875.
Fixed-Point vs. Floating-Point: Where You Meet This in Practice
Where you meet this in practice is rarely by typing raw binary strings into an IDE. You encounter binary fractions when configuring hardware registers, scaling ADC values, or choosing between fixed-point and floating-point math libraries.
Fixed-point math treats the binary radix point as fixed in place. Think of it like a standard ruler where you only measure in 1/16th inch increments; you cannot measure exactly 1/32nd of an inch, but the math is just integer bit-shifting, which is blazing fast and deterministic on an 8-bit AVR chip. In embedded C, this is often implemented using Q-format (like Q15), where 15 bits represent the fraction and 1 bit represents the sign.
Floating-point arithmetic (IEEE 754) uses a mantissa and an exponent, allowing the radix point to 'float' to accommodate vastly different scales.
What this changes in a real circuit is your silicon selection and timing budget. If you flash an ESP32-WROOM-32, its Xtensa LX6 core includes a hardware Floating Point Unit (FPU) that crunches IEEE 754 fractions in a single clock cycle. If you attempt the same float operations on an ATtiny85, the compiler must inject software-emulation routines. This bloats your flash memory by roughly 2KB and introduces massive, unpredictable timing jitter into your interrupt service routines (ISRs), which can ruin precision sensor sampling. According to ARM's developer documentation on FPU support, hardware floating-point units are essential for real-time DSP tasks, whereas fixed-point remains king for low-power, low-cost motor control loops.
Bench Scenario: When Binary Fractions Cause PWM Jitter
Setup: You are driving a precision DC gearmotor via PWM on an Arduino Nano (ATmega328P) using the standard 8-bit analogWrite() function, which maps duty cycle to a 0-255 integer range. The mechanical requirement demands exactly a 30% duty cycle (0.30) to maintain a specific conveyor belt speed.
Numbers: You calculate 30% of 255, which is 76.5. However, 0.30 in binary is a repeating fraction: 0.01001100110011... (base 2). Because 8-bit hardware registers only hold whole integers, the C++ compiler casts 76.5 down to 76 (or rounds to 77).
Outcome: Writing 76 to the OCR register yields a 29.8% duty cycle. Writing 77 yields 30.19%. The motor runs slightly off-spec.
What went wrong: The repeating binary fraction could not be mapped perfectly to an 8-bit integer register without rounding. In a simple conveyor, a 0.2% error is invisible. But if this 0.30 value was the proportional gain (Kp) in a PID control loop, that rounding error accumulates every single cycle. Over thousands of iterations, this leads to integral windup and severe system oscillation. As noted in All About Circuits' guide to embedded arithmetic, ignoring quantization errors in fractional mapping is a primary cause of instability in digital control systems.
The Fix: Switch to a 16-bit hardware timer (like Timer1 on the ATmega328P), giving you a 0-65535 range. 30% of 65535 is 19660.5. It still rounds, but the resulting duty cycle error drops to 0.0007%, well below the electrical noise floor of the motor driver.
Quick Reference: Common Binary Fraction Values
When debugging DAC outputs or calculating PWM thresholds, keep this reference table handy. It maps the first eight negative powers of two to their decimal equivalents.
| Bit Position | Negative Power | Decimal Value | Cumulative Sum (Binary) |
|---|---|---|---|
| 1st bit after radix | 2^-1 | 0.5 | 0.1 |
| 2nd bit | 2^-2 | 0.25 | 0.11 |
| 3rd bit | 2^-3 | 0.125 | 0.111 |
| 4th bit | 2^-4 | 0.0625 | 0.1111 |
| 5th bit | 2^-5 | 0.03125 | 0.11111 |
| 6th bit | 2^-6 | 0.015625 | 0.111111 |
| 7th bit | 2^-7 | 0.0078125 | 0.1111111 |
| 8th bit | 2^-8 | 0.00390625 | 0.11111111 |
FAQ: Fractions in Binary Explained
Why can't binary represent 0.1 exactly?
In base-10, a fraction terminates cleanly if its denominator's prime factors are only 2 and 5 (the prime factors of 10). In base-2, a fraction only terminates cleanly if its denominator's only prime factor is 2. Because 0.1 is 1/10, and 10 has a prime factor of 5, it results in an infinitely repeating binary fraction (0.000110011...). This is why 0.1 + 0.2 == 0.3 evaluates to false in standard IEEE 754 floating-point C++ code.
How do I handle fractions on an ATtiny85 without an FPU?
Avoid the float data type entirely. Scale your integers up before doing math, then scale them back down. For example, instead of calculating 5 * 0.75, calculate (5 * 75) / 100. If you need strict binary alignment for speed, use bit-shifting: multiplying by 0.5 is just a right-shift by 1 (>> 1), and multiplying by 0.25 is a right-shift by 2 (>> 2).






