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 (including sign) 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 16 15 0 |
| +---------------------+---------------------+ |
| | Integer (15 bits) | Fractional (16) | |
| | (including sign) | (implicit point) | |
| +---------------------+---------------------+ |
| ↑ ↑ |
| bit 31 bit 0 |
| (sign) (LSB = 2^-16) |
| |
| Value = (int32_t raw) / 65536.0 |
| |
+-----------------------------------------------------------------------+

Converting Float to Fixed-Point

// Float → Q15.16 conversion with rounding
static inline int32_t float_to_q15_16(float f) {
return (int32_t)(f * 65536.0f + (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) |
| │ │ |
| ▼ ▼ |
| +----------+ +----------+ |
| │ int32_t │ │ int32_t │ |
| +----------+ +----------+ |
| │ │ |
| └──────────┬──────────┘ |
| ▼ |
| +------------------+ |
| │ int64_t prod │ // 64-bit = 32 + 32 bits |
| │ = a * b │ // Q30.32 format |
| +------------------+ |
| │ |
| ▼ |
| +------------------+ |
| │ prod >> 16 │ // Shift right 16 = divide by 65536 |
| +------------------+ |
| │ |
| ▼ |
| 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 INT32_MAX; // Saturate on div-by-zero
// Pre-shift numerator to preserve fractional bits
int64_t num_shifted = (int64_t)num << 16; // Q31.32
// Round: add half denominator before division
int64_t result = (num_shifted + (den >= 0 ? (den >> 1) : -(den >> 1))) / den;
return (int32_t)result; // 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 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;
}
+-----------------------------------------------------------------------+
| Overflow Scenarios in Q15.16 |
+=======================================================================+
| |
| ADDITION: |
| ───────── |
| 0x40000000 (+32768.0) + 0x40000000 (+32768.0) |
| = 0x80000000 (-32768.0) ← WRONG: wrapped to negative! |
| |
| MULTIPLICATION: |
| ─────────────── |
| 0x7FFF0000 (~32767.0) × 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 = mul_q15_16(error, (int32_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 = div_q15_16(sat_mul_q15_16(pid->kd, d_error),
(int32_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 processingQ1.30 or Q15.16High dynamic range; Q1.30 for [-1,1) normalized signals
Temperature sensingQ7.24Small range (±127°C), need micro-degree resolution
IMU sensor fusionQ15.16 or Q3.28Balance range (accel: ±16g) and precision
PID coefficientsQ15.16Gains typically 0.01–100; fractional precision critical
FFT/DSP algorithmsQ1.30 or Q3.28Normalized 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: result = (numerator << frac_bits) / denominator. This preserves fractional precision. Always check for division by zero and consider rounding: (numerator << frac_bits) + (denominator >> 1) before division.

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

Fixing Cortex-M Vector Table Relocation Bugs in Startup Code
Fixing Cortex-M Vector Table Relocation Bugs in Startup Code
July 21, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media