Summary of The Art of Representing Floating-Point Numbers as Integers
This article argues that using floating-point variables on embedded systems without a Floating-Point Unit (FPU) is inefficient. It recommends converting numbers to fixed-point representation using integers and scaling values to optimize performance and code size. The text explains the mathematical limits of CPU registers, defines Q-format notation for separating integer and fractional bits, and emphasizes selecting bit allocations based on architecture precision needs.
Parts used in Fixed-Point Representation Project:
- Floating-point variables
- Double variables
- Floating-Point Unit (FPU)
- C/C++ compiler
- Assembly code generator
- CPU architecture registers
- CPU buses
- Two's complement system
- Q-format notation
- Scaling value
Why the AVR will not do floating point for you
The ATmega32 is an 8-bit core with 32 general-purpose registers, 32 KB of flash and 2 KB of SRAM. Its instruction set contains 131 instructions and none of them is a floating-point operation. The only arithmetic help is the multiplier: MUL, MULS, MULSU and the fractional variants FMUL, FMULS and FMULSU, each taking two clock cycles and leaving a 16-bit result in R1:R0.
Everything else about float is software. avr-libc defines float and double as the same 32-bit single-precision type, so every operation becomes a library call. Its FAQ records that a float-capable vfprintf adds about 2.5 KB to a binary, roughly 8% of this part’s flash. There is no divide instruction either.
Fixed point keeps integer arithmetic and drops the exponent. The stored value is an integer; the programmer keeps track of where the binary point belongs.
Q-format: placing the binary point
In Qn.m, n bits sit to the left of the point and m bits to the right, with one bit spent on the sign. Microchip’s AN617 writes the same notation as Qi.j and makes the essential point: r only locates the radix point, while the stored bit pattern is independent of it. Two numbers in different Q formats are the same kind of object; only the interpretation differs.
- Q1.15 — −1.0 to 0.99997, step 2−15 ≈ 30.5 × 10−6
- Q8.8 — −128.0 to 127.996, step 0.0039
- Q4.12 — −8.0 to 7.9998, step 0.00024
- Q16.16 — −32768.0 to 32767.99998, step 1/65536
Range and resolution compete for the same 16 or 32 bits: moving the point buys resolution and costs range, and the hardware is unaffected.
Multiplication is where the bookkeeping bites
Adding requires a common format, because the points must line up. Multiplying does not: the fractional bit counts add. A Q4.12 times a Q8.8 gives a Q12.20, which is why a 16 × 16 product cannot stay in 16 bits.
On this core a signed 16 × 16 product is four 8 × 8 partial products using MULS and MULSU, accumulated into two register pairs. Keep it at full width and renormalise once, at the end. AVR223 goes further: its filters hold a 40-bit virtual accumulator across five registers so nothing is shifted until the final downscale, which must use ASR so the sign bit survives. Coefficient sets are arranged so the last step is a shift, never a division.
Rounding, saturation, and the multiply itself
A right shift truncates toward negative infinity, so converted values drift downward and a symmetric signal picks up a DC offset. Adding half an LSB before the shift turns truncation into round-half-up:
int16_t mul_q15(int16_t a, int16_t b)
{
int32_t p = (int32_t)a * (int32_t)b; /* Q2.30 */
p += 0x4000L; /* half of 2^15 */
p >>= 15; /* renormalise */
if (p > 32767L) return 32767;
if (p < -32768L) return -32768;
return (int16_t)p;
}
The clamp is not decoration. Signed overflow in C is undefined behaviour, and on this core it wraps silently: a controller that should stop at full scale instead commands the opposite direction. Saturate where the value leaves its format. Round-half-up is biased toward positive infinity: harmless once, worth remembering when results accumulate.
Worked example: a 10-bit ADC scaling chain
The ATmega32 ADC gives 10 bits over the reference, converts in 13 ADC clocks, and is specified at ±2 LSB absolute accuracy, with 50–200 kHz on the ADC clock for full resolution. Against a 5.000 V reference one count is 4.883 mV, so the converter’s own error exceeds the quantisation step: the analogue front end, not the arithmetic, sets the floor.
Counts to millivolts in integers: mV = (counts * 5000 + 512) >> 10. The largest intermediate is 1023 × 5000 ≈ 5.1 × 106, well inside a 32-bit long. If sensor excitation and AREF share a rail, that rail cancels and counts can carry the chain to the last conversion.
Apply a calibration gain of 0.98 held in Q1.15: the constant is 32113, since 0.98 × 32768 = 32112.6. A reading of 512 counts gives 16,441,856; add 16384, shift right by 15, and the corrected value is 502 counts against an exact 501.76. A gain above 1 would not fit Q1.15 at all — 1.05 needs Q2.13, where the constant is 8602 and the shift becomes 13. Keeping the result at Q12.4 instead, by multiplying by 2500 × 16 before the shift, gives 1225.6 mV for the same reading with no float in the path.
Worked example: one PID term
Put the error in Q8.8 and the gain in Q4.12. The product is Q12.20, whose largest magnitude is 32767 × 32767 ≈ 1.07 × 109, comfortable in 32 bits. Renormalise by 12 to return to Q8.8.
That is where the trap sits. Anywhere in the Q4.12 range the gain can reach 8, and the error can reach 128, so the product can reach 1024 while Q8.8 tops out at 127.996. Clamp in the 32-bit domain before the shift; afterwards is too late, because the wrap has already happened.
The integral term needs a wider accumulator than the output — Q16.16 in a long is common — and it needs a clamp, because an integrator that keeps counting while the actuator is saturated is the classic wind-up failure. Take the derivative on the measurement rather than the error, and fold the fixed sample time into the constants.
The format arithmetic, instruction behaviour and ADC timing above are documented. Multiply cost, generated code size and loop constants depend on the build and must be checked on the target.
References
- Microchip Technology, ATmega32/ATmega32L 8-bit AVR Microcontroller with 32KBytes In-System Programmable Flash, datasheet 2503Q–AVR–02/11.
- Atmel, AVR223: Digital Filters with AVR, 2527B–AVR–07/08.
- Microchip Technology, AN617: Fixed Point Routines.
- avr-libc project, avr-libc User Manual and FAQ (data types, math library, code size notes).
-
Why should you avoid float or double variables on embedded systems without an FPU?
Using them is inefficient because the compiler generates excessive assembly code, increasing program size and execution time compared to hardware-specific operations. -
What is the recommended alternative to floating-point arithmetic on systems lacking an FPU?
You should use fixed-point representation by converting numbers to integers through scaling. -
How does the scaling value relate to the CPU architecture?
The scaling value depends on the bitness of the CPU architecture, ensuring values fit within available registers matching the bus width. -
What is the formula for the range of integer values in a b-bit register using two's complement?
The range is given by −2^(b−1) ≤ value ≤ 2^(b−1) – 1. -
How is the Q-format notation defined in terms of bit allocation?
Qm.n denotes m bits for the integer part and n bits for the fractional part, with one bit reserved for the sign. -
What determines the trade-off between precision and range in fixed-point representation?
The number of bits allocated to the integer and fractional parts determines precision; more bits generally yield greater precision.
