In decimal, one-tenth is a clean fraction, but 0.1 in binary is a non-terminating, repeating sequence (0.000110011...) that cannot be stored exactly in standard floating-point registers. When you type 0.1 into your Arduino or ESP32 IDE, the compiler silently rounds it to the nearest representable hardware value, introducing a microscopic error that compounds over time and breaks exact equality checks in your embedded C++ code.
The Exact Bit Pattern of 0.1 in Binary
To understand why this happens, we have to look at the IEEE 754 standard for 32-bit single-precision floating-point numbers, which is the default float type on almost all 8-bit and 32-bit microcontrollers. A 32-bit float allocates 1 bit for the sign, 8 bits for the exponent, and 23 bits for the mantissa (the significant digits).
Just as 1/3 in decimal is a repeating fraction (0.3333...), 1/10 in binary is a repeating fraction. The exact binary representation of 0.1 is 0.00011001100110011001100110011... repeating infinitely. Because the mantissa is limited to 23 bits, the hardware must chop off the infinite tail and round the final bit.
If you inspect the exact 32-bit hex value for 0.1 using a tool like float.exposed, the stored hex is
0x3DCCCCCD.When the microcontroller's ALU converts
0x3DCCCCCD back to decimal for a calculation, it does not yield 0.1. It yields:0.100000001490116119384765625
That extra
0.00000000149... is the rounding error. It looks negligible until you put it in a loop.
Consider this standard accumulator loop running on an ATmega328P (Arduino Uno) or an ESP32:
float total = 0.0;
for(int i = 0; i < 10; i++) {
total += 0.1;
}
if (total == 1.0) {
Serial.println("Target reached."); // THIS WILL NEVER PRINT
} else {
Serial.println(total, 9); // Prints: 1.000000119
}
Because you added 0.10000000149 ten times, your final value is 1.0000000149. The == operator evaluates to false, and your state machine stalls, your relay never trips, or your PID controller winds up endlessly.
What This Changes in a Real Embedded System
This mathematical quirk fundamentally changes how you must architect timing, thresholding, and control loops in firmware. In a real circuit installation or bench prototype, relying on exact float comparisons leads to three specific failure modes:
- Missed Thresholds: If your code waits for a temperature sensor to hit exactly
25.1degrees before triggering a cooling fan, and the ADC math resolves to25.100002, the fan will never turn on. - Timer Drift: If you use a
floatto accumulate milliseconds in a non-blocking delay (e.g.,elapsed += 0.1inside a fast loop), the rounding error will eventually cause your 1-second interval to drift by several milliseconds, desynchronizing PWM outputs or data logging timestamps. - FPU Performance Penalties: On the ESP32 (Xtensa LX6/LX7 architecture), the hardware Floating Point Unit (FPU) only supports 32-bit single-precision floats natively. If you try to "fix" the 0.1 problem by switching to a 64-bit
double, the ESP32 must emulate the 64-bit math in software. This turns a 1-cycle hardware operation into a multi-cycle software routine, severely bottlenecking high-speed control loops.
Where You Meet This in Practice
You will run into the 0.1 binary problem most frequently in three specific embedded scenarios:
- Analog Sensor Scaling: When converting a 10-bit ADC reading (0-1023) to a voltage (0.0 - 5.0V), you multiply by
5.0 / 1023.0or0.00488. If you then checkif (voltage == 2.5), it will fail. You must always use greater-than/less-than bounds or integer mapping. - PID Control Integrals: The 'I' (Integral) term in a PID loop continuously adds small error fractions over time. If your
dt(delta time) is calculated as a float like0.1seconds, the integral windup will accumulate the1.49e-9error thousands of times per minute, causing steady-state oscillation in motor speed controllers. - Capacitive Touch Calibration: Libraries like ESP32's
touchRead()return raw integer values. If you attempt to normalize these to a 0.0-1.0 float range for a UI slider, comparing the slider position to exact decimal detents (like 0.1, 0.2, 0.3) will result in jittery, unresponsive UI behavior.
== or != operators with float or double variables in C++. Always use an epsilon tolerance check, or better yet, avoid floats entirely for state logic.
Decision Tree: Choosing the Right Data Type
When you need to represent fractional values like 0.1 in your firmware, do not default to float. Use this decision path to select the exact data type and comparison method for your specific hardware constraint.
| If your scenario is... | Then your action is... | Concrete Pick / Implementation |
|---|---|---|
| You need to count time, pulses, or exact state thresholds. | Scale your units to the smallest integer required. Drop the decimal entirely. | Use uint32_t and measure in milliseconds, microvolts, or 0.1-degree increments (e.g., store 25.1°C as 251). |
| You are doing PID math or DSP filtering where fractions are unavoidable. | Use 32-bit floats, but implement an epsilon comparison for all logic branches. | Use float and check std::fabs(a - b) < 1e-5 (using std::fabs). |
| You need high precision (e.g., GPS coordinates or financial math) on an ESP32. | Use 64-bit fixed-point math or accept the software FPU penalty. | Use int64_t scaled by 1,000,000 (fixed-point), or use double if CPU load is under 40%. |
| You are on an 8-bit AVR (Arduino Uno/Nano) and need fractional math. | Avoid floats entirely; the ATmega328P has no hardware FPU, making all float math slow software emulation. | Use int16_t or int32_t with manual decimal shifting (e.g., Q15 fixed-point format). |
Default Recommendation: If you are unsure, scale to integers. Storing 0.1 as 1 (with an implicit divisor of 10) eliminates the binary repeating fraction problem entirely, uses less memory, and executes in a single CPU cycle on any microcontroller.
Common Confusions and Debugging Traps
Confusion 1: "But my Serial.print shows 0.10!"
When you call Serial.print(myFloat, 2), the Arduino print function automatically rounds the output to two decimal places for human readability. It hides the 0.00000000149 tail. To see the actual hardware value during debugging, you must explicitly request 9 decimal places: Serial.print(myFloat, 9).
Confusion 2: "I'll just use double instead of float."
On an Arduino Uno (8-bit AVR), double is simply an alias for float. Both are 32-bit, and both suffer from the exact same 0.1 rounding error. On a 32-bit ESP32 or Raspberry Pi Pico, double is a true 64-bit IEEE 754 value. It pushes the rounding error much further down the line (to the 15th decimal place), but it still does not eliminate it. 0.1 is still a repeating fraction in 64-bit binary.
Confusion 3: Confusing Binary Integers with Binary Floats.
The integer 1 in binary is exactly 00000001. There is no rounding error for integers. The 0.1 problem exclusively applies to the IEEE 754 floating-point format. If your application only requires increments of 0.1, multiply everything by 10 and use binary integers.
Frequently Asked Questions
Does Python or JavaScript on a Raspberry Pi have this same 0.1 problem?
Yes. This is not a microcontroller-specific issue; it is a fundamental property of the IEEE 754 standard used by almost all modern programming languages. In Python, 0.1 + 0.2 == 0.3 evaluates to False for the exact same mathematical reason.
Can I use the modulo operator (%) with 0.1?
No. The standard modulo operator % in C++ only works with integer types. If you need a floating-point modulo (e.g., wrapping a sensor angle at 0.1 increments), you must use the fmod() function from <math.h>, but be prepared for the same floating-point drift issues.
What is the safest epsilon value to use for 32-bit float comparisons?
For values near 1.0, an epsilon of 1e-5 (0.00001) is generally safe for 32-bit floats. If your accumulated values are much larger (e.g., 10,000.0), you must scale your epsilon up, as the absolute gap between representable float values widens as the exponent increases.






