peakrdl_rust/
fixedpoint.rs

1//! Types for numeric fixed-point field representations
2
3use num_traits::{AsPrimitive, Float};
4
5use crate::reg::RegInt;
6
7/// A fixed-point number implementation using an underlying primitive integer type.
8///
9/// # Type Parameters
10///
11/// * `P` - The primitive integer type (signed or unsigned) used to store the fixed-point value
12/// * `I` - The number of integer bits (can be negative for sub-integer representations)
13/// * `F` - The number of fractional bits (can be negative for super-integer representations)
14///
15/// # Examples
16///
17/// Basic usage with different bit configurations:
18///
19/// ```
20/// # use peakrdl_rust::fixedpoint::FixedPoint;
21/// // 8-bit unsigned with 4 integer and 4 fractional bits
22/// let fp = FixedPoint::<u8, 4, 4>::from_f64(2.25);
23/// assert_eq!(fp.to_f64(), 2.25);
24///
25/// // 16-bit signed with 8 integer and 4 fractional bits
26/// let fp = FixedPoint::<i16, 8, 4>::from_f64(-1.5);
27/// assert_eq!(fp.to_bits(), -24);
28/// ```
29///
30/// The total width is calculated as I + F:
31///
32/// ```
33/// # use peakrdl_rust::fixedpoint::FixedPoint;
34/// assert_eq!(FixedPoint::<u8, 8, 0>::width(), 8);
35/// assert_eq!(FixedPoint::<i8, 7, -3>::width(), 4);
36/// ```
37#[derive(Clone, Copy, PartialEq, Eq)]
38pub struct FixedPoint<P, const I: isize, const F: isize> {
39    val: P,
40}
41
42impl<P, const I: isize, const F: isize> core::fmt::Debug for FixedPoint<P, I, F>
43where
44    P: RegInt + AsPrimitive<f64>,
45{
46    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47        use core::fmt::Write as _;
48        let mut name: heapless::String<32> = heapless::String::new();
49        write!(&mut name, "FixedPoint<{I},{F}>")
50            .expect("Fixedpoint type name should fit in small buffer");
51        f.debug_struct(&name)
52            .field("int", &self.val)
53            .field("real", &self.to_f64())
54            .finish()
55    }
56}
57
58impl<P, const I: isize, const F: isize> FixedPoint<P, I, F>
59where
60    P: RegInt,
61{
62    /// Creates a fixed-point number from its raw bit representation.
63    ///
64    /// # Panics
65    ///
66    /// - (At compile time) If the primitive type P is not wide enough for the specified I + F bit width
67    /// - (At compile time) If I + F is not positive
68    /// - If the provided bits would overflow the fixed-point representation
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// # use peakrdl_rust::fixedpoint::FixedPoint;
74    /// let fp = FixedPoint::<u8, 4, 4>::from_bits(16); // represents 1.0
75    /// assert_eq!(fp.to_f64(), 1.0);
76    /// ```
77    ///
78    /// The following should not compile:
79    ///
80    /// ```compile_fail
81    /// # use peakrdl_rust::fixedpoint::FixedPoint;
82    /// FixedPoint::<u8, 5, 4>::from_bits(0); // u8 not large enough
83    /// ```
84    ///
85    /// ```compile_fail
86    /// # use peakrdl_rust::fixedpoint::FixedPoint;
87    /// FixedPoint::<i8, -5, 4>::from_bits(0); // invalid negative width
88    /// ```
89    #[must_use]
90    pub fn from_bits(bits: P) -> Self {
91        const {
92            assert!(
93                I + F <= 8 * core::mem::size_of::<P>().cast_signed(),
94                "The primitive integer type is not wide enough for this fixed-point representation"
95            );
96            assert!(I + F > 0, "The fixed-point bit width must be positive");
97        }
98        assert!(
99            (bits <= Self::max_bits()) && (bits >= Self::min_bits()),
100            "The provided bits overflow this fixed-point representation"
101        );
102        Self { val: bits }
103    }
104
105    /// Returns the raw bit representation of the fixed-point number.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// # use peakrdl_rust::fixedpoint::FixedPoint;
111    /// let fp = FixedPoint::<u16, 8, 2>::from_f64(2.25);
112    /// assert_eq!(fp.to_bits(), 9);
113    /// ```
114    #[must_use]
115    pub const fn to_bits(self) -> P {
116        self.val
117    }
118
119    /// Returns the number of integer bits.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// # use peakrdl_rust::fixedpoint::FixedPoint;
125    /// assert_eq!(FixedPoint::<u8, 10, -4>::intwidth(), 10);
126    /// ```
127    #[must_use]
128    pub const fn intwidth() -> isize {
129        I
130    }
131
132    /// Returns the number of fractional bits.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// # use peakrdl_rust::fixedpoint::FixedPoint;
138    /// assert_eq!(FixedPoint::<u8, 10, -4>::fracwidth(), -4);
139    /// ```
140    #[must_use]
141    pub const fn fracwidth() -> isize {
142        F
143    }
144
145    /// Returns the total bit width (I + F) of the fixed-point representation.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// # use peakrdl_rust::fixedpoint::FixedPoint;
151    /// assert_eq!(FixedPoint::<u8, 8, 0>::width(), 8);
152    /// assert_eq!(FixedPoint::<i8, 7, -3>::width(), 4);
153    /// ```
154    #[must_use]
155    pub const fn width() -> usize {
156        (I + F).cast_unsigned()
157    }
158
159    /// Returns true if the fixedpoint representation (underlying primitive type) is signed.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// # use peakrdl_rust::fixedpoint::FixedPoint;
165    /// assert_eq!(FixedPoint::<u16, 8, 2>::is_signed(), false);
166    /// assert_eq!(FixedPoint::<i16, 8, 2>::is_signed(), true);
167    /// ```
168    #[must_use]
169    pub fn is_signed() -> bool {
170        P::min_value() < P::zero()
171    }
172
173    /// Returns a fixed-point representation of zero.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// # use peakrdl_rust::fixedpoint::FixedPoint;
179    /// let zero = FixedPoint::<u8, 4, 4>::zero();
180    /// assert_eq!(zero.to_f64(), 0.0);
181    /// ```
182    #[must_use]
183    pub fn zero() -> Self {
184        Self::from_bits(P::zero())
185    }
186
187    #[must_use]
188    fn max_bits() -> P {
189        let unused_bits = core::mem::size_of::<P>() * 8 - Self::width();
190        P::max_value().shr(unused_bits)
191    }
192
193    #[must_use]
194    fn min_bits() -> P {
195        let unused_bits = core::mem::size_of::<P>() * 8 - Self::width();
196        P::min_value().shr(unused_bits)
197    }
198
199    /// Returns the maximum representable value for this fixed-point type.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// # use peakrdl_rust::fixedpoint::FixedPoint;
205    /// assert_eq!(FixedPoint::<u8, 2, 6>::max_value().to_f32(), 3.984375);
206    /// assert_eq!(FixedPoint::<i8, 3, 4>::max_value().to_f32(), 3.9375);
207    /// ```
208    #[must_use]
209    pub fn max_value() -> Self {
210        Self::from_bits(Self::max_bits())
211    }
212
213    /// Returns the minimum representable value for this fixed-point type.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// # use peakrdl_rust::fixedpoint::FixedPoint;
219    /// assert_eq!(FixedPoint::<u8, 2, 6>::min_value().to_f32(), 0.0);
220    /// assert_eq!(FixedPoint::<i8, 3, 4>::min_value().to_f32(), -4.0);
221    /// ```
222    #[must_use]
223    pub fn min_value() -> Self {
224        Self::from_bits(Self::min_bits())
225    }
226
227    /// Returns the smallest representable positive value (the resolution).
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// # use peakrdl_rust::fixedpoint::FixedPoint;
233    /// let res = FixedPoint::<u8, 4, 4>::resolution();
234    /// assert_eq!(res.to_f64(), 0.0625); // 2^(-4)
235    /// ```
236    #[must_use]
237    pub fn resolution() -> Self {
238        Self::from_bits(P::one())
239    }
240
241    /// Quantizes a floating-point value to the resolution of this fixed-point type
242    /// and returns it as a floating-point value.
243    ///
244    /// This is equivalent to converting to fixed-point and back to floating-point.
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// # use peakrdl_rust::fixedpoint::FixedPoint;
250    /// // 2.3 gets quantized to the nearest representable value
251    /// let quantized = FixedPoint::<u8, 4, 4>::quantize(2.3);
252    /// assert_eq!(quantized, 2.3125);
253    /// ```
254    pub fn quantize<T>(value: T) -> T
255    where
256        T: Float + 'static,
257        P: AsPrimitive<T>,
258    {
259        Self::from_float(value).to_float()
260    }
261
262    /// Creates a fixed-point number from a 32-bit floating-point value.
263    ///
264    /// Values are rounded to the nearest representable fixed-point value.
265    /// Ties are rounded away from 0.
266    /// Out-of-range values are saturated to the min/max representable values.
267    ///
268    /// # Panics
269    ///
270    /// Panics if the input is NaN.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # use peakrdl_rust::fixedpoint::FixedPoint;
276    /// let fp = FixedPoint::<u8, 4, 4>::from_f32(1.5);
277    /// assert_eq!(fp.to_bits(), 24);
278    ///
279    /// // saturation behavior
280    /// let min_fp = FixedPoint::<i8, 4, 4>::from_f64(-100.0);
281    /// assert_eq!(min_fp, FixedPoint::<i8, 4, 4>::min_value());
282    /// ```
283    #[must_use]
284    pub fn from_f32(value: f32) -> Self
285    where
286        P: AsPrimitive<f32>,
287    {
288        Self::from_float(value)
289    }
290
291    /// Creates a fixed-point number from a 64-bit floating-point value.
292    ///
293    /// Values are rounded to the nearest representable fixed-point value.
294    /// Ties are rounded away from 0.
295    /// Out-of-range values are saturated to the min/max representable values.
296    ///
297    /// # Panics
298    ///
299    /// Panics if the input is NaN.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// # use peakrdl_rust::fixedpoint::FixedPoint;
305    /// let fp = FixedPoint::<u16, 8, 2>::from_f64(2.25);
306    /// assert_eq!(fp.to_bits(), 9);
307    ///
308    /// // Saturation behavior
309    /// let max_fp = FixedPoint::<u8, 4, 4>::from_f64(100.0);
310    /// assert_eq!(max_fp, FixedPoint::<u8, 4, 4>::max_value());
311    /// ```
312    #[must_use]
313    pub fn from_f64(value: f64) -> Self
314    where
315        P: AsPrimitive<f64>,
316    {
317        Self::from_float(value)
318    }
319
320    #[must_use]
321    fn from_float<T>(value: T) -> Self
322    where
323        T: Float + 'static,
324        P: AsPrimitive<T>,
325    {
326        assert!(!value.is_nan(), "Can't convert NaN to FixedPoint");
327
328        // scale
329        #[allow(clippy::cast_possible_truncation)]
330        let scale = T::from(2)
331            .expect("two can be represented by any float type")
332            .powi(F as i32);
333        let scaled_value = value * scale;
334
335        // saturate
336        if scaled_value >= P::max_value().as_() {
337            Self::from_bits(P::max_value())
338        } else if scaled_value <= P::min_value().as_() {
339            Self::from_bits(P::min_value())
340        } else {
341            // round
342            Self::from_bits(
343                P::from(scaled_value.round()).expect("shouldn't be NaN or out of range"),
344            )
345        }
346    }
347
348    /// Converts the fixed-point number to a 32-bit floating-point value.
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// # use peakrdl_rust::fixedpoint::FixedPoint;
354    /// assert_eq!(FixedPoint::<u16, 8, 2>::from_bits(8).to_f32(), 2.0);
355    /// assert_eq!(FixedPoint::<u16, 8, 2>::from_bits(9).to_f32(), 2.25);
356    /// assert_eq!(FixedPoint::<i16, 8, 4>::from_bits(-24).to_f32(), -1.5);
357    /// assert_eq!(FixedPoint::<u8, 4, 4>::from_bits(1).to_f32(), 0.0625);
358    /// assert_eq!(FixedPoint::<i8, 4, 4>::from_bits(-1).to_f32(), -0.0625);
359    /// assert_eq!(FixedPoint::<u8, 4, 4>::from_bits(0).to_f32(), 0.0);
360    /// ```
361    #[must_use]
362    pub fn to_f32(self) -> f32
363    where
364        P: AsPrimitive<f32>,
365    {
366        self.to_float()
367    }
368
369    /// Converts the fixed-point number to a 64-bit floating-point value.
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// # use peakrdl_rust::fixedpoint::FixedPoint;
375    /// assert_eq!(FixedPoint::<u16, 8, 2>::from_bits(8).to_f64(), 2.0);
376    /// assert_eq!(FixedPoint::<u16, 8, 2>::from_bits(9).to_f64(), 2.25);
377    /// assert_eq!(FixedPoint::<i16, 8, 4>::from_bits(-24).to_f64(), -1.5);
378    /// assert_eq!(FixedPoint::<u8, 4, 4>::from_bits(1).to_f64(), 0.0625);
379    /// assert_eq!(FixedPoint::<i8, 4, 4>::from_bits(-1).to_f64(), -0.0625);
380    /// assert_eq!(FixedPoint::<u8, 4, 4>::from_bits(0).to_f64(), 0.0);
381    /// ```
382    #[must_use]
383    pub fn to_f64(self) -> f64
384    where
385        P: AsPrimitive<f64>,
386    {
387        self.to_float()
388    }
389
390    #[must_use]
391    fn to_float<T>(self) -> T
392    where
393        T: Float + 'static,
394        P: AsPrimitive<T>,
395    {
396        #[allow(clippy::cast_possible_truncation)]
397        let scale = T::from(2)
398            .expect("two can be represented by any float type")
399            .powi(-F as i32);
400        self.val.as_() * scale
401    }
402}
403
404/// Automatic conversion from floating-point types to fixed-point.
405///
406/// This provides convenient syntax for creating fixed-point numbers from floats.
407/// Note that this is a lossy conversion that will never fail (unless NaN). Saturation
408/// and rounding are applied.
409///
410/// # Panics
411///
412/// Panics if the value is NaN.
413///
414/// # Examples
415///
416/// ```
417/// # use peakrdl_rust::fixedpoint::FixedPoint;
418/// let fp: FixedPoint<u8, 4, 4> = 2.5.into();
419/// assert_eq!(fp.to_f64(), 2.5);
420/// ```
421impl<T, P, const I: isize, const F: isize> From<T> for FixedPoint<P, I, F>
422where
423    T: Float + 'static,
424    P: RegInt + AsPrimitive<T>,
425{
426    fn from(value: T) -> Self {
427        Self::from_float(value)
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_from_float() {
437        assert_eq!(FixedPoint::<u16, 8, 2>::from_float(2.25).to_bits(), 9);
438        assert_eq!(FixedPoint::<i16, 8, 4>::from_float(-1.5).to_bits(), -24);
439        assert_eq!(FixedPoint::<u8, 4, 4>::from_float(0.0625).to_bits(), 1);
440
441        // Test rounding
442        assert_eq!(FixedPoint::<u8, 4, 4>::from_float(0.03124).to_bits(), 0); // rounds down
443        assert_eq!(FixedPoint::<u8, 4, 4>::from_float(0.03125).to_bits(), 1); // rounds ties away from 0
444        assert_eq!(FixedPoint::<u8, 4, 4>::from_float(0.03126).to_bits(), 1); // rounds up
445        assert_eq!(FixedPoint::<i8, 4, 4>::from_float(-0.03124).to_bits(), 0); // rounds up
446        assert_eq!(FixedPoint::<i8, 4, 4>::from_float(-0.03125).to_bits(), -1); // rounds ties away from 0
447        assert_eq!(FixedPoint::<i8, 4, 4>::from_float(-0.03126).to_bits(), -1); // rounds down
448
449        // Test saturation - positive overflow
450        assert_eq!(
451            FixedPoint::<u8, 4, 4>::from_float(100.0),
452            FixedPoint::<u8, 4, 4>::max_value()
453        );
454        assert_eq!(
455            FixedPoint::<i8, 4, 4>::from_float(100.0),
456            FixedPoint::<i8, 4, 4>::max_value()
457        );
458
459        // Test saturation - negative overflow
460        assert_eq!(
461            FixedPoint::<u8, 4, 4>::from_float(-100.0),
462            FixedPoint::<u8, 4, 4>::min_value()
463        );
464        assert_eq!(
465            FixedPoint::<i8, 4, 4>::from_float(-100.0),
466            FixedPoint::<i8, 4, 4>::min_value()
467        );
468    }
469
470    #[test]
471    #[should_panic(expected = "Can't convert NaN to FixedPoint")]
472    fn test_from_float_nan_panic() {
473        let _ = FixedPoint::<u8, 4, 4>::from_float(f64::NAN);
474    }
475
476    #[test]
477    #[should_panic(expected = "The provided bits overflow this fixed-point representation")]
478    fn test_positive_overflow1() {
479        let _ = FixedPoint::<u8, 2, 4>::from_bits(64);
480    }
481
482    #[test]
483    #[should_panic(expected = "The provided bits overflow this fixed-point representation")]
484    fn test_negative_overflow() {
485        let _ = FixedPoint::<i8, 2, 4>::from_bits(-33);
486    }
487}