In embedded systems, the term binary 0.1 refers to the inherent inability of base-2 floating-point formats (like IEEE 754) to perfectly represent the decimal fraction 0.1, resulting in microscopic rounding errors that break exact equality checks in microcontroller code. If you have ever written if (sensor_voltage == 0.1) on an Arduino or ESP32 and watched it fail to trigger despite the serial monitor printing "0.1", you have been bitten by this exact limitation. It changes how you must write comparison logic and accumulate sensor data in C++, and beginners almost universally confuse it with a faulty sensor, a bad wire connection, or a compiler bug. It is none of those; it is a fundamental mathematical reality of how silicon stores fractions.
The Math: Why Decimal 0.1 Breaks in Base-2
To understand why this happens on your workbench, we have to look at how microcontrollers convert base-10 (decimal) numbers into base-2 (binary). In decimal, the fraction 1/3 becomes a repeating decimal: 0.333333... You eventually have to round it off. In binary, the exact same repeating problem happens with the decimal number 0.1.
Let us run the actual numeric conversion of decimal 0.1 to binary by repeatedly multiplying the fractional part by 2:
- 0.1 × 2 = 0.2 (Integer part: 0)
- 0.2 × 2 = 0.4 (Integer part: 0)
- 0.4 × 2 = 0.8 (Integer part: 0)
- 0.8 × 2 = 1.6 (Integer part: 1, carry the 0.6)
- 0.6 × 2 = 1.2 (Integer part: 1, carry the 0.2)
- 0.2 × 2 = 0.4 (Integer part: 0 — the pattern now repeats infinitely)
The true binary representation of decimal 0.1 is 0.00011001100110011... repeating forever. Because a standard 32-bit float in C++ only allocates 23 bits for the mantissa (the significant digits) under the IEEE 754 standard, the microcontroller is forced to chop off the infinite tail and round the number.
When you type float val = 0.1; on an ESP32 or Arduino Uno, the actual value stored in the silicon's memory registers is not 0.1. It is exactly 0.100000001490116119384765625. When the serial monitor prints "0.1", it is lying to you; the Serial.print() function simply rounds the output to two decimal places for readability by default.
Microcontroller Precision: Float vs. Double Data Table
Not all microcontrollers handle this truncation identically. The C++ language specification defines float and double, but the actual bit-width assigned to those types depends entirely on the microcontroller's architecture and compiler toolchain. Below is a data-dense breakdown of how popular maker boards handle the storage of "0.1".
| Microcontroller Board | Architecture | float Size |
double Size |
Stored Value of 0.1 (float) |
Stored Value of 0.1 (double) |
|---|---|---|---|---|---|
| Arduino Uno (ATmega328P) | 8-bit AVR | 32-bit | 32-bit | 0.10000000149 | 0.10000000149 (No precision gain) |
| ESP32 DevKit V1 | 32-bit Xtensa | 32-bit | 64-bit | 0.10000000149 | 0.10000000000000000555 |
| Raspberry Pi Pico (RP2040) | 32-bit ARM Cortex-M0+ | 32-bit | 64-bit | 0.10000000149 | 0.10000000000000000555 |
| Arduino Due (SAM3X8E) | 32-bit ARM Cortex-M3 | 32-bit | 64-bit | 0.10000000149 | 0.10000000000000000555 |
double to the exact same 32-bit memory footprint as float. Upgrading your variable from float to double on an Uno will consume twice the RAM but yield zero extra precision. On the ESP32 and RP2040, double genuinely upgrades to 64-bit precision, pushing the rounding error much further down the decimal line, though it still does not eliminate it entirely.
Where You Meet This in Practice (And How It Fails)
You will rarely notice binary 0.1 errors when simply printing values to a screen. The failures manifest in logic gates, control loops, and data logging where exactness is assumed. Here are the three most common bench scenarios where this bug ruins projects:
1. The Sensor Threshold Trap
You are reading an analog sensor, scaling it to volts, and want to trigger a relay when it hits exactly 0.1V.
float voltage = analogRead(A0) * (5.0 / 1023.0);
if (voltage == 0.1) {
digitalWrite(RELAY_PIN, HIGH); // THIS WILL ALMOST NEVER TRIGGER
}
Because voltage is calculated via ADC math, it might evaluate to 0.10000002 or 0.09999998. The strict equality == check fails.
2. PID Loop Integral Windup
In a temperature controller or balancing robot, you accumulate an error term over time: integral += error * dt;. If your time step dt is 0.1 seconds, and your loop runs at 10kHz, you are adding a slightly inflated 0.10000000149 ten thousand times a second. Within a minute, your integral accumulator has drifted by a measurable margin, causing your PID controller to overshoot its target and oscillate.
3. Financial and Timing Counters
If you are building a custom UPS or battery monitor and accumulating costs or amp-hours using total += 0.1;, the Arduino float reference explicitly warns that floats lose accuracy after 6 to 7 significant digits. Once your total reaches 10,000.1, the 0.1 fraction is entirely swallowed by the mantissa's limits, and your counter stops incrementing.
The Fix: Epsilon Checks and Fixed-Point Math
When you encounter binary 0.1 drift, you have two professional workarounds. The first patches the floating-point logic; the second abandons floats entirely.
Workaround A: The Epsilon Comparison
Never use == or != with floats. Instead, check if the absolute difference between the two numbers is smaller than a tiny threshold (epsilon). According to C++ type specifications, this is the standard compliant method for floating-point evaluation.
#define EPSILON 1e-5
bool floatEquals(float a, float b) {
return fabs(a - b) < EPSILON;
}
void loop() {
float voltage = readSensor();
if (floatEquals(voltage, 0.1)) {
// Safe trigger logic
}
}
Workaround B: Fixed-Point Integer Math (The Industry Standard)
In professional embedded firmware, we avoid floats for accumulation and thresholds entirely. We use fixed-point math by scaling our units up to integers. Instead of measuring in Volts (which requires 0.1), measure in Millivolts (which uses whole numbers).
// BAD: Floating point accumulation
float amp_hours = 0.0;
amp_hours += 0.1; // Drifts over time
// GOOD: Fixed-point integer accumulation
long milliamp_hours = 0;
milliamp_hours += 100; // Perfect, lossless base-2 math
// When you need to display it to the user:
Serial.print(milliamp_hours / 1000.0);
long or int64_t) and scale your units to milli, micro, or nano.Frequently Asked Questions
Does using an ESP32 solve the binary 0.1 problem?
Only partially. The ESP32 supports true 64-bit double precision, which pushes the rounding error out to the 16th decimal place (0.10000000000000000555). For 99% of hobbyist sensor projects, this is effectively zero error. However, if you declare your variable as a standard float on the ESP32, it still defaults to 32-bit precision and you will face the exact same 0.10000000149 truncation as on an Arduino Uno.
Why does my serial monitor print exactly "0.10" if the memory holds "0.10000000149"?
The Serial.print() function in the Arduino core library is hardcoded to round floating-point numbers to two decimal places by default to save memory and processing time. You can expose the hidden binary 0.1 error by forcing the serial monitor to print more decimal places: Serial.print(val, 15);. This will immediately reveal the trailing garbage digits stored in the IEEE 754 registers.
Can I use the decimal data type in C++ to fix this?
No. Unlike C# or Python, standard embedded C++ (GCC/AVR/ARM toolchains) does not have a native, hardware-accelerated decimal type that uses base-10 storage. You must rely on the epsilon comparison method or integer-based fixed-point math outlined above.






