Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 133 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[![License](https://img.shields.io/badge/License-BSD%202--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause)
[![CI](https://github.com/deftio/fr_math/actions/workflows/ci.yml/badge.svg)](https://github.com/deftio/fr_math/actions/workflows/ci.yml)
[![Coverage](https://img.shields.io/badge/coverage-99%25-brightgreen.svg)](#building-and-testing)
[![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen.svg)](#building-and-testing)
[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://deftio.github.io/fr_math/)
[![Version](https://img.shields.io/badge/version-2.0.5-blue.svg)](release_notes.md)
[![Version](https://img.shields.io/badge/version-2.0.6-blue.svg)](release_notes.md)

# FR_Math: A C Language Fixed-Point Math Library for Embedded Systems

Expand All @@ -20,37 +20,85 @@ beyond `<stdint.h>`.

### Library size (FR_math.c only, `-Os`)

Compiled object code sizes on select platforms (static test build). Your
sizes may vary depending on optimization and linker settings. Sizes
include all code and internal tables; everything is ROMable.

| Target | Code (text) |
|--------|-------------|
| Cortex-M0 (Thumb-1) | 4.2 KB |
| Cortex-M4 (Thumb-2) | 4.1 KB |
| ESP32 (Xtensa) | 4.6 KB |
| 68k | 5.5 KB |
| x86-64 | 5.8 KB |
| RISC-V 32 (rv32im) | 6.5 KB |
| x86-32 | 7.2 KB |
| MSP430 (16-bit) | 8.4 KB |
| 8051 (SDCC) | 20.4 KB * |

Sizes are code-only (text section). The optional 2D module adds ~1 KB.
\* 8051 and MSP430 are 8/16-bit — every 32-bit operation expands to multiple instructions.
| ARM Thumb (Cortex-M0/M4) | 4.2 KB |
| RISC-V 32 (rv32imac) | 4.7 KB |
| RISC-V 64 | 4.5 KB |
| Xtensa LX106 (ESP8266) | 5.2 KB |
| 68k | 5.3 KB |
| ARM32 | 5.4 KB |
| x86-64 (GCC) | 5.7 KB |
| AArch64 (ARM64) | 6.0 KB |
| x86-64 (Clang) | 6.4 KB |
| x86-32 | 6.8 KB |
| PowerPC | 7.4 KB |
| MSP430 (16-bit) | 8.9 KB * |
| AVR (ATmega328P) | 10.6 KB * |

The optional 2D module adds ~1 KB.
\* MSP430 and AVR are 8/16-bit — every 32-bit operation expands to multiple instructions.
See [`docker/`](docker/) for the cross-compile setup.

### Lean build options

Two 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):

| Define | What it removes | Typical savings |
|---|---|---|
| `FR_NO_PRINT` | `FR_printNumF`, `FR_printNumD`, `FR_printNumH`, `FR_numstr` | ~1.3 KB |
| `FR_NO_WAVES` | `fr_wave_*` (6 shapes), `fr_adsr_*` (ADSR envelope), `FR_HZ2BAM_INC` | ~0.6 KB |

With both guards enabled the core math library (trig, inverse trig, log/exp,
sqrt, hypot) compiles to ~3.5 KB on x86-64 / clang -Os. On Thumb-2 this
would be roughly 2.6 KB.

```c
/* 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.

### 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.

| Function | Max error | Note |
|---|---|---|
| sin / cos | 5 LSB (~7.7e-5) | Exact at 0, 90, 180, 270 |
| sqrt | ≤ 0.5 LSB | Round-to-nearest |
| log2 | ≤ 4 LSB | 65-entry mantissa table |
| pow2 | ≤ 1 LSB (integers exact) | 65-entry fraction table |
| ln, log10 | ≤ 4 LSB | Via FR_MULK28 from log2 |
| hypot (exact) | ≤ 0.5 LSB | 64-bit intermediate |
| hypot_fast (4-seg) | 0.34% | Shift-only, no multiply |
| hypot_fast8 (8-seg) | 0.10% | Shift-only, no multiply |
Percent errors skip expected values near zero (|expected| < 0.01).

At other radixes (3-bit, 24-bit, etc.) accuracy will differ due to the
number of fractional bits available. All functions support radix 0 to 30.

<!-- ACCURACY_TABLE_START -->
| Function | Max err (LSB) | Max err (%) | Avg err (%) | Note |
|---|---:|---:|---:|---|
| sin / cos | 7.5 | 0.7169 | 0.0100 | 65536-pt sweep + specials |
| tan | 38020.4 | 0.7118 | 0.0162 | 65536-pt sweep (skip poles) |
| asin / acos | 42.3 | 0.7025 | 0.0105 | 65536-pt; sqrt approx near boundary |
| atan2 | 63.3 | 0.4953 | 0.0268 | 65536x5 radii; asin/acos+hypot_fast8 |
| atan | 61.9 | 0.2985 | 0.0159 | 20001-pt sweep [-10,10]; via FR_atan2 |
| sqrt | 28.4 | 0.0003 | 0.0000 | Round-to-nearest |
| log2 | 10.5 | 0.2479 | 0.0045 | 65-entry mantissa table |
| pow2 | 220.4 | 0.1373 | 0.0057 | 65-entry fraction table |
| ln, log10 | 0.7 | 0.0015 | 0.0004 | Via FR_MULK28 from log2 |
| exp | 65.7 | 0.0719 | 0.0051 | FR_MULK28 + FR_pow2 |
| exp_fast | 195.5 | 0.0719 | 0.0064 | Shift-only scaling |
| pow10 | 143.4 | 0.1163 | 0.0075 | FR_MULK28 + FR_pow2 |
| pow10_fast | 581.9 | 0.1163 | 0.0100 | Shift-only scaling |
| hypot (exact) | 0.2 | 0.0001 | 0.0000 | 64-bit intermediate |
| hypot_fast8 (8-seg) | 59968.8 | 0.0977 | 0.0508 | Shift-only, no multiply |
<!-- ACCURACY_TABLE_END -->

### What's in the box

Expand All @@ -62,7 +110,7 @@ radix — Q16.16 is just the reference point for the table.
| Trig (radian/BAM) | `fr_sin`, `fr_cos`, `fr_tan`, `fr_sin_bam`, `fr_cos_bam`, `fr_sin_deg`, `fr_cos_deg` |
| Inverse trig | `FR_atan`, `FR_atan2`, `FR_asin`, `FR_acos` |
| Log / exp | `FR_log2`, `FR_ln`, `FR_log10`, `FR_pow2`, `FR_EXP`, `FR_POW10`, `FR_EXP_FAST`, `FR_POW10_FAST`, `FR_MULK28` |
| Roots | `FR_sqrt`, `FR_hypot`, `FR_hypot_fast`, `FR_hypot_fast8` |
| Roots | `FR_sqrt`, `FR_hypot`, `FR_hypot_fast8` |
| Wave generators | `fr_wave_sqr`, `fr_wave_pwm`, `fr_wave_tri`, `fr_wave_saw`, `fr_wave_tri_morph`, `fr_wave_noise` |
| Envelope | `fr_adsr_init`, `fr_adsr_trigger`, `fr_adsr_release`, `fr_adsr_step` |
| 2D transforms | `FR_Matrix2D_CPT` (mul, add, sub, det, inv, setrotate, XFormPtI, XFormPtI16) |
Expand All @@ -74,7 +122,7 @@ radix — Q16.16 is just the reference point for the table.
git clone https://github.com/deftio/fr_math.git
cd fr_math
make lib # build static library
make test # run all tests (coverage, TDD characterization, 2D)
make test # run all tests (unit, TDD characterization, 2D)
```

## Quick taste
Expand All @@ -84,11 +132,62 @@ make test # run all tests (coverage, TDD characterization, 2D)

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

s32 pi = FR_NUM(3, 14159, 5, R); /* pi at radix 16 */
s32 c45 = FR_CosI(45); /* cos 45 deg = 0.7071 (s15.16) */
s32 root2 = FR_sqrt(I2FR(2, R), R); /* sqrt(2) = 1.4142 */
s32 lg = FR_log2(I2FR(1000, R), R, R); /* log2(1000) ~ 9.97 */
s32 ex = FR_EXP(I2FR(1, R), R); /* e^1 ~ 2.7183 */
/* ---- 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 functions — they contain loops, tables, or
* multi-step algorithms where inlining would waste ROM:
* FR_Cos, FR_sqrt, FR_atan2, FR_log2, FR_pow2, FR_printNumF ...
*
* lowercase fr_ names are v2 functions (radian trig, wave generators,
* ADSR envelopes):
* fr_sin, fr_cos, fr_tan, fr_wave_tri, fr_adsr_step ...
*
* 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(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)*/
```

## Documentation
Expand All @@ -111,7 +210,7 @@ The full docs ship in two forms — pick whichever fits how you read.
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* on PalmOS, then moved forward through ARM, x86, MIPS,
RISC-V, and various 8/16-bit embedded targets. v2.0.2 is the current
RISC-V, and various 8/16-bit embedded targets. v2.0.6 is the current
release with a full test suite, bit-exact numerical specification, and
CI on every push.

Expand All @@ -127,5 +226,5 @@ BSD-2-Clause — see [LICENSE.txt](LICENSE.txt).

## Version

2.0.2 — see [release_notes.md](release_notes.md) for the v1 → v2
2.0.6 — see [release_notes.md](release_notes.md) for the v1 → v2
migration guide, numerical fixes, and new functionality.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.5
2.0.6
9 changes: 9 additions & 0 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ Versioned files (all synced automatically):
4. Verify `llms.txt` and `agents.md` are current with any API changes
5. Commit, tag, push

## Lean build options

Define before including `FR_math.h` to exclude optional subsystems:

| Define | Removes | Savings |
|---|---|---|
| `FR_NO_PRINT` | `FR_printNumF/D/H`, `FR_numstr` | ~1.3 KB |
| `FR_NO_WAVES` | `fr_wave_*`, `fr_adsr_*`, `FR_HZ2BAM_INC` | ~0.6 KB |

## Platform targets

The library compiles on: AVR (Arduino), ARM Cortex-M0/M4, ESP32,
Expand Down
10 changes: 10 additions & 0 deletions compare_lfm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Build artifacts
build/

# Cloned third-party libraries (fetch from GitHub as needed)
libfixmath/
fpm/
liquid-fpm/

# Claude session data
.claude/
102 changes: 102 additions & 0 deletions compare_lfm/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# ============================================================
# Benchmark: FR_math vs libfixmath (macOS ARM / Apple Clang)
# ============================================================
# WARNING: This Makefile only builds files inside .compare/
# It does NOT modify anything in the parent repo.
# ============================================================

CXX := clang++
CC := clang
CXXFLAGS := -std=c++17 -O2 -Wall -Wextra
CFLAGS := -std=c11 -O2 -Wall -Wextra

# --- FR_math (parent repo, compiled read-only) ---
FR_SRC := ../src/FR_math.c
FR_INC := -I../src

# --- libfixmath (cloned into this dir) ---
LFM_DIR := libfixmath/libfixmath
LFM_SRC := $(LFM_DIR)/fix16.c \
$(LFM_DIR)/fix16_sqrt.c \
$(LFM_DIR)/fix16_exp.c \
$(LFM_DIR)/fix16_trig.c \
$(LFM_DIR)/fix16_str.c \
$(LFM_DIR)/uint32.c \
$(LFM_DIR)/fract32.c
LFM_INC := -I$(LFM_DIR)

# --- Build ---
BUILD := build
TARGET := $(BUILD)/benchmark
JSON_OUT := comparison_results.json

OBJS := $(BUILD)/FR_math.o \
$(BUILD)/fix16.o \
$(BUILD)/fix16_sqrt.o \
$(BUILD)/fix16_exp.o \
$(BUILD)/fix16_trig.o \
$(BUILD)/fix16_str.o \
$(BUILD)/uint32.o \
$(BUILD)/fract32.o \
$(BUILD)/benchmark.o

.PHONY: all clean run size

all: $(TARGET)

run: $(TARGET)
./$(TARGET) > $(JSON_OUT) 2>comparison_summary.md
@echo "Results written to $(JSON_OUT) + comparison_summary.md"

# Size comparison: ROM + RAM for both libraries
LFM_OBJS := $(BUILD)/fix16.o $(BUILD)/fix16_sqrt.o $(BUILD)/fix16_exp.o \
$(BUILD)/fix16_trig.o $(BUILD)/fix16_str.o $(BUILD)/uint32.o \
$(BUILD)/fract32.o

size: $(BUILD)/FR_math.o $(LFM_OBJS)
@echo "=== Compiled size: FR_math vs libfixmath ($(CC) -O2) ==="
@echo ""
@echo "FR_math (FR_math.o):"
@size -m $(BUILD)/FR_math.o | grep -E "Section|total"
@echo ""
@echo "libfixmath (all objects):"
@for f in $(LFM_OBJS); do echo " $$(basename $$f):"; size -m $$f | grep -E "Section" | sed 's/^/ /'; done

$(TARGET): $(OBJS) | $(BUILD)
$(CXX) $(CXXFLAGS) -o $@ $^

$(BUILD):
mkdir -p $(BUILD)

# FR_math object
$(BUILD)/FR_math.o: $(FR_SRC) | $(BUILD)
$(CC) $(CFLAGS) $(FR_INC) -c -o $@ $<

# libfixmath objects
$(BUILD)/fix16.o: $(LFM_DIR)/fix16.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

$(BUILD)/fix16_sqrt.o: $(LFM_DIR)/fix16_sqrt.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

$(BUILD)/fix16_exp.o: $(LFM_DIR)/fix16_exp.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

$(BUILD)/fix16_trig.o: $(LFM_DIR)/fix16_trig.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

$(BUILD)/fix16_str.o: $(LFM_DIR)/fix16_str.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

$(BUILD)/uint32.o: $(LFM_DIR)/uint32.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

$(BUILD)/fract32.o: $(LFM_DIR)/fract32.c | $(BUILD)
$(CC) $(CFLAGS) $(LFM_INC) -c -o $@ $<

# Benchmark harness
$(BUILD)/benchmark.o: benchmark.cpp | $(BUILD)
$(CXX) $(CXXFLAGS) $(FR_INC) $(LFM_INC) -c -o $@ $<

clean:
rm -rf $(BUILD) $(JSON_OUT)
Loading
Loading