-
Notifications
You must be signed in to change notification settings - Fork 0
Support custom fixed point data type #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c7cb5ae
b250ad5
e4ac5b0
7978150
d886073
5f96f48
4c5dce3
e9f737b
9c8fde4
f5902ad
6c8e91e
a051f31
8546f77
aee5fd4
ef5a5c5
84d20a9
9136cbd
67b0b14
5c6d868
3c3dd54
c4a2445
99e6572
debab9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| # Fixed-Point Filters | ||
|
|
||
| constfilt can compute filter coefficients in `double` at compile time and then | ||
| convert them to a fixed-point type for deployment on targets that lack | ||
| floating-point hardware. | ||
|
|
||
| ## Quick start | ||
|
|
||
| ```cpp | ||
| #include <constfilt/constfilt.hpp> | ||
|
|
||
| // 1st-order Butterworth lowpass, coefficients stored as Q0.15 (16-bit). | ||
| static constexpr constfilt::FixedButterworth<constfilt::Q0_15, 1, constfilt::ZOH> | ||
| filt(100.0, 1000.0); | ||
|
|
||
| // Batch filtering. | ||
| constfilt::Q0_15 input[8]{}; | ||
| constfilt::Q0_15 output[8]{}; | ||
| filt(input, output); | ||
|
|
||
| // Real-time sample-by-sample filtering. | ||
| constfilt::FixedButterworth<constfilt::Q0_15, 1, constfilt::ZOH> rt(100.0, 1000.0); | ||
| constfilt::Q0_15 y = rt(constfilt::Q0_15{0.5}); | ||
| ``` | ||
|
|
||
| All coefficient math runs at compile time in `double`. The conversion to | ||
| fixed-point happens once, also at compile time. At runtime only the | ||
| Direct Form II Transposed arithmetic executes in fixed-point. | ||
|
|
||
| ## Choosing a Q format | ||
|
|
||
| A Q format defines how many fractional bits a fixed-point value carries and | ||
| therefore its representable range. For a signed N-bit type with F fractional bits | ||
| the range is `[-2^(N-1-F), 2^(N-1-F))` and the resolution is `2^(-F)`. | ||
|
|
||
| Filter coefficients can exceed `[-1, 1)` for higher-order designs, and `b[k]` | ||
| coefficients can exceed `[-1, 1)` too for highpass or elliptic designs with | ||
| gain. The Q format must be wide enough to hold the largest coefficient in | ||
| either array. Check both `coeffs_a()` and `coeffs_b()` on the `double` | ||
| prototype if you are unsure. | ||
|
|
||
| By convention `a[0] = 1.0`, but in a `Q0` format (range `[-1, 1)`) `1.0` | ||
| itself is not representable, so the converting constructor saturates it to | ||
| the format maximum instead (about `0.99997` for `Q0_15`). This is harmless | ||
| for filtering, since the Direct Form II Transposed recurrence never reads | ||
| `a[0]`, but `coeffs_a()[0]` will not read back exactly `1.0` in a `Q0` | ||
| format. | ||
|
|
||
| ### Built-in aliases | ||
|
|
||
| | Alias | Storage | FracBits | Range | Resolution | | ||
| |---------|---------------|----------|------------|-------------| | ||
| | `Q0_7` | `signed char` | 7 | `[-1, 1)` | ~7.8e-3 | | ||
| | `Q1_6` | `signed char` | 6 | `[-2, 2)` | ~1.6e-2 | | ||
| | `Q0_15` | `short` | 15 | `[-1, 1)` | ~3.1e-5 | | ||
| | `Q1_14` | `short` | 14 | `[-2, 2)` | ~6.1e-5 | | ||
| | `Q0_31` | `int` | 31 | `[-1, 1)` | ~4.7e-10 | | ||
| | `Q1_30` | `int` | 30 | `[-2, 2)` | ~9.3e-10 | | ||
|
|
||
| All aliases are in the `constfilt` namespace and are defined in | ||
| `include/constfilt/fixed_point_aliases.hpp`, which is included by the umbrella | ||
| header. | ||
|
|
||
| ### Rule of thumb | ||
|
|
||
| Use a `Q0` variant (range `[-1, 1)`) for 1st-order filters and verify the | ||
| coefficients fit. Use a `Q1` variant (range `[-2, 2)`) when denominator | ||
| coefficients exceed `[-1, 1)`, which is common for 2nd-order and higher designs. | ||
| Higher precision (`Q0_31` / `Q1_30`) reduces quantization error at the cost of | ||
| wider arithmetic. | ||
|
|
||
| ## FixedButterworth and FixedElliptic | ||
|
|
||
| These are the primary user-facing types. They mirror the constructor signatures | ||
| of `Butterworth` and `Elliptic` exactly, with the floating-point scalar type | ||
| replaced by the fixed-point type. | ||
|
|
||
| ```cpp | ||
| // FixedButterworth<TFixed, N, Method, FilterType> | ||
| static constexpr constfilt::FixedButterworth<constfilt::Q1_30, 2, constfilt::ZOH> | ||
| bw(100.0, 1000.0); | ||
|
|
||
| // FixedElliptic<TFixed, N, Method, FilterType> | ||
| static constexpr constfilt::FixedElliptic<constfilt::Q1_30, 2, constfilt::ZOH> | ||
| el(100.0, 0.5, 60.0, 1000.0); | ||
| ``` | ||
|
|
||
| `Method` and `FilterType` default to `TustinPW` and `LowPass` respectively, | ||
| matching the floating-point counterparts. | ||
|
|
||
| ## FixedFilter (advanced) | ||
|
|
||
| `FixedFilter` is the lower-level building block. It converts any | ||
| `Filter<double, NB, NA>` to a fixed-point filter by taking the double filter as | ||
| a constructor argument. Use it when you need to convert an `AnalogFilter`, or when you prefer to | ||
| manage the double prototype explicitly. | ||
|
|
||
| ```cpp | ||
| static constexpr constfilt::Butterworth<double, 1, constfilt::ZOH> proto( | ||
| 100.0, 1000.0); | ||
| static constexpr constfilt::FixedFilter<constfilt::Q0_15, 2, 2> filt(proto); | ||
| ``` | ||
|
|
||
| The `NB` and `NA` template arguments must match the source filter. For a | ||
| Butterworth or Elliptic of order N both are `N + 1`. | ||
|
|
||
| ## Custom fixed-point types | ||
|
|
||
| constfilt supplies `FixedPoint<TInt, TWider, FracBits>` as a ready-to-use | ||
| scalar, but `FixedFilter`, `FixedButterworth`, and `FixedElliptic` accept any | ||
| type `T` that provides the arithmetic operators used by `Filter` | ||
|
|
||
| ``` | ||
| T operator+(T) const | ||
| T operator-(T) const | ||
| T operator-() const | ||
| T operator*(T) const | ||
| T() // default constructor produces zero | ||
| explicit T(double) // conversion from double coefficient | ||
| ``` | ||
|
|
||
| `T` must also support value-initialization (`T{}` produces zero) and | ||
| `static_cast<T>(double_value)` must be valid. `T` must also be a literal | ||
| type and copy assignable, both of which the `constexpr` batch path relies | ||
| on through `T _b[NB]{}` and the assignments inside `Filter::operator()`. | ||
|
|
||
| ## FixedPoint reference | ||
|
|
||
| ```cpp | ||
| template <typename TInt, typename TWider, unsigned FracBits> | ||
| class constfilt::FixedPoint; | ||
| ``` | ||
|
|
||
| **Template parameters** | ||
|
|
||
| | Parameter | Description | | ||
| |------------|----------------------------------------------------------------| | ||
| | `TInt` | Signed integer storage type (`signed char`, `short`, `int`, `long`, `long long`) | | ||
| | `TWider` | Signed integer strictly wider than `TInt`, used as multiply intermediate | | ||
| | `FracBits` | Number of fractional bits; must satisfy `0 < FracBits < 8*sizeof(TInt)` | | ||
|
|
||
| **Construction** | ||
|
|
||
| | Expression | Result | | ||
| |-------------------------|--------------------------------------------------| | ||
| | `FixedPoint{}` | Zero | | ||
| | `FixedPoint{double v}` | Nearest representable value; saturates at limits | | ||
| | `FixedPoint{TInt n}` | Integer `n` scaled by `2^FracBits` | | ||
| | `FixedPoint::from_raw(TInt r)` | Raw bit pattern `r` without scaling | | ||
|
|
||
| An integer literal such as `Q1_14{1}` fails to compile: it converts equally | ||
| well to `TInt` (the integer constructor) and to `double` (the double | ||
| constructor), so the call is ambiguous. Cast the literal to the storage type | ||
| or to `double` to disambiguate, for example `Q1_14{static_cast<short>(1)}` or | ||
| `Q1_14{1.0}` (both produce raw value `16384`, the representation of `1.0` in | ||
| `Q1_14`'s `[-2, 2)` range). | ||
|
|
||
| The two forms are not interchangeable in general, and picking the wrong one | ||
| matters most near a format's limits. The integer constructor scales its | ||
| argument by `2^FracBits` and wraps on overflow; the double constructor | ||
| rounds to the nearest representable value and saturates at the format's | ||
| limits. In a `Q0` format (range `[-1, 1)`) the value `1` is not representable | ||
| at all, so `Q0_15{static_cast<short>(1)}` wraps to a negative raw value while | ||
| `Q0_15{1.0}` saturates to just under the maximum. Pick the form that matches | ||
| what the value actually means. | ||
|
Comment on lines
+151
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 'integer_val|static_cast<TInt>.*SCALE' include/constfilt/fixed_point.hppRepository: MitchellThompkins/constfilt Length of output: 1487 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- fixed_point.hpp relevant implementation ---'
sed -n '80,145p' include/constfilt/fixed_point.hpp
printf '%s\n' '--- project standard and target assumptions ---'
rg -n -C 3 'CXX_STANDARD|cxx_std_17|std=c\+\+17|CMAKE_CXX|two.?complement|wrap|integer constructor|Wraps silently' \
CMakeLists.txt cmake include test tests docs 2>/dev/null || true
printf '%s\n' '--- uses and tests for the integer constructor ---'
rg -n -C 5 'FixedPoint<|Q0_15|Q1_14|raw_value\(\)|wrap|overflow' \
test tests include docs 2>/dev/null || trueRepository: MitchellThompkins/constfilt Length of output: 50385 🌐 Web query:
💡 Result: Under the C++17 standard, specifically section [conv.integral], when an integer value is converted to a signed integer type and the value cannot be represented in the destination type, the resulting value is implementation-defined [1][2]. Unlike the C standard (specifically C11, section 6.3.1.3), which explicitly states that such a conversion results in either an implementation-defined value or an implementation-defined signal [3], the C++17 standard does not mention signals [1][2]. It simply specifies that the result is implementation-defined [1][2]. In practice, virtually all modern C++ implementations define this behavior as a silent wraparound (e.g., using two's complement representation) [4][5], though the standard requires that the specific behavior be documented by the implementation [6]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- focused integer-construction tests ---'
sed -n '24,72p' tests/fixed_point.test.cpp
rg -n -C 4 'FromInteger|static_cast<short>|static_cast<signed char>|integer_val|Q0_15.*1|Q1_14.*1' \
tests/fixed_point.test.cpp docs/fixed-point.md include/constfilt/fixed_point.hpp
printf '%s\n' '--- supported target documentation ---'
rg -n -C 3 'ARM|target|platform|two.?complement|signed integer|supported' \
README.md docs CMakeLists.txt cmake/toolchains include/constfilt/fixed_point.hpp 2>/dev/null | head -n 180Repository: MitchellThompkins/constfilt Length of output: 19046 Define signed wrapping for the integer constructor. C++17 makes an out-of-range conversion to 🤖 Prompt for AI AgentsSources: Coding guidelines, Path instructions |
||
|
|
||
| **Members** | ||
|
|
||
| | Member | Description | | ||
| |-------------------|---------------------------------------------| | ||
| | `raw_value()` | Returns the raw integer representation | | ||
| | `to_double()` | Converts back to `double` | | ||
| | `from_raw(TInt)` | Constructs from raw bits (static) | | ||
|
|
||
| Arithmetic follows Q-format rules. Multiplication widens to `TWider`, adds a | ||
| half LSB bias, and shifts right by `FracBits` to renormalize. This rounds | ||
| half up, ties toward positive infinity, which is not the same tie rule as | ||
| the double constructor's ties away from zero. Division widens and shifts | ||
| left before dividing, then truncates toward zero rather than rounding, so | ||
| `operator/` and `operator*` do not agree on rounding. `Filter` never | ||
| divides, so this asymmetry only matters if `FixedPoint` is used as a | ||
| general purpose scalar outside `Filter`. Addition, subtraction, unary | ||
| negation, and multiplication saturate at the format's limits on overflow, | ||
| matching CMSIS DSP and TI IQmath convention. Wrapping inside an IIR | ||
| feedback loop flips the sign of the accumulated value and can drive | ||
| sustained large amplitude oscillation rather than clipping gracefully, | ||
| which is why saturation is preferred. The integer constructor is the one | ||
| place that still wraps on overflow, see above. Division by zero is | ||
| undefined behavior. | ||
|
|
||
| `fixed_limits<TInt, TWider, FracBits>` provides `min()`, `max()`, `lowest()`, | ||
| and `epsilon()` without requiring `<limits>`. This follows the integer | ||
| convention rather than the floating point one `std::numeric_limits` uses. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Use Change “floating point one” to “floating-point one.” 🧰 Tools🪛 LanguageTool[grammar] ~193-~193: Use a hyphen to join words. (QB_NEW_EN_HYPHEN) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| `min()` returns the most negative representable value, not the smallest | ||
| positive one, and `lowest()` is identical to `min()` since `FixedPoint` has | ||
| no separate notion of a smallest finite value. `max()` returns the most | ||
| positive representable value, and `epsilon()` returns one LSB, the smallest | ||
| representable step, `2^(-FracBits)`. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Document the actual prototype type accepted by
FixedFilter.The constructor accepts
Filter<U, NB, NA>for a templatedU; it is not limited toFilter<double, NB, NA>. UseFilter<U, NB, NA>or “a floating-pointFilter” in this section.🤖 Prompt for AI Agents