Tangent accuracy rewrite and trig rounding fix.
- New
fr_tan_bam(u16 bam)function with a dedicated 65-entry octant lookup table (gFR_TAN_TAB_OinFR_trig_table.h, 130 bytes ROM). First octant uses direct table + lerp; second octant uses the reciprocal identitytan(x) = 1/tan(90-x)with one 32-bit division. No 64-bit intermediates anywhere in the tan path. FR_TanI,FR_Tan,fr_tanare now thin wrappers overfr_tan_bam. The old sin/cos division implementation is removed.- Peak error: 0.17% (BAM), 0.60% (deg r7), 0.17% (rad r16).
fr_cos,fr_sin,fr_tan,FR_Cos,FR_Sin,FR_Tannow add 0.5 LSB (1 << (radix-1)) before the>> radixshift when converting from radians/degrees to BAM. This rounds to the nearest BAM value instead of truncating, eliminating a systematic 1-BAM rounding error that caused ~1% peak error near zero crossings.- Radian-path sin/cos/tan now match BAM-native accuracy (0.16-0.17% peak, was ~1.03%).
FR_DEG2BAM: 10 terms (~28 bits) reduced to 7 terms (~18 bits)FR_RAD2BAM: 9 terms (~27 bits) reduced to 7 terms (~21 bits)FR_DEG2RAD: 3 terms (~13 bits) extended to 5 terms (~17 bits)- 18 bits of precision gives 4 bits of headroom over the 14-bit effective BAM resolution of the trig tables. Verified: reverting to the old full-precision macros changes sin/cos peak error by <0.04%.
FR_TRIG_MINVALfixed: was-FR_TRIG_MASK(-65535), now-FR_TRIG_MAXVAL(-2147483647) to properly pair withFR_TRIG_MAXVALfor tan saturation clamping.- Accuracy table in all docs now shows separate BAM/deg/rad rows for sin/cos and tan, matching the TDD characterization report.
fr_tan_bamadded to function listings across README, docs, HTML pages, and llms.txt.
README restructure, accuracy table cleanup, and expanded cross-compile support.
A single #define FR_CORE_ONLY before including FR_math.h (or -DFR_CORE_ONLY
on the command line) now strips both print helpers and wave generators in one step.
It expands to FR_NO_PRINT + FR_NO_WAVES internally.
The LSB column has been removed from the accuracy table output. Percent error is the metric that matters to callers; LSB error is an implementation detail that varies with the chosen radix.
- RP2040 (Cortex-M0+) and STM32 (Cortex-M4) added as named targets in the Docker cross-build
- 68HC11 and MIPS32 toolchains added to the Docker image
- Size table now shows three columns: Lean, Core, and Full
- Consolidated
scripts/crossbuild_sizes.sh— single script runs Docker, builds all targets, writes CSV + markdown, and patches doc files (replacescrossbuild-docker.sh,size_report.sh,update_sizes.sh) - Size table sorted by architecture width (8-bit → 64-bit)
Sections reordered: accuracy table moved above the size table to lead with the library's primary selling point. Badges cleaned up from Quikdown HTML to standard markdown syntax. Build flavor descriptions made more concise.
Accuracy improvements, lean-build options, and library cleanup.
- FR_acos boundary fix: deferred quantization computes
1-xat the caller's radix instead of r15, giving 12x better accuracy near ±1.0 (max LSB error 512.6 → 42.3) - FR_atan2 rewrite: uses asin/acos + hypot_fast8 with octant switching for well-conditioned results everywhere (0.41% peak vs 20% for libfixmath)
Two new compile-time #define guards strip optional subsystems for
ROM-constrained targets:
| 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 |
With both guards the core math library compiles to ~3.5 KB on x86-64 (clang -Os), roughly 2.6 KB on Thumb-2.
- FR_hypot_fast (4-segment) deleted — FR_hypot_fast8 (8-segment) is strictly better in both accuracy (0.10% vs 0.34%) and is used internally by FR_atan2. The 4-segment variant was dead weight.
- libfixmath comparison benchmark (
compare_lfm/) added to repo - Documentation updated across all markdown and HTML pages
Release pipeline fixes. No functional changes to the math library.
- Fixed
tools/make_release.shfailing after squash-merge: local master diverges from origin (squash creates a new commit), sogit pull --ff-onlyfails. Now detects divergence and resets to origin/master. - Fixed on-master release path: script now pushes master to origin and waits for CI before tagging (previously skipped both, causing tags to point at commits not yet on the remote).
- Release pipeline auto-commits pipeline-generated changes (badge updates, version sync) instead of failing on a dirty working tree. Unexpected dirty files still block the release.
CI fix release. No functional changes to the math library.
- Fixed
release.ymlcoverage step failing due to stale gcov invocation - Removed conflicting auto-release job from
ci.yml(replaced by tag-triggeredrelease.yml) - Documentation updated:
release_management.md,docs/building.md, andpages/guide/building.htmlnow accurately describe the guided release pipeline
CI and release pipeline cleanup. No functional changes to the math library itself.
- New guided release script
tools/make_release.sh(ported from xelp): validates, opens PR, waits for CI, merges, tags, waits for GitHub Release, then publishes to PlatformIO and ESP-IDF registries - Removed old
scripts/make_release.sh(validation-only, manual merge) - All doc/page references updated to
tools/make_release.sh - Cleaned up stale branches
Embedded library publishing support. No functional changes to the math library itself — this release adds package manager integration and improves example organization.
- Added
library.propertiesfor Arduino Library Manager - Added
library.jsonfor PlatformIO Registry - Added
idf_component.yml+CMakeLists.txtfor ESP-IDF Component Registry - Added
keywords.txtfor Arduino IDE syntax highlighting - Added
.github/workflows/release.ymlfor tag-triggered release validation
- New focused Arduino sketches:
basic-math,trig-functions,wave-generators - Moved
FR_Math_Example1.cpptoexamples/posix-example/ - Removed symlinks from
examples/arduino_smoke/(Arduino forbids symlinks) - Added
examples/README.md
- Added
llms.txt— machine-readable API summary for AI coding agents - Added
agents.md— coding agent conventions and contribution guide - Added
dev/misc/publish checklists for Arduino, PlatformIO, and ESP-IDF - Updated
scripts/sync_version.shto sync version across all new metadata files - Added release step 11: verify
llms.txtandagents.mdare current
Precision and accuracy release. All changes are backward-compatible with v2.0.0 except where noted.
fr_cos_bam/fr_sin_bamnow return s15.16 (was s0.15). Exact values at cardinal angles:cos(0°) = 65536,cos(90°) = 0.FR_TRIG_ONE = 65536.FR_TanI/fr_tanreturn s15.16 (was s16.15). Saturation is now±INT32_MAX.- All wrappers updated:
FR_Cos,FR_Sin,FR_CosI,FR_SinI,FR_TanI,FR_Tan.
FR_acos,FR_asin,FR_atangain anout_radixparameter and return radians at that radix (was degrees ass16).FR_atan2(y, x, out_radix)also returns radians.FR_BAM2RADmacro corrected (was off by a factor of 1024).
FR_FixMuls/FR_FixMulSat: add 0.5 LSB (+0x8000) before the>>16shift. Both now round to nearest instead of truncating.FR_sqrt/FR_hypot: the internalfr_isqrt64now rounds to nearest (remainder > root → +1). Worst-case error drops from <1 LSB to ≤ 0.5 LSB.FR_DIVnow rounds to nearest (≤ 0.5 LSB error) instead of truncating.FR_DIV_TRUNCpreserves the old truncating behaviour.
FR_pow2table expanded from 17 entries (16 segments) to 65 entries (64 segments, 260 bytes). Interpolation error drops by ~16×.FR_log2table expanded from 33 entries to 65 entries (6-bit index / 24-bit interpolation). Worst-case error ≤ 4 LSB at Q16.16.FR_MULK28macro added: multiplies any fixed-point value by a radix-28 constant using a 64-bit intermediate with round-to-nearest. ~9 decimal digits of precision.FR_EXPandFR_POW10now useFR_MULK28for the base conversion instead of shift-only macros.FR_lnandFR_log10also useFR_MULK28internally.FR_EXP_FASTandFR_POW10_FASTadded as shift-only alternatives for 8-bit targets where 64-bit multiply is expensive.
FR_MIN,FR_MAX,FR_CLAMP— standard min/max/clamp.FR_DIV_TRUNC(x, xr, y, yr)— truncating division (the oldFR_DIVbehaviour).FR_MOD(x, xr, y, yr)— fixed-point modulus.- Radix-28 constants:
FR_kLOG2E_28,FR_krLOG2E_28,FR_kLOG2_10_28,FR_krLOG2_10_28.
| Change | v2.0.0 | v2.0.1 |
|---|---|---|
| sin/cos return type | s16 (s0.15) |
s32 (s15.16) |
| sin/cos 1.0 value | 32767 | 65536 (exact) |
| tan return format | s16.15 (radix 15) | s15.16 (radix 16) |
| tan saturation | ±(32767 << 15) |
±INT32_MAX |
| FR_acos/asin signature | (input, radix) → s16 degrees |
(input, radix, out_radix) → s32 radians |
| FR_atan signature | (input, radix) → s16 degrees |
(input, radix, out_radix) → s32 radians |
| FR_atan2 signature | (y, x) → s16 degrees |
(y, x, out_radix) → s32 radians |
| FR_BAM2RAD | off by 1024× (bug) | correct |
| FR_DIV rounding | truncates toward zero | rounds to nearest (use FR_DIV_TRUNC for old behaviour) |
This is a significant bug-fix and precision-improvement release. Several
v1 functions produced wrong numerical results on every platform due to
arithmetic bugs; others produced wrong results specifically on 64-bit
hosts because of a typedef choice. Every changed function is covered by
the TDD characterization suite in tests/test_tdd.cpp.
See dev/fr_math_precision.md for the full per-symbol reference
(inputs, outputs, precision, saturation) and dev/fr_math2_impl_plan.md
for the implementation plan this release executed.
FR_defs.h: migrated typedefs to<stdint.h>(s8→int8_t,s32→int32_t, etc.). v1 defineds32assigned long, which is 64 bits on LP64 platforms (Linux and macOS on x64 / ARM64), so every fixed-point computation that relied ons32being exactly 32 bits silently produced wrong answers on desktop Linux and macOS. C99<stdint.h>is now mandatory in v2 (every modern toolchain — gcc, clang, MSVC, IAR, Keil C51, sdcc, MSP430-gcc, AVR-gcc, RISC-V/ARM — ships it). If you are stuck on a pre-C99 compiler, FR_Math 1.0.x remains the version for you.
FR_FixMulSat/FR_FixMuls: v1 used a split-multiply formula with an algebraic bug that returned wrong values for certain sign combinations. v2 uses anint64_tfast path with explicit saturation.FR_log2: v1 was missing the accumulator in the mantissa-table step and returned wrong values for non-power-of-2 inputs. v2 rewrites the algorithm: leading-bit-position → normalize to s1.30 → 33-entry mantissa lookup with linear interpolation.FR_ln/FR_log10: inherit theFR_log2fix automatically (they are multiplies ofFR_log2by a constant).FR_pow2: v1 used C truncation toward zero to extract the integer exponent, which gave wrong answers for negative non-integer inputs (e.g.FR_pow2(-0.5)returned ~2 instead of ~0.707). v2 uses mathematical floor (toward −∞) and a 17-entry fraction table with linear interp.FR_atan2: v1 was a placeholder that returned garbage. v2 is a correct octant-reduced arctan with a 33-entry table. Max error ≤ 1°. v2 also drops the vestigialradixparameter — new signature isFR_atan2(s32 y, s32 x).FR_atan: v1 was wired up incorrectly. v2 implements asFR_atan2(input, 1<<radix).FR_Tan: v1 useds16loop variables that overflowed for angles near 90°. v2 usess32.FR_TanI: removed dead unreachable code (if (270 == deg)).FR_printNumD: v1 invoked unary minus onINT_MIN, which is undefined behavior, and always returned 0. v2 works in unsigned magnitude and returns the real byte count (or −1 on nullf).FR_printNumF: same INT_MIN fix, plus v1's fraction extraction was mathematically wrong and printed bogus tail digits.FR_printNumH: v1 shifted a signed negative int right. v2 casts to unsigned first.
FR_DEG2RADandFR_RAD2DEG: v1 had the macro bodies swapped relative to their names, soFR_DEG2RAD(x)actually multiplied by 57.3 andFR_RAD2DEG(x)multiplied by 0.017. v2 swaps them back.FR_DEG2RADalso had missing parens aroundxin a subexpression.FR_NUM: v1 signature wasFR_NUM(i, f, r)and the body ignored thefargument entirely. v2 adds an explicitddigit-count argument — new signature isFR_NUM(i, f, d, r). Any caller of the v1 form must be updated.
- Radian-native trig (
fr_cos,fr_sin,fr_tan,fr_cos_bam,fr_sin_bam,fr_cos_deg,fr_sin_deg): takes radians (or BAM, or integer degrees) directly rather than forcing everything through integer degrees. Uses a new 129-entry s0.15 quadrant cosine table insrc/FR_trig_table.hwith linear interpolation and round-to-nearest. Max error ≤ 1 LSB of s0.15 (~3e−5). Mean error ~0. - BAM (Binary Angular Measure) macros:
FR_DEG2BAM,FR_BAM2DEG,FR_RAD2BAM,FR_BAM2RAD. BAM is the natural integer representation for trig: 16 bits per full circle, top 2 bits select the quadrant, next 7 bits index the table, bottom 7 bits drive interpolation. - Table size is a compile-time knob:
-DFR_TRIG_TABLE_BITS=8gives a 257-entry table for halved worst-case error (default is 7 / 129 entries). - Square root and hypot (
FR_sqrt,FR_hypot): radix-aware fixed point square root and 2D vector magnitude.FR_sqrtuses a digit-by-digit (shift-and-subtract) integer isqrt onint64_t. Negative input returnsFR_DOMAIN_ERROR(INT32_MIN).FR_hypotcomputessqrt(x^2 + y^2)with no intermediate overflow up to the full s32 range. Bit-exact for perfect squares; max error ~1 LSB at the requested radix. - Fast approximate magnitude (
FR_hypot_fast8): shift-only piecewise-linear approximation ofsqrt(x^2 + y^2)— no multiply, no divide, no 64-bit math, no ROM table, no iteration.FR_hypot_fast8uses 8 segments (~0.14% peak error). Based on the method of US Patent 6,567,777 B1 (Chatterjee, public domain). Noradixparameter needed — the algorithm is scale-invariant. - Wave function family for embedded audio / LFOs / control
signals. All take a
u16BAM phase and return s0.15 (s16, ±32767):fr_wave_sqr(phase)— symmetric square wave.fr_wave_pwm(phase, duty)— variable-duty pulse wave;dutyis au16threshold in BAM units.fr_wave_tri(phase)— symmetric triangle, peaks clamped to ±32767. Max error vs ideal triangle ~3e−5 (1 LSB s0.15).fr_wave_saw(phase)— sawtooth,(s16)(phase - 0x8000)with boundary clamp.fr_wave_tri_morph(phase, break_point)— variable-symmetry triangle that morphs into a sawtooth asbreak_pointapproaches0or0xffff. Returns unipolar [0, 32767]. Uses one division per sample.fr_wave_noise(state)— 32-bit Galois LFSR (poly0xD0000001, period 2^32 − 1) returning a full-range s16. Caller owns theu32state; seed with any non-zero value.
FR_HZ2BAM_INC(hz, sample_rate): phase increment helper. Computeshz * 65536 / sample_rateso a u16 phase accumulator driven by this increment produces the requested frequency. Example:FR_HZ2BAM_INC(440, 48000) = 600(≈ 439.45 Hz).- ADSR envelope generator (
fr_adsr_t,fr_adsr_init,fr_adsr_trigger,fr_adsr_release,fr_adsr_step): linear-segment attack/decay/sustain/release envelope. Internal levels are stored as s1.30 so very long durations (e.g. 48000-sample attack at 48 kHz) still get a non-zero per-sample increment;fr_adsr_stepreturns s0.15 for direct multiplication into a wave sample. State machine exposes constantsFR_ADSR_IDLE/_ATTACK/_DECAY/_SUSTAIN/_RELEASE.
dev/fr_math_precision.md— comprehensive per-symbol precision reference. Every public macro, constant, and function is documented with inputs, output format, worst-case error, saturation behavior, and side-effect notes.CONTRIBUTING.md— PR expectations, test discipline, portability rules, commit message format.tools/interp_analysis.html— interactive Chart.js analysis of trig interpolation methods. Compares nearest-neighbor, linear truncated, linear rounded, cosine interp, smoothstep, Hermite cubic, and Catmull-Rom on the same 129-entry table, over θ ∈ [−45°, +45°]. Includes pan/zoom on a second chart showing the actual interpolated values vsMath.cosreference. Use this to verify interpolation claims and to pick a default for the library.scripts/clean_build.sh— one-shot clean ofbuild/andcoverage/directories. Handy when switching branches or debugging stale object files.
FR_NUMsignature:FR_NUM(i, f, r)→FR_NUM(i, f, d, r). Any code that called the 3-argument form will fail to compile. The old form was broken (it ignoredfentirely), so any caller was already getting wrong results.FR_atan2signature:FR_atan2(y, x, radix)→FR_atan2(y, x). The radix parameter was vestigial (ignored by the v1 placeholder and unnecessary in v2).FR_RESULTandFR_E_*codes removed: v1's HRESULT-style return codes are gone. The matrixinv()methods now returnbool(trueon success,falseif singular).add(),sub(), andsetrotate()returnvoid. Math functions that can hit a domain error return the named sentinelFR_DOMAIN_ERROR; saturating arithmetic returnsFR_OVERFLOW_POS/FR_OVERFLOW_NEG.FR_SQUAREandFR_FIXMUL32uremoved: these s16.16-only macros were narrow specializations ofFR_FixMuls/FR_FixMulSat. Use the generic versions, which now have anint64_tfast path and work at any radix.FR_NO_INT64andFR_NO_STDINTbuild flags removed: every C99-or-newer toolchain on every architecture (including 8-bit targets like sdcc, AVR-gcc, MSP430-gcc) provides<stdint.h>and 64-bit integer arithmetic, so the conditional fallbacks were carrying their weight in maintenance and ROM for no benefit.- Wider intermediate types: if you had code that poked at
s32as if it werelong(e.g. printing with%ld), that will now warn. Use%dwiths32/int32_t, or cast.
- Increased overall test coverage from 4% to 72%
- FR_math.c: 60% coverage
- FR_math_2D.cpp: 98% coverage
- Added comprehensive test suite with multiple test files:
test_comprehensive.c- Comprehensive tests for core math functionstest_overflow_saturation.c- Edge case and overflow behavior teststest_full_coverage.c- Tests for previously uncovered functionstest_2d_complete.cpp- Complete 2D transformation matrix tests
- Fixed test failures in 2D transformation tests (XFormPtI now correctly expects integer inputs)
- Fixed FR_atan function - Was incorrectly calling FR_atan2 with wrong arguments, now properly calls FR_atanI
- Fixed constant declarations - Changed FR_PI, FR_2PI, FR_E from non-const to proper const declarations
- Fixed XFormPtI test assumptions - Tests were incorrectly passing fixed-point values instead of integers
- Added comprehensive Makefile targets:
make lib- Build static librarymake test- Run all testsmake coverage- Generate coverage reports with lcovmake examples- Build example programsmake clean- Clean build artifacts
- Added GitHub Actions CI/CD:
- Multi-platform testing (Ubuntu, macOS)
- Multi-compiler support (gcc, clang)
- Cross-compilation testing (ARM, RISC-V)
- 32-bit compatibility testing
- Automated coverage reporting to Codecov
- Overflow and saturation testing with sanitizers
- Cleaned up README.md:
- Fixed grammar and spelling throughout
- Added "Building and Testing" section with clear instructions
- Improved formatting with consistent markdown usage
- Added supported platforms list
- Better code examples with syntax highlighting
- Clearer mathematical notation and explanations
- Added CLAUDE.md - Assistant-friendly documentation with:
- Project structure overview
- Key concepts and math notation explained
- Testing and linting commands
- Common pitfalls and tips
- Added inline documentation for test files explaining coverage goals
- Removed unused/unimplemented functions:
- Removed FR_atan declaration (was declared but never implemented)
- Cleaned up test code to remove references to non-existent functions
- Fixed compiler warnings:
- Resolved deprecated C++ warnings
- Fixed format string warnings in tests
- Addressed unused variable warnings
- Improved code organization:
- Better separation of test types
- Clearer test naming conventions
- More maintainable test structure
- Verified compilation and testing on:
- x86/x64 (Linux, macOS)
- ARM (32-bit and 64-bit)
- RISC-V
- 32-bit x86 targets
- Added cross-compilation support in CI
- Updated all source file headers to version 1.0.3
- Added version section to README.md
- Initial public release
- Basic fixed-point math operations
- 2D transformation matrices
- Core trigonometric functions
- Internal development version
- Cleaned up naming conventions
- Initial test framework
Note: FR_Math has been in development since 2000, originally used in embedded systems for Palm Pilots and later ARM cores. This is the first version with comprehensive testing and CI/CD integration.