Skip to content
Back to Blog
Tutorials

Q15 Fixed-Point Explained: Q7, Q31, Rounding and Overflow

0.1 stored as Q15 becomes 3277, and 1.0 flips to -1.0 unless you saturate. Read any Q notation, convert it by hand, then check it in the online converter.

15 min read

Q15 Fixed-Point Explained: Q7, Q31, Rounding and Overflow

Q15 fixed point stores a fraction as an ordinary 16-bit signed integer with an implied binary scale of 2^15 = 32768. Encoding is one multiply plus one rounding step:

raw = round(value × 32768)

Decoding is one division:

value = raw ÷ 32768

That is the entire arithmetic. 0.5 × 32768 = 16384, so 0.5 lives in memory as the integer 16384, and 16384 ÷ 32768 returns exactly 0.5. There is no exponent field and no hidden bit. The binary point is a convention agreed between you and whoever reads the word; the hardware only ever sees an int16.

Two consequences fall straight out of that formula, and both are where people lose an afternoon.

0.1 × 32768 = 3276.8, which is not an integer. Q15 keeps 3277, and the value you read back is 0.100006103515625, not 0.1.

1.0 × 32768 = 32768, which is one past the largest signed 16-bit integer. So 1.0 has no Q15 encoding at all, and what your code does about that depends on a policy most codebases never write down. Saturate, and you get 0.999969482421875. Wrap, and you get -1.0.

Both failures trace back to a convention somebody chose and nobody wrote down. So does most of the pain in fixed-point code: which Q layout a datasheet means, which way the encoder rounds, and what happens when a value runs off the end of the range.

Q15, Q1.15, Qm.n: are they the same layout?

Ask three references what Q15 means and you can get three answers. This is not you misreading them. The notation genuinely was never standardised, and the disagreement is about one bit.

How to read Qm.n

The two-number form is the honest one. In Qm.n, n is the number of fractional bits and m is the number of integer bits. This article counts the sign position inside m, which is the reading that makes Q16.16 a 32-bit word: 16 integer bits including the sign, 16 fractional bits, scale 2^16 = 65536. Encode 1.5 in Q16.16 and you get 1.5 × 65536 = 98304, hex 0x00018000, with no rounding error at all.

The trouble starts with the sign bit. Some authors count it inside m, some add it on top. Q1.15 under the first convention is a 16-bit word: one sign/integer position plus 15 fraction bits. Under the second convention the same label describes 17 bits, which no machine has.

Why the same Q15 label means different widths in different documents

The single-number form Q15 drops m entirely and leaves the width implicit. In DSP practice it almost always means a signed 16-bit two’s-complement word with 15 fractional bits, and that is the meaning the TI and ARM ecosystems settled on decades ago.

But you will find widely copied explanations that say something like: Q15 means 15 fractional bits, so if we define a 32-bit number, there is 1 sign bit and 16 integer bits. Read it carefully and it is counting the sign bit on top of m rather than inside it — the other convention from the one used here. The layout it describes is real, and it is normally written Q16.15. What is wrong is the label on it: a 32-bit word with 16 integer bits is not Q15 under either convention. The same paragraph has been reproduced across enough blogs that it now outranks the correct definition for some queries.

Treat a bare Q15 in an unfamiliar document as a hypothesis, not a fact. Check it against something you can measure: the register width in the memory map, the C type in the driver header, or a known sample value.

The only description that survives contact with someone else’s code

Write down three things and the ambiguity disappears:

  • Signedness — signed two’s complement, or unsigned
  • W — total bits in the word
  • F — fractional bits

signed, W=16, F=15 cannot be misread. Neither can unsigned, W=32, F=16. Everything else is derived: the scale is 2^F, the resolution is 2^-F, and the range is the integer range of the word divided by 2^F. Put those three values in the protocol document and in the struct comment, and the argument stops happening.

The online Q-format converter prints signedness, W, F and the scale beside every result for the same reason. When a datasheet says “Q15” and a colleague’s parser says something else, decoding one known word settles it.

The conversion formula, worked by hand

Both directions are short enough to do on paper, which matters when you are staring at a hex dump on a scope screen.

Float to fixed: round(x × 2^F)

Take 0.5 into Q15.

  1. Scale: 0.5 × 32768 = 16384
  2. Round: already an integer, so 16384 stands
  3. Range check: signed 16-bit holds -32768 through 32767, and 16384 fits
  4. Store: 16384, hex 0x4000, binary 0100000000000000

Only step 2 can lose information, and only step 3 can fail. The rest is bookkeeping.

Fixed to float: raw ÷ 2^F

Now go the other way, starting from a register capture that reads 0xC000 in a signed Q15 field.

  1. Parse the text as an unsigned 16-bit code: 0xC000 = 49152
  2. The format is signed and the top bit is set, so subtract 2^16: 49152 - 65536 = -16384
  3. Scale down: -16384 ÷ 32768 = -0.5

Step 2 is the one people skip. Without the two’s-complement correction, 0xC000 reads as +1.5, which is not even in range for Q15, a useful smell test. If a decoded value sits outside the format’s range, you almost certainly forgot the sign step.

from fractions import Fraction


def encode_q15(x):
    """Decimal value -> signed Q15 stored integer (ties to even)."""
    return round(Fraction(str(x)) * 32768)


def decode_q15(word):
    """Unsigned 16-bit code -> exact Q15 value."""
    if word & 0x8000:
        word -= 0x10000
    return Fraction(word, 32768)


print(encode_q15(0.5))              # 16384
print(encode_q15(0.1))              # 3277
print(float(decode_q15(0xC000)))    # -0.5
print(float(decode_q15(0x0CCD)))    # 0.100006103515625

Fraction is doing real work here. Scaling through a float first would reintroduce binary rounding at the exact moment you are trying to measure it, and Fraction(str(x)) reads the decimal literal you typed rather than the double nearest to it.

Reading and writing the hex word

Hex is how these values actually appear in register views, and the conversion is mechanical: 3277 in hex is 0xCCD, padded to W/4 digits as 0x0CCD. Always pad. A Q15 word is four hex digits and a Q31 word is eight; dropping the leading zero is how a value ends up misaligned in a batch decoder.

Keep raw codes unsigned in hex, too. 0x8000 is the most negative Q15 word, not +32768, and writing it as -0x8000 helps nobody. Byte order is a separate question, since Q format specifies numeric scaling and says nothing about endianness. A little-endian dump of 0x0CCD arrives as the bytes CD 0C. For plain radix changes while reading a dump, the number base converter handles binary, octal and hex without a scale or signed width attached.

Q7, Q15, Q31 and Q16.16: range and resolution

Every number in this table is -2^(W-1) divided by 2^F at one end and (2^(W-1) - 1) divided by 2^F at the other. The values are exact, not rounded for display.

FormatWidthFraction bits FScale 2^FMinimumMaximum (exact)Resolution
Q787128-1127/128 = 0.99218751/128 = 0.0078125
Q15161532768-132767/32768 = 0.9999694824218751/32768 = 0.000030517578125
Q3132312147483648-1(2^31−1)/2^31 = 0.99999999953433871269226074218751/2^31 ≈ 4.6566128730773926e-10
Q16.16321665536-32768(2^31−1)/65536 = 32767.99998474121093751/65536 = 0.0000152587890625

Paste any of these boundary values into the Q-format converter and it echoes back the stored integer, the hex word and the exact decimal, which is a quick way to check a firmware constant before it ships.

Why the top of the Q15 format range is 0.999969482421875

The Q15 format range is asymmetric, and the asymmetry comes from two’s complement rather than from anything specific to fixed point. A signed 16-bit word covers the integers -32768 through 32767. Divide both ends by 32768 and the range becomes -32768/32768 through 32767/32768, which is -1 through 0.999969482421875.

So 1.0 is missing by exactly one LSB. It is not “approximately 1.0 is the maximum” or “1.0 with rounding”. The value has no encoding, and a converter that reports 1.0 for Q15 without saying it saturated is lying to you.

Why -1 is included

Plenty of references write the Q15 range as -1 < X < 0.9999695, with an open bracket on both sides. The lower bound is wrong. -32768 ÷ 32768 = -1 exactly, so -1 is representable, and both ends of [-1, 0.999969482421875] are attainable values. You will also see the range written [-1, 1), which is the same statement made against full scale: 1.0 itself is out of reach, but the largest value that is in reach is 0.999969482421875, not something short of it.

This matters more than a notation quibble. -1 is the value that breaks multiplication, because -1 × -1 = 1 and 1 is out of range. Anyone who assumes -1 is unreachable will not write the saturation branch that catches it.

The truncated upper bound 0.9999695 in those same references is a display artefact. There is nothing repeating about the value: it is 32767/32768, and 32768 is a power of two, so the decimal expansion terminates after 15 digits at 0.999969482421875.

0.1 does not fit in Q15

Scale it and the problem shows up: 0.1 × 32768 = 3276.8. Fixed point can only store integers, so something has to give.

With round-to-nearest, the stored word is 3277, hex 0x0CCD. The value that word represents is:

3277 ÷ 32768 = 0.100006103515625

The quantization error is the difference between what you asked for and what the grid could give you:

0.100006103515625 - 0.1 = +0.000006103515625

That is about six millionths, or one fifth of an LSB. Harmless in a volume control. In an integrator that adds it back a thousand times a second it becomes a steady drift of roughly 0.006 per second in the accumulated value.

Fixed-point error is uniform; floating-point error is not

How the error is spread out, rather than how large it is, tends to decide which system a project wants.

Q15 lays down a grid of 65536 points spaced exactly 1/32768 apart across [-1, 0.999969482421875]. The spacing near 0.9 is the same as the spacing near 0.0001, so worst-case absolute error is half an LSB everywhere. That makes error analysis boring in the best way: you can bound the noise floor of a filter with arithmetic a junior engineer can check.

IEEE 754 does the opposite. It keeps a fixed number of significant bits and moves the exponent, so the absolute gap between neighbouring doubles grows with magnitude while the relative error stays near constant. Near 1.0 that gap is about 2.2e-16; near 1e12 it is about 0.0001220703125.

Same failure, different shape. A double cannot hold 0.1 either. It stores 0.1000000000000000055511151231257827021181583404541015625, which is why 0.1 + 0.2 returns 0.30000000000000004. That case is worked through in the companion piece on floating point precision. Fixed point does not solve decimal fractions in binary; it only makes the error size predictable.

Three rounding modes, one LSB apart

The rounding rule is part of the data contract, not an implementation detail. Two correct implementations that disagree on rounding produce test vectors that differ in the last bit forever, and tracking that down is miserable work.

ModeRule10911.744-3276.8
Round to nearest, ties to evenNearest grid point; exact halves go to the even integer10912-3277
Truncate toward zeroDrop the fraction, magnitude only shrinks10911-3276
Floor toward negative infinityAlways down the number line10911-3277

The two columns show why one example is never enough. For a positive value, truncate and floor agree. For a negative value they split by a full LSB, because truncation pulls -3276.8 up toward zero and floor pushes it down.

The worked example everyone gets wrong

0.333 in Q15 is a good test case because it sits close to a boundary. 0.333 × 32768 = 10911.744.

  • Truncate: 10911, which reads back as 0.332977294921875
  • Round to nearest: 10912, which reads back as 0.3330078125

Older tutorials print 10911 and move on without saying which rule produced it. Take that number into a codebase whose encoder rounds and your golden vectors fail on the first run, with a one-count difference that looks like a typo rather than a policy mismatch.

Negatives are where the three rules separate. -0.1 × 32768 = -3276.8 gives -3276 under truncation (value -0.0999755859375) and -3277 under floor or nearest (value -0.100006103515625). Truncation and nearest each treat the two signs alike — ±3276 and ±3277 respectively — so a symmetric coefficient pair stays symmetric. Floor does not: it sends +0.1 to 3276 but -0.1 to -3277, and the pair comes out lopsided by one count.

What your language does by default

None of these defaults are wrong. They are just different, and they do not announce themselves.

  • C/C++: a cast from floating point to an integer type truncates toward zero. (int16_t)(0.333f * 32768) gives 10911.
  • Python: the built-in round() uses ties-to-even, so round(3276.8) is 3277 and round(2.5) is 2.
  • JavaScript: Math.round breaks ties toward positive infinity, which is not symmetric. Math.round(2.5) is 3 but Math.round(-2.5) is -2.
  • Hardware: many DSP multiply-accumulate paths round on the shift and offer nearest-even as a mode bit, which is why the reference C model and the silicon can disagree until someone reads the mode register.

Pick one rule and name it in the format spec next to W and F, so the test vectors carry it too.

Overflow: saturation vs wraparound

Q15 overflow does one of three things: reject the value, clamp it to 0.999969482421875, or wrap it around to -1.0. Which one you get is a policy your toolchain picked, and the third option silently reverses the sign.

Take 1.0. Scaling gives 1.0 × 32768 = 32768, and the largest signed 16-bit integer is 32767. The value is out of range by exactly one.

PolicyStored wordValue read backWhat it looks like downstream
Errornoneconversion rejectedLoud, catchable, usually right for tooling
Saturate0x7FFF = 327670.999969482421875Indistinguishable from 1.0 by ear or eye
Wrap0x8000 = -32768-1.0Full-scale sign inversion

The saturate row loses 0.000030517578125 and nobody notices. The wrap row turns a full-scale positive sample into a full-scale negative one, and in an audio path that is a click you can hear across a room. In a control loop it is a full-scale command in the wrong direction.

The dangerous property of wraparound is that it produces a perfectly valid-looking word. 0x8000 is a legal Q15 encoding of -1.0. Nothing downstream can tell it apart from a genuine -1.0 sample, so there is no signature to grep for after the fact; you only find it by instrumenting the point where the overflow happened.

Why DSP silicon ships saturating instructions

Saturation is the behaviour signal processing wants, so processors implement it rather than leaving it to a branch. ARM has QADD/QSUB and the SSAT/USAT saturating-shift instructions, NEON has VQADD and friends, and x86 SSE has packed saturating adds such as paddsw. TI’s C6000 and C55x families expose saturation as a mode bit on the accumulator path.

In a filter or a mixer, an occasional clipped sample is a small local distortion. A wrapped sample is a discontinuity with energy across the whole spectrum. By default the hardware picks slightly wrong over catastrophically wrong, but only if you enabled it. Plain C integer arithmetic on the same chip still wraps.

Why a Q15 multiply needs a 15-bit right shift

Multiply two Q15 words as integers and the result is correct, but it is no longer Q15. Fractional bits add during multiplication: Q15 × Q15 gives Q30.

Work through 0.5 × 0.5, where the right answer is obviously 0.25:

16384 × 16384 = 268435456          ← this is Q30, not Q15
268435456 ÷ 2^30 = 0.25            ← read as Q30, correct
268435456 >> 15 = 8192             ← realign to Q15
8192 ÷ 32768 = 0.25                ← same answer, back in Q15

Interpret 268435456 as Q15 and you would read 8192.0, which is off by a factor of 32768. That factor is the entire bug, and it explains why fixed-point filters that “almost work” are often out by a power of two.

The product also needs room. Two 16-bit values multiply into up to 32 bits, so the intermediate must be int32_t. Accumulating many products needs more headroom still; that is why DSP accumulators on parts like the C55x are 40 bits.

Round the shift, do not just drop the bits

A bare >> 15 throws away the low 15 bits, which is truncation toward negative infinity for signed values. Adding half an LSB of the outgoing precision first turns it into round-to-nearest:

#include <stdint.h>
#include <stdio.h>

static int16_t sat_q15(int32_t v) {
    if (v >  32767) return  32767;
    if (v < -32768) return -32768;
    return (int16_t)v;
}

static int16_t mul_q15(int16_t a, int16_t b) {
    int32_t prod = (int32_t)a * (int32_t)b;   /* Q30 */
    int32_t back = (prod + (1 << 14)) >> 15;  /* round, then Q30 -> Q15 */
    return sat_q15(back);
}

int main(void) {
    printf("0.5*0.5   -> %d\n", mul_q15(16384, 16384));
    printf("0.1*0.1   -> %d\n", mul_q15(3277, 3277));
    printf("-1*-1     -> %d\n", mul_q15(-32768, -32768));
    printf("no-round  -> %d\n", (int)(((int32_t)3277 * 3277) >> 15));
    return 0;
}

Compiled with cc -std=c11 -Wall -o q15 q15.c && ./q15, this prints:

0.5*0.5   -> 8192
0.1*0.1   -> 328
-1*-1     -> 32767
no-round  -> 327

The third line is the -1 case from earlier. -32768 × -32768 = 1073741824, which is 1.0 in Q30 and out of range for Q15, so sat_q15 clamps it to 32767. Remove the clamp and the cast to int16_t wraps it to -32768, turning -1 × -1 into -1.

The last two lines are the rounding difference. 3277 × 3277 = 10738729, and a bare shift gives 327 (0.009979248046875) while the rounded shift gives 328 (0.010009765625). The true product is 0.01, so the rounded version lands more than twice as close. One extra add buys that.

One caveat on the shift itself: right-shifting a negative signed integer is implementation-defined in C prior to C23, though every compiler you will meet performs an arithmetic shift. If that makes you uncomfortable, divide by 32768 and let the compiler emit the shift, or do the shift on an unsigned type after biasing. The broader mechanics of shifts and masks are covered in the guide to bitwise operations.

Addition needs matching Q values first

Multiplication changes the Q value predictably. Addition does not tolerate a mismatch at all: adding a Q7 word to a Q15 word produces nonsense, because the operands do not share a scale.

Line them up with a shift first. 0.5 in Q7 is 64, and 64 << 8 is 16384, which is 0.5 in Q15. The shift amount is the difference in fractional bits, 15 - 7 = 8.

Shifting up is exact but costs headroom, since a Q7 value promoted to Q15 needs the wider container. Shifting down is lossy and needs the same rounding decision as the multiply. Either way, write the Q value of every intermediate in a comment. Fixed-point code where the scales live only in the author’s head is unmaintainable within a month.

Q15 fixed point or IEEE 754 floating point: how to choose

Both are binary place-value systems, so neither has an accuracy advantage in principle. The choice comes down to what the target hardware charges you and what guarantees you need.

QuestionPoints to fixed pointPoints to floating point
Is there a hardware FPU?No FPU, or a soft-float libraryHardware FPU with single-cycle ops
How wide is the dynamic range?Known and bounded, like normalised audioSpans many orders of magnitude
Does the wire format define a scale?Protocol or register fixes the binary scaleField is a genuine float
Do results need to be bit-exact across builds?Yes, integers reproduce everywhereTolerable to vary with FMA and optimisation
Is memory or bandwidth tight?16-bit samples halve the footprint of 32-bit floatsNot a constraint
Who maintains the code?Team already fluent in Q notationMixed team, scaling bugs are the bigger risk

The last row is not a joke. Fixed point moves error from the runtime to the design phase, which is a good trade only when someone is doing the design work. On a Cortex-M4F with a hardware FPU, single-precision float is often the faster and safer choice, and the traditional reflex to reach for Q15 is a habit from parts that no longer dominate.

Where IEEE 754 takes over

Reach for floating point when the word itself carries a sign, an exponent and a significand rather than a fixed scale. Q format stops applying at that point: there is no single 2^F to divide by, because the exponent varies per value.

The two representations meet constantly in practice. Sensor data arrives as Q15 register words, gets promoted to float for a long computation, and comes back as Q15 for the DAC. To inspect the floating-point half of that path bit by bit, the IEEE 754 converter breaks a value into sign, exponent and mantissa and prints the exact stored decimal, which is the same job the Q-format converter does for a fixed scale.

Both sit on the same foundation

Q format, IEEE 754 and plain integers are all reading the same bits with different rules about where the point sits and whether it can move. If the place-value part feels shaky, or you want to be quicker at reading 0x0CCD as 0000 1100 1100 1101 without reaching for a calculator, the primer on binary, hex and octal conversion covers the groundwork that both formats build on.

Q15 fixed point FAQ

What does Q15 mean?

In the common DSP convention, Q15 is a signed 16-bit two’s-complement word with 15 fractional bits: one sign position and 15 fraction positions. The scale is 2^15 = 32768, the resolution is 1/32768 = 0.000030517578125, and the range is -1 through 32767/32768. Because Q labels vary between documents, confirm the total width and signedness rather than trusting the label alone.

Are Q15 and Q1.15 the same?

They usually describe the same signed 16-bit layout, with the 1 in Q1.15 counting the sign position. But the notation is not universal, and some authors add the sign bit on top of m instead of counting it inside. The reliable description is signedness plus total bits W plus fractional bits F: for this layout, signed, W=16, F=15.

What is 0.5 in Q15?

0.5 in Q15 is the stored integer 16384, hex 0x4000. The arithmetic is 0.5 × 32768 = 16384, which is already an integer, so there is no rounding and no quantization error. Decoding confirms it: 16384 ÷ 32768 = 0.5 exactly.

What are the maximum and minimum values of Q15?

The minimum is -1, and it is included, because -32768 ÷ 32768 is exactly -1. The maximum is 32767/32768 = 0.999969482421875. Both of those are attainable, so the range is [-1, 0.999969482421875]; 1.0 is the value that falls outside. References that write the lower bound as an open interval are wrong, and that error hides the -1 × -1 overflow case.

What happens when Q15 overflows?

It depends on the policy in force. An error policy rejects the conversion. Saturation clamps to the nearest endpoint, so 1.0 becomes 0.999969482421875, a loss of one LSB that is usually inaudible. Wraparound applies modulo 2^16, so 1.0 scales to 32768, which reads back as -32768, and therefore -1.0, a full sign inversion. DSP hardware defaults to saturation for exactly this reason, but plain C integer arithmetic wraps.

Why do two Q15 numbers need a right shift by 15 after multiplying?

Because fractional bits add. Q15 × Q15 produces a Q30 product, so the integer result carries 30 fractional bits instead of 15. Shifting right by 15 realigns it to Q15: 16384 × 16384 = 268435456, and 268435456 >> 15 = 8192, which decodes to 0.25. Add 1 << 14 before the shift to round to nearest instead of truncating, and keep the intermediate in an int32_t so the 32-bit product does not overflow.

When should I use Q format instead of IEEE 754?

Use Q format when a protocol, register map, DSP algorithm or codec has already fixed a binary scale for an integer word. The scale is part of the interface, and you do not get to choose it. Use IEEE 754 when the value needs a wide dynamic range, when the target has a hardware FPU, or when the field genuinely stores a sign, exponent and significand. For a plain radix change with no scale attached, neither applies; that is ordinary base conversion.

The short version

Q15 fixed point is a multiply, a rounding decision and a range check. raw = round(value × 32768) going in, value = raw ÷ 32768 coming out. The formula is trivial; the failures all live in the parts nobody documents.

So document them. Write signedness, W and F next to every Q label instead of assuming the label is enough. Name the rounding mode in the same place, because truncate and floor split by a full LSB on negatives. Say explicitly whether overflow errors, saturates or wraps, because the wrap case turns 1.0 into -1.0 and leaves no evidence behind.

When a register word and a spreadsheet disagree, decode one known value in the Q-format converter. It shows the stored integer, the exact decimal, the quantization error and the representable range side by side, all computed in the browser. Usually that is enough to tell whose assumption about W and F was wrong.

Tags: fixed-point dsp embedded q-format number-representation

Related Articles

View all articles