
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.
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 ppmQ7.24: 1 sign + 7 integer + 24 fractional → range: ±127.9999999, res: 0.06 ppmQ31.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 || |+-----------------------------------------------------------------------+
// Float → Q15.16 conversion with roundingstatic inline int32_t float_to_q15_16(float f) {return (int32_t)(f * 65536.0f + (f >= 0 ? 0.5f : -0.5f));}// Q15.16 → Floatstatic 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.
// 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 intermediatereturn (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 || |+-----------------------------------------------------------------------+
// Q15.16 ÷ Q15.16 → Q15.16static 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 bitsint64_t num_shifted = (int64_t)num << 16; // Q31.32// Round: add half denominator before divisionint64_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.
// Saturated addition for Q15.16static 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 multiplicationstatic 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 || |+-----------------------------------------------------------------------+
typedef struct {int32_t kp; // Q15.16int32_t ki; // Q15.16int32_t kd; // Q15.16int32_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 * errorint32_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.16pid->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) / dtint32_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 saturationint32_t output = sat_add_q15_16(sat_add_q15_16(p_term, i_term), d_term);return output; // Q15.16 output}
| Application | Recommended Q-Format | Rationale |
|---|---|---|
| Motor control (current/position) | Q15.16 | ±32767 range covers most sensor scales; 15 ppm resolution |
| Audio processing | Q1.30 or Q15.16 | High dynamic range; Q1.30 for [-1,1) normalized signals |
| Temperature sensing | Q7.24 | Small range (±127°C), need micro-degree resolution |
| IMU sensor fusion | Q15.16 or Q3.28 | Balance range (accel: ±16g) and precision |
| PID coefficients | Q15.16 | Gains typically 0.01–100; fractional precision critical |
| FFT/DSP algorithms | Q1.30 or Q3.28 | Normalized signals, maximize SNR |
| Pitfall | Symptom | Fix |
|---|---|---|
int32_t prod = a * b; | Overflow at 32 bits | Use int64_t prod = (int64_t)a * b; |
result = num / den; | Loses all fractional bits | Pre-shift: (int64_t)num << frac_bits |
| No rounding on conversion | Systematic bias | Add 0.5 LSB before truncation |
| Mixing Q-formats | Silent precision loss | Explicit conversion functions per format |
| Division by zero | HardFault / undefined | Check denominator, saturate to MAX/MIN |
Fixed-point arithmetic trades dynamic range for deterministic execution and zero hardware dependencies. The discipline is simple:
speed_q15_16)int64_t for multiply intermediates — shift back afterOn 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.
Quick Links
Legal Stuff




