Fixed-Point Arithmetic Implementation in C

Fixed-point arithmetic provides a computationally efficient and deterministic method for handling non-integer values on target processors lacking a dedicated Floating-Point Unit (FPU). In resource-constrained embedded systems, relying on compiler-driven software floating-point emulation introduces substantial code bloat and cycle overhead. This article explains how to represent fractional numbers using standard C integer types and implements the fundamental arithmetic operations, scaling techniques, and overflow management required for a robust fixed-point math system.

Number Representation and the Q Format

Fixed-point arithmetic simulates fractional numbers by scaling an integer by an implicit fixed factor, typically a power of two (\(2^n\)). This allows the underlying hardware to manipulate numbers using native integer instructions while conceptually placing a fractional "radix point" within the register.

The most widely used notation is the Q format:

  • Qm.n: Indicates an integer where \(m\) bits represent the integer component and \(n\) bits represent the fractional component.
  • A signed 32-bit integer configured as Q16.16 uses 1 sign bit, 15 magnitude bits for the whole number part, and 16 fractional bits.
  • The real-world value is calculated as: \(\text{Real Value} = \frac{\text{Integer Value}}{2^n}\).

In C99 and later, fixed-point types are defined using fixed-width integers from <stdint.h>:

#include <stdint.h>

#define FIXED_SHIFT 16
#define FIXED_SCALE (1 << FIXED_SHIFT)

typedef int32_t fixed_t;

Conversions

Converting between native integers, floating-point literals, and fixed-point types requires shifting or scaling by the fractional factor:

// Convert integer to fixed-point
#define INT_TO_FIXED(x) ((fixed_t)((x) << FIXED_SHIFT))

// Convert fixed-point to integer (truncating toward zero)
#define FIXED_TO_INT(x) ((int32_t)((x) >> FIXED_SHIFT))

// Convert floating-point literal at compile-time/initialization
#define FLOAT_TO_FIXED(x) ((fixed_t)((x) * FIXED_SCALE + ((x) >= 0 ? 0.5f : -0.5f)))

// Convert fixed-point back to float (debugging/logging only)
#define FIXED_TO_FLOAT(x) ((float)(x) / (float)FIXED_SCALE)

Addition and Subtraction

When two operands share the same fractional scaling (\(n\)), their addition and subtraction execute as standard integer operations. The position of the radix point remains unchanged:

fixed_t fixed_add(fixed_t a, fixed_t b) {
    return a + b;
}

fixed_t fixed_sub(fixed_t a, fixed_t b) {
    return a - b;
}

Both operations take a single clock cycle on most architectures.

Multiplication

Multiplying two \(Q16.16\) numbers produces a result with \(16 + 16 = 32\) fractional bits (\(Q32.32\)), which requires a 64-bit storage container to prevent truncation and overflow. To restore the result to the target \(Q16.16\) format, the 64-bit intermediate value must be right-shifted by 16 bits:

fixed_t fixed_mul(fixed_t a, fixed_t b) {
    int64_t product = (int64_t)a * (int64_t)b;
    return (fixed_t)(product >> FIXED_SHIFT);
}

To incorporate nearest-neighbor rounding instead of truncation toward negative infinity, add half of the fractional base (\(2^{n-1}\)) prior to the shift:

fixed_t fixed_mul_round(fixed_t a, fixed_t b) {
    int64_t product = (int64_t)a * (int64_t)b;
    return (fixed_t)((product + (1 << (FIXED_SHIFT - 1))) >> FIXED_SHIFT);
}

Division

Dividing two \(Q16.16\) values directly causes the scaling factors to cancel out, yielding an ordinary unscaled integer. To preserve the fractional component, the dividend must first be cast to a wider integer type and shifted left by the fractional bit count before performing the division:

fixed_t fixed_div(fixed_t a, fixed_t b) {
    if (b == 0) {
        // Handle divide-by-zero according to system requirements
        return 0;
    }
    int64_t temp = ((int64_t)a << FIXED_SHIFT);
    return (fixed_t)(temp / b);
}

Handling Overflow and Saturation

Unlike floating-point values that degrade to +INF or -INF upon overflow, standard integer overflow causes wraparound, leading to severe calculation errors. In safety-critical or digital signal processing (DSP) contexts, saturation arithmetic clamps values to maximum or minimum allowable limits instead of wrapping:

#include <limits.h>

fixed_t fixed_add_saturated(fixed_t a, fixed_t b) {
    int64_t sum = (int64_t)a + (int64_t)b;
    if (sum > INT32_MAX) {
        return INT32_MAX;
    }
    if (sum < INT32_MIN) {
        return INT32_MIN;
    }
    return (fixed_t)sum;
}

By substituting 64-bit temporary variables for multiplication and division, carefully controlling radix alignments, and selecting saturation where appropriate, fixed-point implementations deliver deterministic mathematical execution using minimal instruction cycles on microcontrollers without hardware FPUs.