FR_Math

A C language fixed-point math library for embedded systems.

FR_Math is a compact, integer-only fixed-point math library built for systems where floating point is too slow, too big, or unavailable. Designed for embedded targets ranging from legacy 16 MHz 68k processors to modern Cortex-M and RISC-V cores, it provides a full suite of math primitives — trigonometry, logarithms, roots, transforms, and signal generators — while remaining deterministic, portable, and small. Unlike traditional fixed-point libraries, FR_Math lets the caller choose the binary point per operation, trading precision and range explicitly instead of locking into a single format.

Measured accuracy

Errors below are measured at Q16.16 (s15.16). All functions accept any radix — Q16.16 is just the reference point for the table. Run make test-tdd to generate the TDD report (build/test_tdd_report.md) with sweeps at radixes 8, 12, 16, and 24.

FunctionMax err (%)*Avg err (%)Note
sin/cos (BAM)0.15260.0030very fast binary angle trig
sin/cos (deg)0.15260.0029degree input trig fns
sin/cos (rad)0.18280.0033radian (traditional) trig
tan (BAM)0.58230.0008binary angle tangent; ±maxint at poles
tan (deg)0.53110.0008degree input tangent; saturated at poles
tan (rad)0.03860.0001radian (traditional) tangent
asin / acos0.77710.0280reverse trig, radian output
atan20.25640.0237reverse tangent, always safe
atan0.24250.0155reverse tangent, accepts up to maxint
sqrt0.00000.0000Round-to-nearest
log20.01160.0016shift/add only for speed
pow20.00180.0004shift/add only for speed
ln, log100.00040.0000shift/add only for speed
exp0.00030.0000shift/add only for speed
exp_fast0.00090.0001Shift-only scaling
pow100.00050.0000shift/add only for speed
pow10_fast0.00220.0002Shift-only scaling
hypot (exact)0.00000.0000Uses 64-bit intermediate
hypot_fast8 (8-seg)0.09150.0320Shift-only, no multiply

*Relative error; reference clamped to 1% of full-scale output.

What’s in the box

AreaFunctions
ArithmeticFR_ADD, FR_SUB, FR_DIV, FR_DIV32, FR_MOD, FR_FixMuls, FR_FixMulSat, FR_CHRDX
UtilityFR_MIN, FR_MAX, FR_CLAMP, FR_ABS, FR_SGN
Trig (integer deg)fr_sin_deg, fr_cos_deg, fr_tan_deg, FR_SinI, FR_CosI, FR_TanI
Trig (radian/BAM)fr_sin, fr_cos, fr_tan, fr_sin_bam, fr_cos_bam, fr_tan_bam
Inverse trigFR_atan, FR_atan2, FR_asin, FR_acos
Log / expFR_log2, FR_ln, FR_log10, FR_pow2, FR_EXP, FR_POW10, FR_EXP_FAST, FR_POW10_FAST, FR_MULK28
RootsFR_sqrt, FR_hypot, FR_hypot_fast8
Wave generatorsfr_wave_sqr, fr_wave_pwm, fr_wave_tri, fr_wave_saw, fr_wave_tri_morph, fr_wave_noise
Envelopefr_adsr_init, fr_adsr_trigger, fr_adsr_release, fr_adsr_step
2D transformsFR_Matrix2D_CPT (mul, add, sub, det, inv, setrotate, XFormPtI, XFormPtI16)
Formatted outputFR_printNumD, FR_printNumF, FR_printNumH, FR_numstr

Every function is covered by the TDD characterization suite in the repo.

Lean build options

Compile-time #define guards let you strip optional subsystems for ROM-constrained targets. Define them before including FR_math.h (or pass -D on the compiler command line):

DefineWhat it removesTypical savings
FR_LEANDegree trig, BAM tan, angle converters, FR_log10, FR_hypot, waves + ADSR~3.7 KB
FR_NO_PRINTFR_printNumF, FR_printNumD, FR_printNumH, FR_numstr~1.3 KB
FR_NO_WAVESfr_wave_* (6 shapes), fr_adsr_* (ADSR envelope), FR_HZ2BAM_INC~0.6 KB

FR_LEAN keeps only radian trig (sin, cos, tan), inverse trig, sqrt, log2, ln, exp, pow2, and arithmetic — comparable to libfixmath’s API but at 4.7 KB text vs libfixmath’s 4.9 KB + 112 KB BSS. With FR_LEAN + FR_NO_PRINT the library compiles to ~4.7 KB on x86-64 / clang -Os.

/* Example: headless sensor node — math only, no print, no audio */
#define FR_NO_PRINT
#define FR_NO_WAVES
#include "FR_math.h"

With -ffunction-sections and linker --gc-sections, the linker will also strip any unused functions automatically, so these guards are most useful when you include the library as a single .c file or static archive without section-level dead-code elimination.

Why fixed-point?

Many modern microcontrollers have an FPU and can use float freely. Older and low-cost MCUs remain common. Fixed-point is often faster and more deterministic than float, and it excels in situations like:

FR_Math is engineered for these use cases. It does not try to be a generic float replacement.

Quick taste

#include "FR_math.h"

#define R 16  /* work at radix 16 (s15.16) throughout */

/* ---- Creating fixed-point values ----
 *
 * FR_NUM(integer, frac_digits, num_digits, radix) encodes a decimal
 * literal at compile time.  The fractional part is the digits AFTER
 * the decimal point, and num_digits says how many digits that is.
 * Think: FR_NUM(3, 14159, 5, 16) means "3.14159" at radix 16.
 */
s32 pi   = FR_NUM(3, 14159, 5, R);  /* 3.14159 → raw 205886 at r16  */
s32 half = FR_NUM(0, 5, 1, R);      /* 0.5     → raw 32768           */
s32 neg  = FR_NUM(-2, 75, 2, R);    /* -2.75   → raw -180224         */

/* Or parse from a string at runtime (no floats, no strtod): */
s32 pi2  = FR_numstr("3.14159", R); /* same result as FR_NUM above    */

/* Integer-to-fixed: I2FR(n, radix) just shifts left */
s32 two  = I2FR(2, R);              /* 2.0 → raw 131072              */

/* ---- Naming convention: macros vs functions ----
 *
 * UPPERCASE FR_ names are macros — they expand inline with no call
 * overhead, and the compiler can constant-fold them.  Use these for
 * conversions and simple arithmetic:
 *   I2FR, FR2I, FR_NUM, FR_ADD, FR_DIV, FR_ABS, FR_CHRDX, FR_EXP ...
 *
 * MixedCase FR_ names are legacy functions — they still work but
 * map to the current lowercase names:
 *   FR_Cos → fr_cos_deg, FR_Sin → fr_sin_deg, FR_Tan → fr_tan_deg
 *
 * lowercase fr_ names are the current API (degree wrappers, radian
 * trig, BAM trig, wave generators, ADSR envelopes):
 *   fr_cos_deg, fr_sin_deg, fr_tan_deg, fr_sin, fr_cos, fr_tan,
 *   fr_wave_tri, fr_adsr_step ...
 *
 * Other MixedCase / lowercase FR_ names are functions with loops,
 * tables, or multi-step algorithms:
 *   FR_sqrt, FR_atan2, FR_log2, FR_pow2, FR_printNumF ...
 *
 * Some macros wrap functions: FR_EXP(x,r) scales x then calls
 * FR_pow2 — one-liner convenience, heavy lifting in the function.
 */

/* ---- Math functions ---- */
s32 c45   = fr_cos_deg(45, 0);             /* cos(45°) = 0.7071       */
s32 s30   = fr_sin(FR_numstr("0.5236", R), R); /* sin(0.5236 rad)    */
s32 root2 = FR_sqrt(two, R);              /* sqrt(2)  = 1.4142       */
s32 angle = FR_atan2(I2FR(1,R), I2FR(1,R), R); /* atan2(1,1) rad     */
s32 lg    = FR_log2(I2FR(1000, R), R, R); /* log2(1000) ~ 9.97       */
s32 ex    = FR_EXP(I2FR(1, R), R);        /* macro: scales then calls
                                            * FR_pow2 internally      */

/* ---- Printing (serial / UART / file friendly) ----
 *
 * FR_printNumF takes a per-character output function — works with
 * putchar, Serial.write, UART_putc, or any int(*)(char).  No
 * sprintf, no floats, no heap.  Ideal for bare-metal targets.
 */
int my_putchar(char c) { return putchar(c); }  /* or your UART func */

FR_printNumF(my_putchar, pi, R, 8, 5);    /* prints " 3.14159"      */
FR_printNumF(my_putchar, neg, R, 8, 2);   /* prints "   -2.75"      */
FR_printNumD(my_putchar, FR2I(lg, R), 4); /* prints "   9" (integer)*/

See Getting Started for a complete walkthrough, or jump straight to the Fixed-Point Primer if you want to understand how the radix notation works first.

Comparison

FeaturelibfixmathCMSIS-DSPFR_Math
Fixed formatQ16.16 onlyQ31 / Q15Any radix
Angle inputRadians (Q16.16)Radians (float)BAM (u16), degrees, or radians
Exact cardinal anglesNoN/AYes
Multiply-free optionNoNoYes (e.g. FR_EXP_FAST, FR_hypot_fast8)
Wave generatorsNoNo6 shapes + ADSR
DependenciesNoneARM onlyNone
Code size (Cortex-M0, -Os)2.4 KB~40 KB+3.4 KB lean / 5.7 KB full

Sizes measured with arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -Os. libfixmath covers trig/sqrt/exp in Q16.16 only; FR_Math includes log/ln/log10, wave generators, ADSR, print helpers, and variable radix. CMSIS-DSP estimate is for the math function subset only. See scripts/crossbuild_sizes.sh for the build script.

History

FR_Math has been in service since 2000, originally built for graphics transforms on 16 MHz 68k Palm Pilots (it shipped inside Trumpetsoft’s Inkstorm), then ported forward to ARM, x86, MIPS, RISC-V, and various 8/16-bit embedded targets. The current release has a full test suite, bit-exact numerical specification, and CI on every push.

License

BSD-2-Clause. Use it freely in open source or commercial projects.