Choosing between Quick Float and Fixed-Point Math

Same API, same function coverage — float or fixed-point integer, depending on the target.

qf_math operates on IEEE-754 float. fr_math operates on fixed-point integers with a caller-selectable radix — the radix parameter controls how many of the 32 bits are fractional (e.g. radix 16 = s15.16, radix 12 = s19.12, radix 8 = s23.8). Both libraries expose the same set of functions — trig, inverse trig, log, exp, sqrt, hypot, waveform generators, ADSR envelopes — so switching from one to the other is largely a matter of changing the include and the numeric type. The API names follow a parallel convention: qf_sin() / fr_sin(), qf_log2() / FR_log2(), and so on.

Choosing between them

Factorqf_math (float)fr_math (fixed-point)
Best targetsCortex-M4F/M7, ESP32, RP2040/RP2350 with FPU, any core with hardware single-precisionCortex-M0/M0+, 8/16-bit MCUs, RISC-V without F extension, any core without FPU
Soft-float costHigh — compiler inserts software multiply/add when no FPU is presentZero — pure integer ALU throughout
Dynamic range~1e-38 to ~3.4e+38Determined by radix choice (e.g. R=16: ±32K; R=12: ±512K; R=8: ±8M)
Precision~7 significant digits (float32)Determined by radix choice (e.g. R=16: ~1.5e-5 resolution; R=12: ~2.4e-4)
Typical accuracy< 0.002% FS (trig), < 0.001% rel (log/exp)< 0.008% FS (trig), < 0.4% rel (log/exp)
Table ROM~4.4 KB (512-entry float tables)~908 bytes (quadrant/octant unsigned short tables)
16-bit supportRequires 32-bit floatSupports 16-bit platforms (s16 mode)
LanguageC99C89 (pre-C99 fallback typedefs for compilers without <stdint.h>)

In short: if the target has a hardware FPU or the pipeline already works in float, use qf_math. If the target has no FPU or soft-float overhead is unacceptable, use fr_math and pick the radix that fits the application’s range/resolution needs. Both produce the same outputs for the same mathematical inputs — the accuracy and speed trade-offs differ with the numeric representation.

What they share

Both libraries use BAM (Binary Angular Measure) phase, lookup-based approximation with sub-step linear interpolation, the same 8-segment piecewise-linear fast hypot, and identical waveform/ADSR API shapes.

Featureqf_mathfr_math
BAM phaseuint16_t, 65536 = full turnuint16_t, 65536 = full turn
Log2 / Pow2 tables65-entry, float65-entry, u32 (s.16 fixed-point)
hypot_fast88-segment piecewise-linear (float multiply)8-segment piecewise-linear (shift-only, no multiply)
Waveformssqr/pwm/tri/saw/noise via BAM phasesqr/pwm/tri/saw/noise via BAM phase
ADSRqf_adsr_t (float levels)fr_adsr_t (fixed-point levels)

Where the internals differ

The table layouts and numerical methods are tuned to each domain:

Algorithmqf_mathfr_math
Sine table512-entry full-cycle, float (2048 bytes)129-entry first-quadrant, unsigned short u0.15 (258 bytes); other quadrants via symmetry
Tangent table512-entry full-cycle, float (2048 bytes)65-entry first-octant, unsigned short u0.15 (130 bytes); second octant via tan(x) = 1/tan(π/2 − x)
Interpolation7-bit sub-step linear interpolation7-bit sub-step linear interpolation + small-angle approximations near cardinals (< 0.7°)
Inverse trigPiecewise Hermite polynomial spans; atan2 via reciprocal reductionBinary search on ascending sine quadrant table; near-1.0 fast path acos(x) ≈ √(2(1−x)); atan2 via hypot_fast8 + conditional asin/acos
SqrtNewton-Raphson on 1/√x, seeded by IEEE 754 magic constantNon-restoring shift-and-subtract digit-by-digit (no multiply, no divide)
ReciprocalIEEE 754 magic constant + Newton-RaphsonShift-based initial estimate + Newton-Raphson
Exact hypotqf_sqrt(x*x + y*y)fr_isqrt64(x*x + y*y) (64-bit integer product + digit-by-digit sqrt)
Small-angle pathsNone (table covers full range uniformly)sin(θ) ≈ θ, tan(δ) ≈ δ near zero-crossings; cot(δ) ≈ 1/δ near poles; atan2 uses asin(x) ≈ x below ~4.8°

qf_math uses larger float tables (4 bytes/entry) for uniform full-cycle indexing. fr_math packs quadrant/octant tables into unsigned short (2 bytes/entry) and recovers the full circle via symmetry, keeping total table ROM under 1 KB.

Switching between them

Because the API names are parallel, porting between the two is straightforward. The main change is the numeric type and the include:

/* qf_math — float pipeline */
#include "qf_math.h"
qf y   = qf_sin(angle);
qf len = qf_hypot(dx, dy);
qf dB  = 20.0f * qf_log10(v / ref);

/* fr_math — fixed-point pipeline (radix 16 = s15.16) */
#define R 16
#include "FR_math.h"
s32 y   = fr_sin(angle, R);
s32 len = FR_hypot(dx, dy, R);
s32 dB  = FR_MUL(I2FR(20, R), FR_log10(FR_DIV(v, ref, R), R, R), R);

fr_math calls carry an explicit radix parameter — change R to trade range for resolution. qf_math functions take and return plain float.

Bridging macros

qf_math.h includes macros for converting between float and fixed-radix integers at system boundaries:

int32_t fr_val = QF_TO_FR(1.234f, 16);     // float → Q16.16 (truncating)
int32_t fr_val = QF_TO_FR_RND(1.234f, 16);  // float → Q16.16 (rounding)
qf float_val   = FR_TO_QF(80871, 16);       // Q16.16 → float

The C++ wrapper provides the same conversions:

int32_t fr = qf_math::to_fr(1.234f, 16);
qf back    = qf_math::from_fr(fr, 16);

Speed comparison

The compare/ harness benchmarks both libraries (and others) against libm on the same hardware. Ratios below are libm time ÷ library time — values above 1.0 mean the library is faster than libm.

Functionqf_mathfr_mathNotes
sin (rad)4.30×1.32×
cos (rad)4.39×0.90×
tan (rad)3.56×1.31×
asin1.10×0.77×
acos1.11×0.77×
atan21.41×0.70×
sqrt1.07×0.08×libm sqrtf often maps to a hardware instruction
exp2.63×1.88×
ln2.20×0.92×
hypot2.51×0.21×fr_math hypot goes through float bridge overhead

ESP32-S3 (with FPU). fr_math numbers include float↔fixed bridge overhead — native s32 / fix16_t calls in a pure-integer pipeline are faster. See compare/BENCHMARK_CROSSPLATFORM.md for full matrices including multiple architectures, libfixmath, FastTrig, and ESP-DSP.