liveBook · Manning

Fixed-point representation

As explained in Rust in Action: Systems programming concepts and techniques

A fixed-point number format represents fractional values with a fixed position for the decimal point and a predetermined allocation of bits.

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

How Rust in Action: Systems programming concepts and techniques covers it

How deep it goes
Works through a one-byte Q7 type, scaling, endpoint clamping, conversions, and tests; does not develop fixed-point arithmetic operations.
Examples
Implements Q7 as a Rust i8 wrapper, converts f64 and f32 values, and tests out-of-bounds inputs.
What it assumes
Introduces tuple structs and relevant derived traits while using Rust integers, floating-point types, and trait implementations.
What you can do after
Build it from scratch as a Q7 type with bounded floating-point conversions and tests.
How it connects
Connects fixed point to quantization, floating-point formats, integer storage, Rust conversion traits, and microcontrollers.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Rust in Action: Systems programming concepts and techniques. The section links point to the parts of the book it draws on.