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}