A fixed-point representation keeps the fractional position in one place. Instead of moving it to accommodate different ranges, it stores an integer with an implicit denominator. This can save space when values stay within a known range, at the cost of accuracy. It is also useful for calculations on CPUs without a floating-point unit, such as microcontrollers. section 5.5 section 5.8
Q7 is a one-byte example for values near −1 through 1. Its seven value bits and one sign bit fit in a Rust i8. The stored integer represents a fraction with an implicit denominator of 128: for example, a stored value of 64 represents 64/128, or 0.5. section 5.5
A small Q7 type needs three practical pieces:
- Wrap an i8 in a tuple struct so callers work with Q7 rather than its stored integer; derive traits for debugging, copying, and equality comparisons. section 5.5
- Convert from f64 by multiplying in-range values by 128 and casting to i8. Clamp values at or above 1 to 127, and values at or below −1 to −128. section 5.5
- Convert back by dividing the stored value by 128. Handle f32 through the f64 conversions rather than duplicating the scaling code. section 5.5
What happens at the endpoints?
Q7 can store −1 exactly as −128/128. At the positive end, 1 is clamped to 127/128, not stored exactly. Values such as 10 and −10 are clamped to the same respective endpoints; tests check this behavior. section 5.5
Listing 5.12 Converting from f64 to Q7
impl From<f64> for Q7 {
fn from (n: f64) -> Self {
// assert!(n >= -1.0);
// assert!(n <= 1.0);
if n >= 1.0 { #1
Q7(127)
} else if n <= -1.0 { #1
Q7(-128)
} else {
Q7((n * 128.0) as i8)
}
}
}
impl From<Q7> for f64 {
fn from(n: Q7) -> f64 {
(n.0 as f64) * 2_f64.powf(-7.0) #2
}
}
The conversion listing puts the implicit denominator into working code: multiply when storing, scale back when reading, and handle both bounds explicitly. section 5.5
