Fractional binary numbers represent values less than one by assigning negative powers of two to the bits right of the binary point, just as decimal fractions use negative powers of ten.

When you move from blinking LEDs to closed-loop control systems or digital signal processing (DSP) on microcontrollers, you inevitably hit the limits of integer math. Understanding fractional binary changes how you scale sensor readings, generate waveforms, and implement PID controllers without the massive CPU overhead of floating-point operations. Most hobbyists confuse fractional binary (the basis of fixed-point math) with IEEE 754 floating-point representation, leading to severe precision errors when they try to force decimal logic onto binary hardware.

The Anatomy of a Fractional Binary Number

In standard decimal, the number 0.8125 means 8 tenths, 1 hundredth, 2 thousandths, and 5 ten-thousandths. In binary, we don't have tenths or hundredths; we have halves, quarters, eighths, and sixteenths. The 'binary point' (the binary equivalent of a decimal point) separates the integer bits on the left from the fractional bits on the right.

Let's break down the fractional binary number 0.1101 into its decimal equivalent:

Bit Position1st (1/2)2nd (1/4)3rd (1/8)4th (1/16)
Binary Digit1101
Weight (2^-n)2^-1 (0.5)2^-2 (0.25)2^-3 (0.125)2^-4 (0.0625)
Value0.50.2500.0625

Adding those values together: 0.5 + 0.25 + 0 + 0.0625 = 0.8125. Therefore, 0.1101_2 = 0.8125_10. This exact mapping is the foundation of fixed-point math, where we implicitly agree that a certain number of bits in an integer variable represent the fractional part.

Where You Meet This in Practice

You rarely type a binary point into your Arduino or ESP32 IDE. Instead, fractional binary numbers manifest in three specific embedded scenarios:

  • ADC Scaling: When reading a 12-bit ADC (0–4095) that maps to 0.0V–3.3V, each bit represents 3.3 / 4095 = 0.0008058V. Multiplying the raw integer by this fractional binary weight gives you the real voltage.
  • DAC Waveform Generation: Generating a sine wave via I2S or an internal DAC requires calculating fractional amplitudes. Storing these as scaled integers (e.g., multiplying by 32767 for 16-bit audio) saves thousands of CPU cycles per second.
  • PID Control Loops: Proportional, Integral, and Derivative gains (Kp, Ki, Kd) are almost always fractional values between 0.0 and 1.0. Using fractional binary math (fixed-point) allows the microcontroller to execute the control loop inside a high-frequency Interrupt Service Routine (ISR) without the context-switching penalty of a software floating-point unit (FPU).
Why not just use float?
On an ESP32-WROOM-32, hardware floating-point math is relatively fast, but on Cortex-M0 chips (like the RP2040 or basic STM32s), a single floating-point multiplication can take 30+ clock cycles. A fixed-point fractional binary shift takes exactly 1 cycle. In a 20kHz motor control ISR, those wasted cycles cause timing jitter that degrades motor performance.

Real-World Scenario: The PID Integral Windup Disaster

To understand why treating fractional binary numbers like decimal numbers causes hardware failures, let's look at a real bench scenario involving a DC motor speed controller.

The Setup: An ESP32 is running a PID loop at 10,000 Hz (10kHz) to control a 12V DC motor via an L298N driver. The Integral gain (Ki) is tuned in a simulation to be exactly 0.1. To avoid floating-point math in the ISR, the developer decides to use an 8-bit fractional binary representation for Ki.

The Numbers: In decimal, 0.1 is a clean, finite number. But in fractional binary, 0.1_10 is a repeating fraction: 0.00011001100110011..._2. Because the developer only allocated 8 bits for the fraction, the microcontroller truncates the value to 0.00011001_2.
Let's convert that back to decimal: 1/16 + 1/32 + 1/256 = 0.09765625.

The Outcome: The motor violently overshoots the target speed, the PID loop maxes out the PWM duty cycle to 100%, the motor stalls under the sudden load, and the L298N driver chip overheats and shuts down (or melts if it lacks thermal protection).

What Went Wrong: The error between the intended 0.1 and the truncated 0.09765625 is 0.00234375. That looks tiny. But the integral term in a PID controller accumulates error over time. Running at 10,000 loops per second, the accumulator loses 23.43 counts every single second. The controller thinks the motor is constantly lagging behind the setpoint, so it keeps winding up the PWM duty cycle to compensate for a mathematical ghost. This is called systematic bias, and it is the direct result of truncating a repeating fractional binary number.

How to Fix It: Q-Format and Proper Rounding

The industry standard for handling fractional binary numbers in embedded C/C++ is Q-format (specifically Q15 or Q31). Q15 uses a 16-bit signed integer where 1 bit is the sign and 15 bits are the fractional part. This gives you a range of -1.0 to 0.999969 with a resolution of 0.0000305.

Here is the step-by-step process to implement this safely without falling into the truncation trap:

  1. Scale to Q15: Multiply your decimal fraction by 32768 (which is 2^15). For 0.1, this is 0.1 * 32768 = 3276.8.
  2. Round, Don't Truncate: Add 0.5 before casting to an integer to force rounding. (int)(3276.8 + 0.5) = 3277. This minimizes the systematic bias.
  3. Store as Integer: Store 3277 in an int16_t variable.
  4. Multiply and Shift: When multiplying your sensor reading (also in Q-format) by this gain, the result will be in Q30 format (32-bit). Shift the result right by 15 bits (>> 15) to return it to Q15.

By rounding to 3277, your actual fractional value becomes 3277 / 32768 = 0.100006. The error is now 0.000006 per loop, accumulating at just 0.06 counts per second—a negligible amount that the proportional term will easily absorb without winding up.

Common Confusions and Pitfalls

Fractional Binary vs. IEEE 754 Floating Point

People frequently confuse fixed-point fractional binary with floating-point. Floating-point (like a 32-bit float) uses a mantissa and an exponent, allowing it to represent incredibly large and incredibly small numbers dynamically. Fractional binary (fixed-point) has a fixed binary point. It cannot dynamically scale; what you gain in execution speed, you lose in dynamic range. You must manually manage overflows when adding two large Q15 numbers.

The '0.1' Repeating Fraction Trap

Just as 1/3 becomes 0.333... in decimal, numbers like 0.1, 0.2, and 0.3 become repeating fractions in binary. Only fractions that are sums of inverse powers of two (like 0.5, 0.25, 0.75, 0.125) can be represented perfectly in fractional binary. Always assume your decimal gains will require rounding when converted to binary.

FAQ: Fractional Binary in Embedded Systems

Q: Can I just use the Arduino map() function for fractional scaling?
A: No. The standard Arduino map() function uses integer math and truncates the result at every step. If you are mapping a 10-bit ADC to a 16-bit PWM and need fractional precision, map() will introduce severe dead-bands and stepping artifacts. Write a custom scaling function using 32-bit integers and bit-shifting.

Q: What happens if my fractional binary addition overflows?
A: In Q15 math, adding 0.8 and 0.5 yields 1.3, which exceeds the maximum positive value of 0.999. In a signed 16-bit integer, this wraps around to a negative number, causing your motor to instantly reverse direction or your audio waveform to violently clip. Always implement saturation logic (clamping the value to the max/min limits) after fractional additions.

Q: Do modern ESP32s even need fixed-point fractional math?
A: The ESP32 has an FPU, so float is fast enough for 1kHz loops. However, if you are writing an ISR for a 20kHz+ biquad audio filter or a high-speed field-oriented control (FOC) motor drive, fixed-point fractional math is still mandatory to meet the strict microsecond timing deadlines. For more on ESP32 peripheral timing, refer to the Espressif LEDC peripheral documentation.

Mastering fractional binary numbers bridges the gap between theoretical control algorithms and reliable, physical hardware. By respecting the binary point and managing truncation errors, you ensure your embedded systems behave exactly as your simulations predict.