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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ See also:

## [UNRELEASED](https://github.com/quartiq/idsp/compare/v0.22.0...HEAD) - DATE

### Added

* `kalman`: Scalar static Kalman filter and tooling

## [0.22.0](https://github.com/quartiq/idsp/compare/v0.21.0...v0.22.0) - 2026-06-11

### Changed
Expand Down
156 changes: 54 additions & 102 deletions dsp-fixedpoint/README.md
Original file line number Diff line number Diff line change
@@ -1,130 +1,82 @@
# Fixed point primitives
# `dsp-fixedpoint`

`dsp-fixedpoint` provides small `no_std` fixed-point primitives with explicit
integer storage and widening accumulators.
`no_std` fixed-point arithmetic with explicit storage, accumulator, and
quantization boundaries.

## Model
## Type shape

`Q<T, A, F>` stores a raw integer `T` and interprets it as scaled by `2^F`.
`Q<T, A, F>` stores raw `T` bits scaled by `2^-F`; products accumulate in `A`.

- `T` is the storage type.
- `A` is the widened accumulator type used for intermediate results.
- `F` is the number of fractional bits.
- `T`: stored and signal value
- `A`: wide accumulator
- `F`: fractional bits

Type aliases cover the common signed, unsigned, and wrapping pairs:
`Q8/Q16/Q32/Q64`, `P8/P16/P32/P64`, `W8/W16/W32/W64`, and `V8/V16/V32/V64`.
Aliases include `Q32<F> = Q<i32, i64, F>` and wrapping/unsigned variants.

## Construction

There are two construction modes:
## Wide MAC, late quantization

- `from_int`, `from_f32`, and `from_f64` scale into the fixed-point domain.
- `from_bits` is the raw-representation constructor.
Put the coefficient on the left and the raw signal on the right:

```rust
use dsp_fixedpoint::Q8;

let scaled = Q8::<4>::from_int(3);
let raw = Q8::<4>::from_bits(3 << 4);

assert_eq!(scaled, raw);
assert_eq!(raw.into_bits(), 48);
```
use dsp_fixedpoint::Q32;

## Operator semantics
type C = Q32<28>;
let a = [C::from_f32(0.25), C::from_f32(-0.5)];
let x = [100_i32, 20];

The crate keeps addition-like operators conservative and multiplication-like
operators efficient. `Q` means `Q<T, A, F>` unless shown otherwise.
let mut acc = a[0] * x[0]; // Q<i64, i32, 28>
acc += a[1] * x[1]; // still wide
let y = acc.quantize(); // one shift and narrowing conversion

| Operation | Result | Notes |
| --- | --- | --- |
| `Q<F> + Q<F>`, `Q<F> - Q<F>`, `Q<F> % Q<F>` | `Q<T, A, F>` | Requires the same `F`; use `.scale::<F1>()` explicitly when scales differ. |
| `Q * Q`, `Q / Q` | `Q<T, A, F>` | Preserves the left-hand scale and quantizes the fixed-point result. |
| `Q * T` | `Q<A, T, F>` | Widened raw-integer multiplication; same as `q.mul_wide(t)`. |
| `T * Q` | `T` | Applies `Q` as a gain to `T` and quantizes; same as `q.apply(t)`. |
| `Q / T` | `Q<T, A, F>` | Raw integer division of the stored representation. |
| `T / Q` | `T` | Divides by `Q` as a fixed-point gain and quantizes. |
| `Q *= Q`, `Q /= Q` | `Q<T, A, F>` | Assignment forms follow the left-hand scale. |
| `Q *= T`, `Q /= T` | `Q<T, A, F>` | Assignment forms operate on the raw stored representation. |

The mixed multiplication and division asymmetry is intentional: operand order
chooses whether the result remains wide or is quantized immediately. Use
`mul_wide()` and `apply()` when spelling that choice explicitly is clearer than
relying on operand order.

## Scale restrictions

`F` is an `i8`, but not every `i8` value is meaningful everywhere.

- `Q::<_, _, -128>::DELTA` is rejected at compile time.
- `Q::<_, _, F>::one()` and `Q::<_, _, F>::ONE` are rejected at compile time
when the mathematical value `1` is not exactly representable by the storage
type and scale.

```compile_fail
use dsp_fixedpoint::Q8;
use num_traits::One;

let _ = Q8::<7>::one();
assert_eq!(y, 15);
```

## Serialization

With `feature = "serde"`, `Q` serializes transparently as its raw representation.
For lossy scaled values, use `dsp_fixedpoint::serde::as_f32` or
`dsp_fixedpoint::serde::as_f64`.
The shape is `Q<T, A, F> * T -> Q<A, T, F>`. Accumulate products of that type,
then call `quantize()` once. Reversing the operands, `T * Q -> T`, quantizes each
product immediately.

```rust,ignore
# use serde::{Deserialize, Serialize};
use dsp_fixedpoint::Q32;
## Construction

#[derive(Deserialize, Serialize)]
struct Config {
#[serde(with = "dsp_fixedpoint::serde::as_f64")]
gain: Q32<3>,
}
```
`from_int` and `from_f32`/`from_f64` construct scaled values; `from_bits`
preserves raw bits. `FromRatio` constructs a coefficient from two raw values:

## Ecosystem Traits
```rust
use dsp_fixedpoint::{FromRatio, Q32};

`Q` implements a small set of generic numeric traits where the semantics stay
clear:
let gain = Q32::<28>::from_ratio(1, 3);
assert!((gain.as_f64() - 1.0 / 3.0).abs() < Q32::<28>::DELTA as f64);
```

- `Bounded` forwards to the raw storage bounds.
- `ToPrimitive`, `FromPrimitive`, and `NumCast` convert numeric values rather
than raw bits.
- `Signed` is implemented for signed `Q`/`W` families.
- `Hash` follows the raw representation.
For positive `F`, `FromRatio` widens the numerator before scaling and division.
The denominator must be nonzero and the result must fit `T`.

`Signed` inherits `One`, so the existing representability restriction still
applies: operations that need an exact `1`, such as `signum()`, are only usable
when `F >= 0`.
## Operators

## Formatting
| Operation | Result | Quantization |
| --- | --- | --- |
| `Q * T` | `Q<A, T, F>` | none; wide product |
| `T * Q` | `T` | immediate |
| `Q * Q`, `Q / Q` | `Q<T, A, F>` | immediate |
| `Q + Q`, `Q - Q` | `Q<T, A, F>` | none; equal `F` required |
| `T / Q` | `T` | immediate |

Display shows decimal notation. `Binary/Octal/UpperHex/LowerHex` show dot
notation.
`mul_wide()` and `apply()` spell `Q * T` and `T * Q` explicitly.

```rust
use dsp_fixedpoint::Q8;
## Scale and conversion

assert_eq!(
format!("{:#b}", Q8::<3>::from_bits(0b01101001)),
"0b1101.001"
);
assert_eq!(format!("{:x}", Q8::<4>::from_bits(-0x14)), "-1.4");
```
Use `.scale::<F1>()` when changing fractional bits. Numeric traits convert the
represented value; `from_bits`/`into_bits` access the representation.

## `defmt`
`Q::<_, _, -128>::DELTA` is invalid. `One` is available only when `1` is exactly
representable by `T` and `F`.

With `feature = "defmt"`, `Q` implements `defmt::Format` and logs as a decimal
value using `f32`. This keeps the target-side implementation compact for
embedded use. Exact radix-dot formatting remains available through the standard
`Binary`, `Octal`, and hex format traits.
## Optional integrations

## `bytemuck`
- `serde`: transparent raw representation; `serde::as_f32`/`as_f64` for scaled
values.
- `defmt`: compact decimal logging through `f32`.
- `bytemuck`: `Pod`, `Zeroable`, and `TransparentWrapper` when the component
types permit them.

With `feature = "bytemuck"`, `Q` derives `Pod`, `Zeroable`, and
`TransparentWrapper`. That makes zero-copy buffer casts and wrapper conversions
available when the underlying storage and accumulator types also satisfy the
corresponding `bytemuck` bounds.
`Display` is decimal. Binary, octal, and hexadecimal formatting place the radix
point according to `F`.
89 changes: 87 additions & 2 deletions dsp-fixedpoint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,54 @@ use core::{
ops::{Div, Mul, Shl, Shr},
};

/// Construct a value from the ratio of two raw values.
///
/// For positive `F`, `Q<T, A, F>` computes `(numerator << F) / denominator` in
/// `A`, then narrows to `T`. The denominator must be nonzero and the result must
/// fit `T`.
pub trait FromRatio<T> {
/// Return `numerator / denominator` in `Self`'s representation.
fn from_ratio(numerator: T, denominator: T) -> Self;

/// Divide several numerators by one denominator.
///
/// Implementations may prepare the denominator once; floating-point types
/// multiply every numerator by one reciprocal.
fn from_ratios<const N: usize>(numerators: [T; N], denominator: T) -> [Self; N]
where
T: Copy,
Self: Sized,
{
numerators.map(|numerator| Self::from_ratio(numerator, denominator))
}
}

impl FromRatio<f32> for f32 {
#[inline]
fn from_ratio(numerator: f32, denominator: f32) -> Self {
numerator / denominator
}

#[inline]
fn from_ratios<const N: usize>(numerators: [f32; N], denominator: f32) -> [Self; N] {
let reciprocal = denominator.recip();
numerators.map(|numerator| numerator * reciprocal)
}
}

impl FromRatio<f64> for f64 {
#[inline]
fn from_ratio(numerator: f64, denominator: f64) -> Self {
numerator / denominator
}

#[inline]
fn from_ratios<const N: usize>(numerators: [f64; N], denominator: f64) -> [Self; N] {
let reciprocal = denominator.recip();
numerators.map(|numerator| numerator * reciprocal)
}
}

/// Helper trait to unify over missing impl AsPrimitive<f*> for Wrapping<T>
pub(crate) trait AsFloat: Copy {
fn as_f32(self) -> f32;
Expand Down Expand Up @@ -121,7 +169,7 @@ pub trait Accu<A> {
// fn f64_as(value: f64) -> Self;
}

/// Fixed point integer
/// Fixed-point value with storage `T`, accumulator `A`, and `F` fractional bits.
///
/// Generics:
/// * `T`: Base integer
Expand Down Expand Up @@ -300,7 +348,10 @@ impl<A: Shift, T: Accu<A>, const F: i8> Q<A, T, F> {
}

impl<T: Accu<A>, A: Mul<Output = A>, const F: i8> Q<T, A, F> {
/// Multiply by a raw integer and keep the widened accumulator result.
/// Multiply a coefficient by a raw value without quantizing.
///
/// Accumulate the returned `Q<A, T, F>` values, then call [`Q::quantize`]
/// once.
///
/// ```
/// # use dsp_fixedpoint::{Q, Q8};
Expand All @@ -312,6 +363,23 @@ impl<T: Accu<A>, A: Mul<Output = A>, const F: i8> Q<T, A, F> {
}
}

impl<T, A, const F: i8> FromRatio<T> for Q<T, A, F>
where
T: Copy + Shift + Accu<A> + Div<Output = T>,
A: Shift + Div<Output = A>,
{
#[inline]
fn from_ratio(numerator: T, denominator: T) -> Self {
const { assert!(F > i8::MIN, "fractional bits must not be i8::MIN") }
let inner = if F > 0 {
T::down(numerator.up().shs(F) / denominator.up())
} else {
numerator.shs(F) / denominator
};
Self::new(inner)
}
}

impl<T: Accu<A>, A: Shift + Mul<Output = A>, const F: i8> Q<T, A, F> {
/// Apply this fixed-point value as a gain to a raw integer and quantize.
///
Expand Down Expand Up @@ -498,6 +566,23 @@ mod test {
use super::*;
use num_traits::{Bounded, FromPrimitive, Signed, ToPrimitive};

#[test]
fn ratio() {
let third = Q32::<28>::from_ratio(1i32, 3);
assert!((third.as_f64() - 1.0 / 3.0).abs() < Q32::<28>::DELTA as f64);
assert_eq!(Q32::<8>::from_ratio(-3, 2), Q32::from_bits(-384));

let reciprocal = 3.0f32.recip();
assert_eq!(
f32::from_ratios([1.0, 2.0], 3.0),
[reciprocal, 2.0 * reciprocal]
);
assert_eq!(
Q32::<8>::from_ratios([-3, 3], 2),
[Q32::from_bits(-384), Q32::from_bits(384)]
);
}

#[test]
fn simple() {
assert_eq!(
Expand Down
Loading