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.