Search code examples
cfixed-point

Notation for fixed point representation


I'm looking for a commonly understandable notation to define a fixed point number representation. The notation should be able to define both a power-of-two factor (using fractional bits) and a generic factor (sometimes I'm forced to use this, though less efficient). And also an optional offset should be defined.
I already know some possible notations, but all of them seem to be constrained to specific applications.

  • For example the Simulink notation would perfectly fit my needs, but it's known only in the Simulink world. Furthermore the overloaded usage of the fixdt() function is not so readable.

  • TI defines a really compact Q Formats, but the sign is implicit, and it doesn't manage a generic factor (i.e. not a power-of-two).

  • ASAM uses a generic 6-coefficient rational function with 2nd-degree numerator and denominator polynomials (COMPU_METHOD). Very generic, but not so friendly.

See also the Wikipedia discussion.

The question is only about the notation (not efficiency of the representation nor fixed-point manipulation). So it's a matter of code readability, maintenability and testability.


Solution

  • Ah, yes. Having good naming annotations is absolutely critical to not introducing bugs with fixed point arithmetic. I use an explicit version of the Q notation which handles any division between M and N by appending _Q<M>_<N> to the name of the variable. This also makes it possible to include the signedness as well. There are no run-time performance penalties for this. Example:

    uint8_t length_Q2_6;                // unsigned, 2 bit integer, 6 bit fraction
    int32_t sensor_calibration_Q10_21;  // signed (1 bit), 10 bit integer, 21 bit fraction.
    
    /*
     * Calculations with the bc program (with '-l' argument):
     *
     * sqrt(3)
     * 1.73205080756887729352
     *
     * obase=16
     * sqrt(3)
     * 1.BB67AE8584CAA73B0
     */
    const uint32_t SQRT_3_Q7_25 = 1 << 25 | 0xBB67AE85U >> 7; /* Unsigned shift super important here! */
    

    In case someone have not fully understood why such annotation is extremely important, Can you spot the if there is an bug in the following two examples?

    Example 1:

    speed_fraction = fix32_udiv(25, speed_percent << 25, 100 << 25);
    squared_speed  = fix32_umul(25, speed_fraction, speed_fraction);
    tmp1 = fix32_umul(25, squared_speed, SQRT_3);
    tmp2 = fix32_umul(12, tmp1 >> (25-12), motor_volt << 12);
    

    Example 2:

    speed_fraction_Q7_25 = fix32_udiv(25, speed_percent << 25, 100 << 25);
    squared_speed_Q7_25  = fix32_umul(25, speed_fraction_Q7_25, speed_fraction_Q7_25);
    tmp1_Q7_25  = fix32_umul(25, squared_speed_Q7_25, SQRT_3_Q1_31);
    tmp2_Q20_12 = fix32_umul(12, tmp1_Q7_25 >> (25-12), motor_volt << 12);
    

    Imagine if one file contained #define SQRT_3 (1 << 25 | 0xBB67AE85U >> 7) and another file contained #define SQRT_3 (1 << 31 | 0xBB67AE85U >> 1) and code was moved between those files. For example 1 this has a high chance of going unnoticed and introduce the bug that is present in example 2 which here is done deliberately and has a zero chance of being done accidentally.