HomeAbout UsContact Us

Fixed-Point Arithmetic in Embedded C: A Practical Guide

By Jithin Tom
Published in Embedded C/C++
July 22, 2026
1 min read
Fixed-Point Arithmetic in Embedded C: A Practical Guide

Table Of Contents

01
Q-Format Representation
02
Converting Float to Fixed-Point
03
Fixed-Point Multiplication
04
Fixed-Point Division
05
Overflow and Saturation Arithmetic
06
Practical Example: PID Controller in Q15.16
07
Q-Format Selection Checklist
08
Common Pitfalls
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

Fixed-point arithmetic remains the backbone of deterministic embedded signal processing. While Cortex-M4/M7 MCUs include an FPU, fixed-point delivers cycle-exact timing, zero hardware dependency, and predictable error bounds — critical for control loops, motor drives, and safety-critical systems.


Q-Format Representation

A Qm.n format uses m integer bits (excluding the sign bit) and n fractional bits in a signed 32-bit word:

Q15.16: 1 sign + 15 integer + 16 fractional → range: ±32767.99998, res: 15.3 ppm
Q7.24: 1 sign + 7 integer + 24 fractional → range: ±127.9999999, res: 0.06 ppm
Q31.0: 1 sign + 31 integer + 0 fractional → range: ±2,147,483,647, res: 1.0

The binary point is implied by the format — it does not exist in hardware.

+-----------------------------------------------------------------------+
| Q15.16 Bit Layout (32-bit) |
+=======================================================================+
| |
| 31 30 16 15 0 |
| +-+---------------------------+-------------------------------+ |
| |S| Integer (15 bits) | Fractional (16 bits) | |
| | | (magnitude only) | (implicit point) | |
| +-+---------------------------+-------------------------------+ |
| ^ ^ |
| | | |
| sign bit 0 |
| (LSB = 2^-16) |
| |
| Value = (int32_t raw) / 65536.0 |
| |
+-----------------------------------------------------------------------+

Converting Float to Fixed-Point

// Float → Q15.16 conversion with rounding and bounds clamping
static inline int32_t float_to_q15_16(float f) {
float scaled = f * 65536.0f;
if (scaled >= 2147483647.0f) return INT32_MAX;
if (scaled <= -2147483648.0f) return INT32_MIN;
return (int32_t)(scaled + (f >= 0 ? 0.5f : -0.5f));
}
// Q15.16 → Float
static inline float q15_16_to_float(int32_t q) {
return (float)q / 65536.0f;
}

Critical: The + 0.5f rounding avoids systematic truncation bias. For negative values, subtract 0.5.


Fixed-Point Multiplication

// Q15.16 × Q15.16 → Q15.16 (with 64-bit intermediate)
static inline int32_t mul_q15_16(int32_t a, int32_t b) {
int64_t prod = (int64_t)a * (int64_t)b; // Q30.32 intermediate
return (int32_t)(prod >> 16); // Back to Q15.16
}
+-----------------------------------------------------------------------+
| Q15.16 Multiplication Flow |
+=======================================================================+
| |
| Input A (Q15.16) Input B (Q15.16) |
| | | |
| v v |
| +----------+ +----------+ |
| | int32_t | | int32_t | |
| +----------+ +----------+ |
| | | |
| +----------+----------+ |
| v |
| +------------------+ |
| | int64_t prod | // 64-bit = 32 + 32 bits |
| | = a * b | // Q30.32 format |
| +------------------+ |
| | |
| v |
| +------------------+ |
| | prod >> 16 | // Shift right 16 = divide by 65536 |
| +------------------+ |
| | |
| v |
| Output (Q15.16) |
| |
| ! WITHOUT int64_t: prod overflows at 32 bits > WRONG RESULT |
| |
+-----------------------------------------------------------------------+

Fixed-Point Division

// Q15.16 ÷ Q15.16 → Q15.16
static inline int32_t div_q15_16(int32_t num, int32_t den) {
if (den == 0) return (num >= 0) ? INT32_MAX : INT32_MIN; // Saturate on div-by-zero
// Pre-shift numerator to preserve fractional bits
int64_t num_shifted = (int64_t)num << 16; // Q31.32
int64_t half_den = (den >= 0) ? (den >> 1) : (-((int64_t)den) >> 1);
// Correct symmetric rounding for positive and negative numerators
if (num_shifted >= 0) {
num_shifted += half_den;
} else {
num_shifted -= half_den;
}
return (int32_t)(num_shifted / den); // Back to Q15.16
}

Key insight: Without the << 16 pre-shift, integer division discards all fractional bits.


Overflow and Saturation Arithmetic

// Saturated addition for Q15.16
static inline int32_t sat_add_q15_16(int32_t a, int32_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 (int32_t)sum;
}
// Saturated subtraction for Q15.16
static inline int32_t sat_sub_q15_16(int32_t a, int32_t b) {
int64_t diff = (int64_t)a - (int64_t)b;
if (diff > INT32_MAX) return INT32_MAX;
if (diff < INT32_MIN) return INT32_MIN;
return (int32_t)diff;
}
// Saturated multiplication
static inline int32_t sat_mul_q15_16(int32_t a, int32_t b) {
int64_t prod = (int64_t)a * (int64_t)b;
int64_t shifted = prod >> 16;
if (shifted > INT32_MAX) return INT32_MAX;
if (shifted < INT32_MIN) return INT32_MIN;
return (int32_t)shifted;
}
// Saturated division for Q15.16
static inline int32_t sat_div_q15_16(int32_t num, int32_t den) {
if (den == 0) return (num >= 0) ? INT32_MAX : INT32_MIN;
int64_t num_shifted = (int64_t)num << 16;
int64_t half_den = (den >= 0) ? (den >> 1) : (-((int64_t)den) >> 1);
if (num_shifted >= 0) {
num_shifted += half_den;
} else {
num_shifted -= half_den;
}
int64_t quot = num_shifted / den;
if (quot > INT32_MAX) return INT32_MAX;
if (quot < INT32_MIN) return INT32_MIN;
return (int32_t)quot;
}
+-----------------------------------------------------------------------+
| Overflow Scenarios in Q15.16 |
+=======================================================================+
| |
| ADDITION: |
| --------- |
| 0x60000000 (+24576.0) + 0x60000000 (+24576.0) |
| = 0xC0000000 (-16384.0) < WRONG: wrapped to negative! |
| |
| MULTIPLICATION: |
| --------------- |
| 0x7FFF0000 (~32767.0) x 0x00020000 (2.0) |
| = 0xFFFE00000000 (Q30.32) >> 16 = 0xFFFE0000 (-2.0) < WRONG! |
| |
| FIX: Use int64_t intermediate + saturation |
| |
+-----------------------------------------------------------------------+

Practical Example: PID Controller in Q15.16

typedef struct {
int32_t kp; // Q15.16
int32_t ki; // Q15.16
int32_t kd; // Q15.16
int32_t integral; // Q15.16 (accumulator)
int32_t prev_error; // Q15.16
} pid_q15_16_t;
int32_t pid_update(pid_q15_16_t *pid, int32_t setpoint, int32_t measured, uint32_t dt_ms) {
// Error = setpoint - measured (all Q15.16)
int32_t error = sat_sub_q15_16(setpoint, measured);
// Proportional: Kp * error
int32_t p_term = sat_mul_q15_16(pid->kp, error);
// Integral: Ki * integral (integral accumulates error * dt)
int32_t error_dt = sat_mul_q15_16(error, (int32_t)((uint64_t)dt_ms * 65536 / 1000)); // dt in Q15.16
pid->integral = sat_add_q15_16(pid->integral, error_dt);
int32_t i_term = sat_mul_q15_16(pid->ki, pid->integral);
// Derivative: Kd * (error - prev_error) / dt
int32_t d_error = sat_sub_q15_16(error, pid->prev_error);
int32_t d_term = sat_div_q15_16(sat_mul_q15_16(pid->kd, d_error),
(int32_t)((uint64_t)dt_ms * 65536 / 1000));
pid->prev_error = error;
// Sum all terms with saturation
int32_t output = sat_add_q15_16(sat_add_q15_16(p_term, i_term), d_term);
return output; // Q15.16 output
}

Q-Format Selection Checklist

ApplicationRecommended Q-FormatRationale
Motor control (current/position)Q15.16±32767 range covers most sensor scales; 15 ppm resolution
Audio processingQ0.31 or Q15.16High dynamic range; Q0.31 for [-1,1) normalized signals
Temperature sensingQ7.24Small range (±127°C), need micro-degree resolution
IMU sensor fusionQ15.16 or Q5.26Balance range (accel: ±16g) and precision
PID coefficientsQ15.16Gains typically 0.01–100; fractional precision critical
FFT/DSP algorithmsQ0.31 or Q5.26Normalized signals, maximize SNR

Common Pitfalls

PitfallSymptomFix
int32_t prod = a * b;Overflow at 32 bitsUse int64_t prod = (int64_t)a * b;
result = num / den;Loses all fractional bitsPre-shift: (int64_t)num << frac_bits
No rounding on conversionSystematic biasAdd 0.5 LSB before truncation
Mixing Q-formatsSilent precision lossExplicit conversion functions per format
Division by zeroHardFault / undefinedCheck denominator, saturate to MAX/MIN

Summary

Fixed-point arithmetic trades dynamic range for deterministic execution and zero hardware dependencies. The discipline is simple:

  1. Pick one Q-format per signal path — document it in the variable name (speed_q15_16)
  2. Always use int64_t for multiply intermediates — shift back after
  3. Pre-shift numerators for division — preserve fractional bits
  4. Saturate, don’t wrap — overflow in control loops causes instability
  5. Verify with error analysis — compute worst-case error propagation

On Cortex-M, a fixed-point multiply-accumulate (MAC) in Q15.16 compiles to 2–3 cycles (SMULL + shift) versus 5–14 cycles for soft-float or 3–5 for FPU single-precision. For ISR contexts and hard real-time loops, that determinism is the difference between meeting deadline and missing it.



References

  1. ARM, “CMSIS-DSP Library User Guide”, ARM DUI 0603, Chapter “Fixed-Point Arithmetic”
  2. Texas Instruments, “Fixed-Point Arithmetic: An Introduction”, SPRA684, 2004
  3. Randy Yates, “Fixed-Point Arithmetic: An Introduction”, Digital Signal Labs, 2013
  4. Joe Lemieux, “Fixed-Point Math in C”, Embedded Systems Programming, 2003
  5. Jack Ganssle, “Fixed-Point vs Floating-Point: The Embedded Debate”, Embedded.com, 2017
  6. ISO/IEC 9899:2018 (C18), Section 6.2.5 “Types” — Integer representation and arithmetic

Frequently Asked Questions

What is the difference between fixed-point and floating-point in embedded systems?

Fixed-point uses integer arithmetic with an implicit binary point position, providing deterministic execution cycles and no hardware FPU requirement. Floating-point (IEEE 754) offers dynamic range but requires FPU or software emulation, adding cycle variance and code size.

How do I choose the right Q-format for my application?

Analyze your signal's dynamic range (max/min values) and required precision. Q15.16 gives 15 integer bits (range ±32767) with 16 fractional bits (15.3 ppm resolution). Q7.24 sacrifices range for precision. Always verify: max_value < (1 << integer_bits) and resolution ≤ required_precision.

Why does fixed-point multiplication require a 64-bit intermediate?

Multiplying two Q15.16 values produces a Q30.32 result. The 32 fractional bits need 64 bits to hold before shifting right by 16 to return to Q15.16. Using 32-bit intermediate truncates the high bits, causing precision loss and overflow bugs.

How do I handle division in fixed-point without losing precision?

Pre-shift the numerator left by fractional bits before dividing. This preserves fractional precision. Always check for division by zero. For correct rounding, symmetrically add or subtract half the denominator based on the numerator's sign before dividing.

When should I use fixed-point instead of floating-point on Cortex-M4/M7 with FPU?

Use fixed-point for: deterministic timing (ISR paths), ultra-low-power (FPU off), signal processing chains where error analysis is required, and when porting to Cortex-M0/M3 without FPU. Use floating-point for: complex math (trig, sqrt), prototyping, and when dynamic range varies unpredictably.

Tags

fixed-pointembedded-carithmeticq-formatcortex-moptimization

Share


Previous Article
Fixing Cortex-M Vector Table Relocation Bugs in Startup Code
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Link-Time Optimization (LTO) for Embedded Firmware Size Reduction
Link-Time Optimization (LTO) for Embedded Firmware Size Reduction
August 08, 2026
2 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media