Drop Floats for Q15 Fixed-Point on FPU-less Cortex-M
On FPU-less Cortex-M0/M0+ cores, soft-float can dominate a control loop. This post explains when Q15 fixed-point helps, how to handle Q30 intermediates and saturation, and a verifiable decision rule.
04 Oct 2025, 01:58 UTC

A control loop that meets timing on a dev board can miss its deadline on production hardware. The usual culprit on low-cost Cortex-M0/M0+ parts is not the algorithm, it is soft-float. Without a hardware FPU, every float multiply compiles to a library call like __aeabi_fmul. Those calls are integer arithmetic under the hood, cost several kilobytes of code, and dominate a tight inner loop.
The useful takeaway is not "always use fixed-point". It is: reserve fixed-point for FPU-less cores or hard real-time inner loops, and verify the cost on your target before committing.
Why soft-float hurts on Cortex-M0/M0+
Fixed-point Qm.n stores a scaled integer. Value = raw / 2^n. The fraction bits n set resolution, the integer bits m set range. Q15 is 1 sign bit and 15 fraction bits in a 16-bit int16_t, representing about [-1, 1).
On a core with no FPU, float operations become software library calls. Integer arithmetic stays in registers and uses single-cycle multiply-accumulate. The code size difference matters in flash-constrained devices.
A quick sanity check on the build host: compile a small float filter for the target and inspect the map file for soft-float symbols. On a typical GNU Arm toolchain you can search the map for __aeabi_fmul and __aeabi_fadd. Their presence indicates software emulation. Risk: linker garbage collection can remove unused symbols, so check after a full link with the actual firmware.
Q-format basics without the textbook
Choosing n is range versus resolution. Q15 is common for normalized signals because it maps cleanly to int16_t and fits CMSIS-DSP conventions.
Addition and subtraction require aligned formats. Adding a Q15 to a Q13 without rescaling silently corrupts the result. A practical safeguard is naming: suffix variables _q15 or _q31 so mismatches stand out in code review.
Multiplication widens. Two Q15 values produce a Q30 intermediate. You need a wider accumulator, typically int32_t, then shift back. Overflow before the shift is the common bug.
Worked example: Q15 multiply-accumulate with saturation
Consider a simple gain stage: y = x * k where both are Q15 in [-1,1). In C with explicit types:
int16_t mul_q15_sat(int16_t a_q15, int16_t k_q15) {
int32_t prod = (int32_t)a_q15 * (int32_t)k_q15; // Q30
// rounding can be added before the shift
int32_t shifted = prod >> 15; // back to Q15
// saturate to int16_t range
if (shifted > 32767) return 32767;
if (shifted < -32768) return -32768;
return (int16_t)shifted;
}On ARM you can replace the manual clamp with the __SSAT intrinsic to saturate in hardware. Plain C wraps on overflow, which creates large discontinuities in control loops.
For an accumulator over N taps, accumulate in int64_t or int32_t with headroom, then shift once at the end. Mixing scales across taps is a classic silent error.
Trade-offs and where fixed-point bites back
Fixed-point bugs fail silently. Overflow wraps unless you saturate, and misaligned scales produce plausible but wrong numbers that only appear at extreme input values.
64-bit shifts and divisions on small MCUs can themselves be slow. If your algorithm needs division, fixed-point may not win.
Double-precision literals like 3.14 without an 'f' suffix can pull in double soft-float code even on single-precision FPUs. Use 3.14f and enable compiler warnings for implicit conversions.
The C standard _Fixed-point types exist in ISO/IEC TR 18037 but are rarely implemented in common embedded toolchains. Hand-rolled integer fixed-point remains the portable approach.
A practical decision rule you can verify
If the MCU has a single-precision FPU, such as Cortex-M4F/M7, prefer float for clarity unless profiling shows a bottleneck. Reserve fixed-point for FPU-less parts or deterministic inner loops.
Verification steps you can run without guessing:
- Build for the target and inspect the map file for soft-float symbols to confirm hardware vs software float.
- Benchmark a Q15 MAC loop against the float equivalent on the actual target using a cycle counter such as DWT CYCCNT where available, not on the host.
- Unit-test the fixed-point code on the host with randomized inputs against a double-precision reference, checking maximum absolute error and boundary overflow.
Fixed-point is a tool for predictable integer math, not a moral choice. Measure first, then decide.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.