Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 21 – Embedded C++

Fixed-point arithmetic in C++ firmware

Store fractions as scaled integers in Q15 and Q16.16, multiply through a wider type, round and saturate, and get the same bits on a laptop and a Cortex-M.

  • Advanced
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A Q format fixes where the binary point sits · Adding is integer adding; multiplying needs a wider type · Q16.16 products round down, and a result too large wraps · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Represent fractions as Q15 and Q16.16 integers and read their values back
  • Implement fixed-point multiplication and division through a wider type without overflow
  • Choose a format, a rounding rule and saturation from the range and resolution a value needs
  • Verify that the host and the target compute identical results

Before you start

A Cortex-M0 or Cortex-M3 has no floating-point unit, so every float operation in its firmware becomes a call to a software routine, and the same float code can round its last bit differently under another compiler. Fixed-point arithmetic avoids both by storing a number with a fraction as an ordinary integer and a scale that the code agrees on: in Q16.16, an int32_t holds the value times 65,536, so 1.5 is stored as 98,304.

In Q15, an int16_t holds the value times 32,768, which covers −1 up to just below 1. Adding two such numbers is integer addition. Multiplying needs a type twice as wide for the product, a shift back to the scale, and a rule for rounding and overflow. Integer operations have one right answer, so the code gives the same bits on a laptop, on the device and with every compiler.

A Q format fixes where the binary point sits

The figure draws both layouts bit by bit, with the The place in a binary number that separates the integer bits from the fraction bits, as the decimal point does in 1.5. In a fixed-point format it never moves, so the format alone says where it is. marked:

Bit layouts of Q16.16 (sign, 15 integer bits, 16 fraction bits) and Q15 (sign, 15 fraction bits), with stored examples.Q16.16 in an int32_t: value = raw ÷ 65,536Sinteger part: 15 bitsfraction: 16 bitsbit 31binary pointbit 01.5 is stored as 98,304 = 0x0001 8000range −32,768 to 32,767.99998, step 1/65,536Q15 (1.15) in an int16_t: value = raw ÷ 32,768Sfraction: 15 bitsbit 15↑ binary point, right after the sign bitbit 00.5 is stored as 16,384 = 0x4000; −1 as −32,768 = 0x8000range −1 to 0.999969, step 1/32,768S: sign bit (two's complement)

Where the binary point sits in Q16.16 and in Q15

Text description of the diagram

The figure shows two integers as bars of bits, the most significant bit on the left.

Q16.16 in an int32_t, where the value is the raw integer divided by 65,536. Bit 31 is the sign bit of the two's complement integer. The next 15 bits hold the integer part, and the last 16 bits hold the fraction; the binary point sits between them. 1.5 is stored as 98,304, which is 0x0001 8000 in hexadecimal. The range is −32,768 to 32,767.99998, in steps of 1/65,536.

Q15, also written 1.15, in an int16_t, where the value is the raw integer divided by 32,768. Bit 15 is the sign bit, and the binary point sits right after it, so all 15 remaining bits are fraction. 0.5 is stored as 16,384 (0x4000) and −1 as −32,768 (0x8000). The range is −1 to 0.999969, in steps of 1/32,768.

This lesson writes a Q format as Qm.n: m integer bits, the sign bit included, and n fraction bits; the value is the stored integer divided by 2ⁿ. Arm’s CMSIS-DSP library uses the same convention and calls its q15_t a “16-bit fractional data type in 1.15 format”. Some datasheets leave out the sign bit and write Q0.15 for the same layout, so check the convention before trusting a scale.

Format and type Range, and the size of one step
Q15 (1.15) in int16_t −1 to 0.999969, in steps of 1/32,768 ≈ 0.0000305
Q31 (1.31) in int32_t −1 to just below 1, in steps of 1/2,147,483,648 ≈ 0.00000000047
Q16.16 in int32_t −32,768 to 32,767.99998, in steps of 1/65,536 ≈ 0.0000153

Q15 holds a fraction between −1 and 1 in 16 bits, which suits audio samples and filter coefficients. Q31 holds the same range in 32 bits with 65,536 times finer steps. Q16.16 trades fraction bits for an integer part, so it reaches about ±32,768, which suits sensor values and positions. Each bit moved to the integer side doubles both the range and the step.

Programmer Calculator (Bitwise) Enter 0x00018000 to see the bits of 1.5 in Q16.16, or 0x4000 for 0.5 in Q15.

Checkpoint

Check: Storing a fraction in a Q format

  1. Question 1 of 2 Which integer stores −0.5 in Q15?

    Type a number.

    A hint

    Q15 scales by 32,768.

    Show the answer: Question 1 of 2

    Answer: -16384 as the raw value

    Q15 stores the value times 32,768, so −0.5 is −0.5 × 32,768 = −16,384, 0xC000 in 16 bits. The figure shows the other two values of that layout: 0.5 is 16,384 and −1 is −32,768.

  2. Question 2 of 2 A motor position runs from −2,000 mm to +2,000 mm and must be stored to 0.01 mm. Which of the three formats in the table can hold it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Q16.16 in an int32_t

    The range decides first: only Q16.16 has integer bits to reach 2,000. Its step, about 0.0000153, is then far below the 0.01 mm that matters.

Adding is integer adding; multiplying needs a wider type

Values in the same format add and subtract as integers, because (a × 2¹⁶) + (b × 2¹⁶) = (a + b) × 2¹⁶: the scale stays the same. Values in different formats are first shifted to a common scale.

A product doubles the scale: (a × 2¹⁶) × (b × 2¹⁶) = (a × b) × 2³², so the raw product has 32 fraction bits and is shifted right by 16 to return to Q16.16. It also needs room. For 1.5 × 2.25 the raw values are 98,304 and 147,456, and their product, 14,495,514,624, is more than six times the largest int32_t. In int32_t that multiplication would overflow, which is undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++” for signed types; in int64_t it is exact, and the shift gives 221,184, which is 3.375 × 65,536. Division runs the other way: the dividend is widened and shifted left by 16 first, so that the quotient keeps its fraction bits. Step through that product:

Interactive · stage by stage 1.5 × 2.25 in Q16.16, one operation at a time Step through the product as the Q16 type computes it, from the stored values to the result, with the raw number at every stage.
1.5 × 2.25 in Q16.16, one operation at a time, step 6 of 6. Showing The stored values, Widen to std::int64_t, Multiply (32 fraction bits: 3.375), Shift right by 16 (221,184, nothing dropped), Back to std::int32_t (221,184 = 3.375). In focus: Back to std::int32_t.The stored values98,304 and 147,456Widen to std::int64_troom for any productMultiply32 fraction bits: 3.375Shift right by 16221,184, nothing droppedBack to std::int32_t221,184 = 3.375

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The stored values. 1.5 is stored as 1.5 × 65,536 = 98,304, and 2.25 as 2.25 × 65,536 = 147,456. Each raw value carries one factor of 65,536, the scale of Q16.16.
  2. Widen first. Both raw values are converted to std::int64_t before they meet. A product of two 32-bit integers needs up to 64 bits, so in 64 bits it is always exact.
  3. The raw product. 98,304 × 147,456 = 14,495,514,624, about 6.75 times the largest int32_t, 2,147,483,647. Multiplied in int32_t, it would overflow, which is undefined behaviour for a signed type.
  4. Two scales. The product carries the scale twice: (a × 2¹⁶) × (b × 2¹⁶) = (a × b) × 2³². It has 32 fraction bits, and 14,495,514,624 ÷ 2³² is exactly 3.375.
  5. Shift back. Shifting right by 16 divides by 65,536 and drops the low 16 bits: 14,495,514,624 >> 16 = 221,184. Here those bits are all zero, so nothing is lost.
  6. Back to 32 bits. 221,184 fits in an int32_t, and 221,184 ÷ 65,536 = 3.375, exactly 1.5 × 2.25. A result of 32,768 or more would not fit, and narrowing it would wrap round.

Checkpoint

Check: Multiplying through a wider type

  1. Question 1 of 2 0.5 × 0.5 in Q16.16: both raw values are 32,768. Which raw value does the 64-bit product give after the shift right by 16?

    Type a number.

    A hint

    Multiply the raw values first, then divide by 65,536.

    Show the answer: Question 1 of 2

    Answer: 16384 as the raw value

    32,768 × 32,768 = 1,073,741,824, a number with 32 fraction bits. Shifted right by 16 it is 16,384, which is 0.25 × 65,536: the product 0.25 in Q16.16.

  2. Question 2 of 2 Two Q16.16 values are added. What does the code do besides adding the raw integers?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing, as long as the sum stays in range, because the scale stays 2¹⁶

    Values in the same format add and subtract as integers. Only values in different formats are first shifted to a common scale.

Q16.16 products round down, and a result too large wraps

Six products through Q16, whose operators hide the widening and the shift inside a * b:

q16.cpp C++ · q16.cpp
// A Q16.16 number: a 32-bit integer that stores the value times 65,536.
#include <cmath>
#include <cstdint>
#include <format>
#include <print>

struct Q16 {
    std::int32_t raw;  // the value times 2 to the power 16

    static constexpr int frac_bits = 16;
    static constexpr std::int32_t one = std::int32_t{1} << frac_bits;  // 65,536 stands for 1.0

    static Q16 from(double v) { return {static_cast<std::int32_t>(std::lround(v * one))}; }
    double value() const { return static_cast<double>(raw) / one; }

    friend Q16 operator+(Q16 a, Q16 b) { return {a.raw + b.raw}; }
    friend Q16 operator-(Q16 a, Q16 b) { return {a.raw - b.raw}; }
    friend Q16 operator*(Q16 a, Q16 b) {
        const std::int64_t wide = std::int64_t{a.raw} * b.raw;  // 32 fraction bits now
        return {static_cast<std::int32_t>(wide >> frac_bits)};   // back to 16
    }
};

int main() {
    const double pairs[][2] = {{1.5, 2.25}, {3.14159, 2.71828}, {0.1, 0.1},
                               {-0.1, 0.1}, {150, 200}, {200, 200}};
    std::println("{:>7} {:>7}  {:>9} {:>9}  {:>14} {:>13}  {:>7}",
                 "a", "b", "a.raw", "b.raw", "Q16.16 a*b", "exact a*b", "steps");
    for (const auto& [x, y] : pairs) {
        const Q16 a = Q16::from(x), b = Q16::from(y);
        const double exact = a.value() * b.value();     // the two stored values, multiplied in double
        const double got = (a * b).value();
        const double steps = (got - exact) * Q16::one;  // the error in units of 1/65,536
        std::println("{:>7} {:>7}  {:>9} {:>9}  {:>14.6f} {:>13.6f}  {:>7}", x, y, a.raw, b.raw,
                     got, exact, std::abs(steps) < 1 ? std::format("{:.2f}", steps) : "wrapped");
    }
    std::println("1 + 2.25 - 0.5 = {}", (Q16::from(1) + Q16::from(2.25) - Q16::from(0.5)).value());
}
Products in Q16.16 on both compilers Python · q16_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -O2 q16.cpp -o q16
./q16
g++-16 -std=c++23 -Wall -Wextra -O2 q16.cpp -o q16
./q16
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -O2 q16.cpp -o q16
$ ./q16
      a       b      a.raw     b.raw      Q16.16 a*b     exact a*b    steps
    1.5    2.25      98304    147456        3.375000      3.375000     0.00
3.14159 2.71828     205887    178145        8.539688      8.539702    -0.89
    0.1     0.1       6554      6554        0.009995      0.010001    -0.44
   -0.1     0.1      -6554      6554       -0.010010     -0.010001    -0.56
    150     200    9830400  13107200    30000.000000  30000.000000     0.00
    200     200   13107200  13107200   -25536.000000  40000.000000  wrapped
1 + 2.25 - 0.5 = 2.75
$ g++-16 -std=c++23 -Wall -Wextra -O2 q16.cpp -o q16
$ ./q16
      a       b      a.raw     b.raw      Q16.16 a*b     exact a*b    steps
    1.5    2.25      98304    147456        3.375000      3.375000     0.00
3.14159 2.71828     205887    178145        8.539688      8.539702    -0.89
    0.1     0.1       6554      6554        0.009995      0.010001    -0.44
   -0.1     0.1      -6554      6554       -0.010010     -0.010001    -0.56
    150     200    9830400  13107200    30000.000000  30000.000000     0.00
    200     200   13107200  13107200   -25536.000000  40000.000000  wrapped
1 + 2.25 - 0.5 = 2.75

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 q16_session.py

The column exact a*b multiplies the two stored values in double, so steps is the error of the multiplication alone, in units of 1/65,536. The shift drops the product’s low 16 bits, and since C++20 a right shift of a negative value rounds towards minus infinity, so every inexact product here rounded down, by 0.89, 0.44 and 0.56 of a step.

In the last row, 40,000 does not fit below 32,768, and converting the shifted product back to int32_t keeps its low 32 bits, which is defined since C++20, so 200 × 200 came out as −25,536. A reading that suddenly changes sign is worse than one that stops at its limit, which is why production code saturation arithmetic: Arithmetic that stops at the type's limits instead of overflowing: 100 + 100 in std::int8_t gives 127. C++26 adds it to <numeric> as std::saturating_add and its relatives, first named std::add_sat. More in “Integer overflow and signed/unsigned conversions”: CMSIS-DSP’s Q15 and Q31 multiplications clamp results outside their range. Both compilers printed the same table, because every step is integer arithmetic.

Key idea

A Q16.16 product is exact in 64 bits; the shift rounds it down, and narrowing a result out of range wraps it round.

Checkpoint

Check: Rounding down and wrapping round

  1. Question 1 of 2 In the recorded run, −0.1 × 0.1 printed −0.010010, while the exact product of the stored values is −0.010001. Why is the Q16.16 result further from zero?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The shift rounds towards minus infinity, which for a negative product is away from zero

    Since C++20 a right shift of a negative value rounds towards minus infinity. Every inexact product of the run rounded down: 8.539688 instead of 8.539702, 0.009995 instead of 0.010001, −0.010010 instead of −0.010001.

  2. Question 2 of 2 Which of these products wrap round in the lesson's Q16 type?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • 200 × 200, which is 40,000
    • 180 × 190, which is 34,200

    Q16.16 holds −32,768 to 32,767.99998. A product outside that range is exact in 64 bits, but converting it back to int32_t keeps only its low 32 bits, so it wraps round to a value with the wrong size and often the wrong sign.

Integer results are the same bits everywhere; float results are not

Floating-point operations are rounded, and a compiler may turn a multiplication followed by an addition into one One operation that computes a × b + c and rounds only the result, instead of rounding the product and then the sum., which rounds once instead of twice. A four-digit model shows why:

Interactive · stage by stage One multiply-add, rounded twice or once Follow a × b + c in a model that keeps four significant digits, first rounded after each operation, then fused into one operation.
One multiply-add, rounded twice or once, step 6 of 6. Showing a × b exactly (7.006652: seven digits), Round the product (7.007, 0.000348 too high), Add c = −7.006 (0.001), Fused: add c, round once (0.000652). In focus: Add c = −7.006, Fused: add c, round once.fuseda × b exactly7.006652: seven digitsRound the product7.007, 0.000348 too highAdd c = −7.0060.001Fused: add c, round once0.000652

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Four digits. This model keeps four significant decimal digits, where a float keeps 24 binary digits, so that every number can be checked by hand. Take a = 1.234, b = 5.678 and c = −7.006.
  2. The exact product. a × b = 7.006652 exactly. That needs seven digits, three more than the model keeps.
  3. Rounded once already. A separate multiplication rounds its product to four digits before anything else happens: 7.007, which is 0.000348 more than the exact product. That error stays in everything computed from it.
  4. Then the sum. The addition gives 7.007 − 7.006 = 0.001. That fits four digits, so the second rounding changes nothing here: the separate path answers 0.001.
  5. Rounded only once. A fused multiply-add takes the exact product, 7.006652, adds c to it and rounds only the result: 7.006652 − 7.006 = 0.000652, which fits four digits.
  6. Two answers. The two paths differ by 0.000348, the error that the first rounding added to the product. Here c cancels most of the digits, so the gap is large; in a float filter it usually shows up in the last bit.

The separate path answered 0.001 and the fused one 0.000652, from the same three numbers. GCC’s manual makes -ffp-contract=fast the default for C++ on processors with that instruction; Clang’s default, -ffp-contract=on, fuses within one expression; -ffp-contract=off never fuses. The same float code can therefore change its last bits with the compiler, the flags or the processor. Integer operations have one right answer, so once the rounding and overflow rules are written in the code, a fixed-point filter gives the same integers everywhere.

Checkpoint

Check: Why integer results match on every build

  1. Question 1 of 2 What does a fused multiply-add change about a × b + c?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It rounds once, at the end, instead of after the multiplication and again after the addition

    In the four-digit model the separate path answered 0.001 and the fused one 0.000652, because the first path had already rounded the product up by 0.000348. A compiler that fuses changes which of the two answers a program gets.

  2. Question 2 of 2 A team needs a float filter to give the same bits from GCC and Clang builds of one source. Which setting stops both compilers from fusing a multiplication and an addition?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: -ffp-contract=off

    GCC defaults to fast for C++ and Clang to on, so the same source can fuse in different places. Fixed point needs no such setting, because integer operations have one right answer.

Practise

Practice that adapts to you

Practise this lesson

13 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 How large is one step of Q16.16?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: 1/65,536, about 0.0000153

    The value of a Qm.n number is the stored integer divided by 2ⁿ, so one step is 1/2ⁿ: 1/65,536 for Q16.16.

  2. Question 2 of 13 Which integer stores 0.75 in Q16.16?

    Choose one answer.

    Show the answer: Question 2 of 13

    Answer: 49,152

    Q16.16 stores the value times 65,536, and 0.75 × 65,536 = 49,152. The value 24,576 is 0.75 in Q15, which scales by 32,768 instead.

  3. Question 3 of 13 A value is stored as Q17.15 instead of Q16.16 in the same int32_t. What happens to the range and to the step?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: Both double, to about ±65,536 and a step of 1/32,768

    Q17.15 keeps 17 integer bits, the sign included, and 15 fraction bits: the value is the integer divided by 32,768, so it reaches about ±65,536 in steps of about 0.0000305.

  4. Question 4 of 13 Why does the Q16 type multiply in std::int64_t before shifting right by 16?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: The raw product of two Q16.16 values has 32 fraction bits and can need up to 64 bits

    Each raw value carries a factor of 65,536, so the product carries 65,536², which is 32 fraction bits. For 1.5 × 2.25 the raw product is 14,495,514,624, more than six times the largest int32_t. The shift by 16 then returns it to Q16.16; on its own, the shift truncates rather than rounds.

  5. Question 5 of 13 Two Q15 values are multiplied in an int32_t. How many fraction bits does the raw product have?

    Type a number.

    A hint

    Each factor brings its own scale.

    Show the answer: Question 5 of 13

    Answer: 30 fraction bits

    Each Q15 value carries 2¹⁵, so the product carries 2³⁰: 30 fraction bits. A shift right by 15 returns it to Q15, and 16 × 16 bits fit in the 32 bits of the product.

  6. Question 6 of 13 Why is the dividend widened and shifted left by 16 before a Q16.16 division?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: Dividing two values with the same scale cancels the scale, so the dividend is scaled up first to keep 16 fraction bits

    Division runs the other way from multiplication: the dividend is widened and shifted left by 16 first, so that the quotient keeps its fraction bits.

  7. Question 7 of 13 A Q15 gain multiplies a Q16.16 reading. How many fraction bits does the raw product have, and which shift returns it to Q16.16?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: 31 fraction bits; shift right by 15

    A product's scale is the product of the factors' scales. Here 2¹⁵ × 2¹⁶ = 2³¹, and a shift right by 15 leaves 2¹⁶, the scale of the reading.

  8. Question 8 of 13 Since C++20, what does a right shift do to a negative signed value?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: It divides by 2ⁿ and rounds towards minus infinity

    The shift rounds down, which is towards minus infinity. That is why every inexact Q16.16 product in the run came out below the exact one.

  9. Question 9 of 13 In the recorded run, 200 × 200 printed −25536. Why?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: 40,000 does not fit in Q16.16, and the conversion back to int32_t kept only the low 32 bits

    The 64-bit product is exact, and after the shift it is 40,000 × 65,536 = 2,621,440,000, which is larger than the largest int32_t. Since C++20, converting it to int32_t is defined and keeps the low 32 bits, which reads as −1,673,527,296, that is −25,536 in Q16.16. Saturating code would stop at 32,767.99998 instead.

  10. Question 10 of 13 A speed of 20,000 in Q16.16 is multiplied by a gain of 2.0 with the lesson's Q16 type. What does the controller read, and what would have been safer?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: −25,536; saturating would stop at 32,767.99998 and keep the sign right

    Any product of 32,768 or more wraps round when it is narrowed. Production code saturates instead, as CMSIS-DSP's Q15 and Q31 multiplications do.

  11. Question 11 of 13 Why can the same float code give results that differ in the last bits on two builds for one processor?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: One build may fuse a multiplication and an addition into one instruction that rounds once instead of twice

    A fused multiply-add computes a × b + c with a single rounding. Whether the compiler forms one depends on its contraction setting (-ffp-contract), which differs between GCC and Clang and can change with optimisation. Integer arithmetic has no such freedom, so fixed-point results are the same on every build.

  12. Question 12 of 13 Why did Clang and GCC print exactly the same Q16.16 table?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: Every step is an integer operation with one right answer, and the rounding is written in the code

    Integer operations have one right answer, so once the rounding and overflow rules are written in the code, a fixed-point computation gives the same integers on every compiler and processor.

  13. Question 13 of 13 In the four-digit model, a = 2.345, b = 6.789 and c = −15.92. What does the fused multiply-add give?

    Type a number.

    A hint

    Work out the exact product first, add c, and round only at the end.

    Show the answer: Question 13 of 13

    Answer: 0.000205 (anything from 0.0002045 to 0.0002055 counts)

    2.345 × 6.789 = 15.920205 exactly. Fused: 15.920205 − 15.92 = 0.000205, which fits four digits. Rounded twice, the product becomes 15.92 and the sum 0: the two orders give different answers.

Choosing formats, then rounding, saturating and dividing safely Pro: opens with a Pro, Premium or Ultimate pass

Most fixed-point bugs come from a format chosen without numbers, from dropped bits, from overflow or from division. This part works through each with programs that run, and measures every fix against the failure it prevents.

In this part:

  • Choosing a format for each variable, with worked numbers
  • A filter that never reaches its input: the dead band, measured
  • Saturating instead of wrapping, and counting saturations
  • Q16.16 division is a library call even on a Cortex-M3, read from the compiler’s output

1 diagram · 8 runnable examples (C++, Python) · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

The same filter on a laptop and a Cortex-M3, bit for bit Pro: opens with a Pro, Premium or Ultimate pass

One low-pass filter, written in float and in Q16.16, is built here with two compilers and several flags and also run on an emulated Cortex-M3. The float results move with the build settings, and the fixed-point results never do.

In this part:

  • Float outputs that change with the compiler’s flags, measured
  • The Q16.16 filter on four host builds and on QEMU’s Cortex-M3
  • What the Cortex-M3 build needs: startup code, a linker script and semihosting
  • Your turn: a Fixed template that rounds and saturates

1 diagram · 7 runnable examples (C header, C++, C++ (filter_host.cpp), C++ (filter_target.cpp), LD, Python) · about 8 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher to mid · Warm-up

    What do Q15 and Q16.16 mean, and what range and step does each give?

    Both store a fraction as an integer with an agreed scale. Q15, also written 1.15, is an int16_t that holds the value times 32,768: a sign bit and 15 fraction bits, so it covers −1 up to 32,767/32,768, about 0.99997, in steps of about 0.0000305. Q16.16 is an int32_t that holds the value times 65,536: 16 integer bits, the sign included, and 16 fraction bits, so it covers −32,768 up to just below 32,768 in steps of about 0.0000153. Adding works directly on the stored integers; a product needs a type twice as wide, 32 bits for Q15 and 64 for Q16.16, followed by a shift right of 15 or 16 bits. Conventions differ on whether the sign bit is counted, so Q0.15 can name the same layout as Q15.

  2. Mid to senior

    A Q16.16 low-pass filter, y += alpha * (x - y) with alpha = 0.01, runs on a Cortex-M0. With a constant input of 2.5 it settles 100 steps of 1/65,536 below 2.5 and never gets closer. Explain why, and fix it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    You are porting a PID control loop from float on a PC to a Cortex-M0 without a floating-point unit. How do you choose a fixed-point format for each variable, and how do you show that the port is accurate enough?

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A golden-output test compares 1,000 outputs of a float filter bit for bit. It passes on developers' machines and fails on the CI server, which builds the same code with GCC at -O2 for the same processor. What is the likely cause, and what do you change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A Q format is an integer with an agreed scale: Q16.16 stores the value times 65,536, Q15 times 32,768.
  • Values in one format add as integers; a product needs a type twice as wide and a shift back.
  • The shift rounds a product down, and a result out of range wraps round unless the code saturates.
  • Integer results are the same bits on every build, while a fused multiply-add can change a float’s last bit.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.