Decimals in binary are fractional values represented using either fixed-point integer scaling or IEEE 754 floating-point formats, allowing digital logic to process non-integer data. When you move from simple digital outputs to reading a BME280 pressure sensor or tuning a PID controller, handling decimals fundamentally changes your circuit's performance: it dictates whether your microcontroller spends two clock cycles on a basic bit-shift or fifty cycles on a software-emulated math library. The most common confusion here is mixing up true binary fractions with Binary Coded Decimal (BCD)—a legacy format used in DS3231 RTC modules where each 4-bit nibble represents a base-10 digit rather than a mathematical fraction.
The Core Concept: Radix Points and Binary Fractions
In the decimal system, the radix point (what we call the decimal point) separates powers of 10. In binary, the radix point separates powers of 2. Moving left from the radix point, the columns represent 2^0 (1), 2^1 (2), 2^2 (4), and so on. Moving right, the columns represent negative powers: 2^-1 (0.5), 2^-2 (0.25), 2^-3 (0.125), and 2^-4 (0.0625).
Worked Numeric Example: Translating 5.625
Let’s look at exactly how a decimal number is built in binary, and then how a 32-bit microcontroller stores it in memory.
Step 1: Pure Binary Fraction
We want to convert 5.625 to binary.
- Integer part (5): 4 + 1 = 2^2 + 2^0 =
101 - Fractional part (0.625): 0.5 + 0.125 = 2^-1 + 2^-3 =
.101 - Combined:
101.101
Step 2: IEEE 754 Single-Precision (32-bit Float)
Microcontrollers like the ESP32 or ARM Cortex-M0 don't store 101.101 directly. They normalize it into scientific binary notation: 1.01101 x 2^2. The Arduino float reference and standard C++ compilers map this to 32 bits divided into three fields:
| Field | Bits | Calculation | Binary Value |
|---|---|---|---|
| Sign | 1 | 0 for positive | 0 |
| Exponent | 8 | Bias 127 + 2 = 129 | 10000001 |
| Mantissa | 23 | Drop the leading 1, pad with 0s | 01101000000000000000000 |
Concatenating these gives 01000000101101000000000000000000. Grouped into hex, the decimal 5.625 is stored in your MCU's SRAM as 0x40B40000. If you are debugging an I2C sensor and reading raw memory registers, seeing 0x40B4 at the start of a payload means your sensor is reporting exactly 5.625.
Where You Meet This in Practice: I2C Sensors and ADCs
You rarely type binary fractions manually. You encounter decimals in binary when translating raw hardware registers into human-readable engineering units.
Analog-to-Digital Converters (ADCs)
An ADC never returns a decimal; it returns an integer count of its reference voltage. If your ESP32's 12-bit ADC reads a 1.65V signal on a 3.3V reference, it returns the integer 2048. To get the decimal voltage, you must perform the conversion:
float voltage = (2048 * 3.3) / 4095.0; // Yields 1.65018V
Notice the 4095.0. Forcing the denominator to a float prevents the compiler from doing integer division, which would truncate the decimal and return 1.0 instead of 1.65.
I2C Sensor Registers (e.g., MPU6050 Accelerometer)
When reading an accelerometer over I2C, the chip sends two 8-bit registers that combine into a 16-bit signed integer. If the sensor is set to ±2g sensitivity, the datasheet specifies a scale factor of 16384 LSB/g. If the raw binary registers return the integer 8192, the decimal g-force is calculated as 8192 / 16384.0 = 0.5g. The decimal exists only as a result of your scaling math.
Decision Path: Float vs. Fixed-Point Math
Choosing how to handle decimals in embedded C/C++ is one of the most critical architecture decisions you will make. Using the wrong format leads to either exhausted SRAM or sluggish loop times.
| If your project requires... | And your hardware is... | Then choose... | Why? |
|---|---|---|---|
| Simple sensor logging to SD card | ESP32 / ARM Cortex-M4 | Standard Float (IEEE 754) | These chips have hardware FPUs. Float math executes in 1-3 clock cycles. |
| High-speed PID control loops | 8-bit AVR (Arduino Uno/Nano) | Fixed-Point (Q16.16) | AVRs lack an FPU. A single float multiplication takes ~50+ cycles, causing PID jitter. Integer bit-shifting takes 2 cycles. |
| Displaying currency or exact tenths | Any MCU | Integer Milli-Scaling | Floats cannot represent 0.1 exactly. Store $5.25 as the integer 5250 and insert the decimal point only during the display print routine. |
float. Use Q16.16 fixed-point math via int32_t. Here is the exact, copy-pasteable C++ function to multiply two Q16.16 numbers without losing the decimal precision:
int32_t q_multiply(int32_t a, int32_t b) { return (int32_t)(((int64_t)a * b) >> 16);}
FAQ: Precision, Rounding, and BCD Confusion
Why does 0.1 + 0.2 equal 0.30000001 in my serial monitor?
This is not a bug in your microcontroller; it is a fundamental limitation of the IEEE 754 standard. Just as 1/3 cannot be perfectly represented in base-10 (0.333...), 1/10 cannot be perfectly represented in base-2. The binary fraction repeats infinitely, and the 23-bit mantissa truncates it, leaving a tiny rounding error. If you need exact decimal addition (like for a digital scale or financial timer), use integer scaling (e.g., measure in milligrams or milliseconds) and only format the decimal for the LCD screen.
What is the difference between binary fractions and BCD?
Binary Coded Decimal (BCD) is an encoding scheme, not a mathematical format. In BCD, the decimal number 59 is stored as 0101 1001 (5 and 9 in separate nibbles). In standard binary, 59 is 0011 1011. You will encounter BCD almost exclusively when reading the time and date registers from Real Time Clock (RTC) modules like the DS3231 or DS1307. You must convert BCD to standard binary before doing any math on those time values.
How do I handle decimals when sending data over UART or RF24?
Never send raw IEEE 754 float bytes over a noisy serial or wireless link unless you are using a strict binary protocol with checksums. Endianness (byte order) differences between an ESP32 (little-endian) and a Raspberry Pi (little-endian, but sometimes parsed as big-endian in Python scripts) will scramble your decimals into garbage values. Instead, multiply your decimal by 100 or 1000, cast it to a uint16_t or int32_t, send the integer, and divide it back on the receiving end.
My default recommendation for 2026 hobbyist builds:
Unless you are strictly limited to an 8-bit AVR or writing a high-frequency interrupt service routine (ISR), default to hardware-accelerated floats on an ESP32-S3 or RP2040. The RP2040 (Raspberry Pi Pico) features dual ARM Cortex-M0+ cores with hardware integer dividers and single-cycle multipliers, making standard float math fast enough for 95% of sensor and PID applications without resorting to complex fixed-point bit-shifting.






