Skip to content
返回博客
教程

Q15 定点数怎么算:Q 格式换算、舍入与溢出完全指南

0.1 存进 Q15 定点数会变成 3277,实际值是 0.100006103515625;1.0 溢出后不饱和就会翻成 -1.0。教你读懂 Q 记法、手算定点小数与浮点互转、分清饱和与回绕,在线免费验证。

15 分钟

Q15 定点数怎么算:Q 格式换算、舍入与溢出完全指南

Q15 定点数把一个小数当成普通的 16 位有符号整数来存,二进制小数点的位置隐含约定在 2^15 = 32768 这个刻度上。编码就是一次乘法加一步舍入:

raw = round(value × 32768)

解码就是一次除法:

value = raw ÷ 32768

全部算术就这些。0.5 × 32768 = 16384,所以 0.5 在内存里就是整数 16384,而 16384 ÷ 32768 精确还原成 0.5。这里没有指数域,也没有隐含位。二进制小数点只是你和读这个字的人之间的一个约定,硬件从头到尾看到的都只是一个 int16。

从这条公式里直接掉出来两个后果,而工程师的一个下午通常就折在这两处。

0.1 × 32768 = 3276.8,不是整数。Q15 只能存 3277,你读回来的值是 0.100006103515625,不是 0.1

1.0 × 32768 = 32768,比 16 位有符号整数的最大值大了 1。所以 1.0 在 Q15 里根本没有编码,而你的代码碰到这种情况会怎么办,取决于一条大多数项目从来不写下来的策略。做饱和,得到 0.999969482421875;做回绕,得到 -1.0

这两种失败都不难懂,难在它们藏得深:数据手册说法打架时 Q 记法到底该怎么读,纸笔怎么把两个方向各算一遍,以及工具链在背后替你选了哪种舍入和溢出规则。

Q15、Q1.15、Qm.n 是同一种布局吗

同一个 Q15,问三份资料能拿到三个答案。不是你看错了。这套记法从来就没被标准化过,而分歧只在一个比特上。

Qm.n 怎么读

两个数字的写法是诚实的那一种。在 Qm.n 里,n 是小数位数,m 是整数位数。本文把符号位算在 m 里面,正是这种读法才让 Q16.16 成为一个 32 位字:16 位整数位(含符号位)加 16 位小数位,刻度 2^16 = 65536。把 1.5 编成 Q16.16 得到 1.5 × 65536 = 98304,十六进制 0x00018000,一点舍入误差都没有。

麻烦从符号位开始。有的作者把符号位算进 m 里,有的作者额外加在外面。按第一种约定,Q1.15 是一个 16 位字:一位符号兼整数位,加 15 位小数位。按第二种约定,同一个标签描述的是 17 位,而没有哪台机器是这个位宽。

为什么同一个 Q15 标签在不同文档里位宽不一样

单数字写法 Q15 干脆把 m 省掉,位宽变成隐含信息。在 DSP 实践里,Q15格式几乎总是指一个带 15 位小数的有符号 16 位补码字,TI 和 ARM 这两个生态几十年前就是这么定下来的。

但你会读到一类被大量转载的解释,大意是:「Q15 表示 15 位小数,所以如果我们定义一个 32 位数,就是 1 位符号位加 16 位整数位。」仔细读会发现,这句话把符号位加在 m 之外,而不是算在 m 里面——和本文用的正好是相反的那一套约定。它描述的布局真实存在,通常写作 Q16.15。错的是贴在上面的那个标签:一个有 16 位整数位的 32 位字,在哪一套约定下都不叫 Q15。同一段话被抄进了足够多的博客,以至于在某些查询下它的排名已经压过了正确定义。

在陌生文档里看到光秃秃的 Q15,把它当成一个待验证的假设,而不是事实。拿可测量的东西去核对:内存映射里的寄存器宽度、驱动头文件里的 C 类型,或者一个已知的样本值。

唯一能扛住别人代码检验的描述方式

写下三件事,歧义就消失了:

  • 符号性:有符号补码,还是无符号
  • W:字的总位数
  • F:小数位数

signed, W=16, F=15 不可能被读错,unsigned, W=32, F=16 也一样。其余全是推导出来的:刻度是 2^F,分辨率是 2^-F,范围是这个字的整数范围除以 2^F。把这三个值写进协议文档和结构体注释里,你就再也不用吵这件事了。

在线的 Q 格式定点数转换器在每个结果旁边都会打印符号性、W、F 和刻度,理由也在这里。当数据手册说「Q15」而同事的解析器说的是另一回事时,解码一个已知的字,十秒钟就能定案。

换算公式,手算一遍

两个方向都短到可以在纸上做完。当你正盯着示波器屏幕上的十六进制转储时,这一点很要紧。

浮点转定点:round(x × 2^F)

0.5 转成 Q15。

  1. 放大:0.5 × 32768 = 16384
  2. 舍入:已经是整数,16384 原样保留
  3. 范围检查:有符号 16 位能装 -32768 到 32767,16384 在范围内
  4. 存储:16384,十六进制 0x4000,二进制 0100000000000000

只有第 2 步会丢信息,也只有第 3 步会失败。定点数里会出岔子的地方,基本都在这两处。

定点转浮点:raw ÷ 2^F

反过来走一遍,起点是一次寄存器抓取,某个有符号 Q15 字段读出来是 0xC000

  1. 先把这串文本按无符号 16 位码解析:0xC000 = 49152
  2. 格式是有符号的且最高位为 1,所以减去 2^16:49152 - 65536 = -16384
  3. 缩回刻度:-16384 ÷ 32768 = -0.5

第 2 步是最容易被跳过的。不做补码修正,0xC000 会读成 +1.5,而这个值连 Q15 的范围都进不去——这本身就是个好用的自检信号。如果解码出来的值落在格式范围之外,你八成是漏了符号那一步。

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 是在干实事。先经过 float 再放大,会在你正想测量二进制舍入误差的那一刻把它重新引进来;而 Fraction(str(x)) 读的是你敲进去的那个十进制字面量,不是离它最近的那个 double。

十六进制字的读与写

寄存器视图里这些值就是以十六进制出现的,转换过程很机械:3277 的十六进制是 0xCCD,补齐到 W/4 位就是 0x0CCD。永远要补齐。一个 Q15 字是四个十六进制位,一个 Q31 字是八个;丢掉前导零,值在批量解码器里就会错位。

原始码在十六进制里也保持无符号。0x8000 是最负的那个 Q15 字,不是 +32768,把它写成 -0x8000 对谁都没好处。字节序是另一个独立的问题。Q 格式规定的是数值刻度,对端序只字未提,0x0CCD 的小端转储到手时是 CD 0C 这两个字节。如果只是读转储时做纯粹的进制变换,二进制/十六进制/十进制转换器能处理二进制、八进制和十六进制,不带刻度也不带有符号位宽。

Q7、Q15、Q31 与 Q16.16:范围与分辨率

Q7、Q15、Q31 把符号位以外的每一位都给了小数,存的是纯粹的定点小数,取值范围整个卡在 -1 和 1 之间;Q16.16 把一半字长让给了整数部分,范围因此宽得多。表里每一个数,一端是 -2^(W-1) 除以 2^F,另一端是 (2^(W-1) - 1) 除以 2^F。这些都是精确值,没有为了显示而四舍五入。

格式位宽小数位 F刻度 2^F最小值最大值(精确)分辨率
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

把这些边界值里的任何一个粘进 Q 格式转换器,它都会把存储整数、十六进制字和精确十进制值回显给你。固件常量发布前,这是最快的确认方式。

Q15 的上界为什么是 0.999969482421875

Q15 的取值范围是不对称的,而这个不对称来自补码,不是定点数本身特有的什么东西。一个有符号 16 位字覆盖 -32768 到 32767 这些整数。两端都除以 32768,范围就变成 -32768/32768 到 32767/32768,也就是 -1 到 0.999969482421875。

所以 1.0 差的正好是一个 LSB。说「最大值约等于 1.0」或者「1.0 带点舍入」都不对,这个值根本就没有编码。一个对 Q15 报出 1.0 却不说明自己做了饱和的转换器,是在骗你。

-1 为什么算在范围内

不少资料把 Q15 的范围写成 -1 < X < 0.9999695,两端都是开区间。下界写错了。-32768 ÷ 32768 = -1 精确成立,所以 -1 是可表示的,[-1, 0.999969482421875] 的两端都是取得到的值。你也会看到范围被写成 [-1, 1),那是同一件事换成满量程的说法:1.0 本身够不着,但够得着的最大值就是 0.999969482421875,不是比它再小一点的什么数。

这比在记法上挑刺重要得多。-1 正是那个会把乘法搞坏的值,因为 -1 × -1 = 1,而 1 超出了范围。任何一个认定 -1 取不到的人,都不会去写那个能接住它的饱和分支。

同一批资料里那个被截断的上界 0.9999695 是显示造成的假象。这个值没有任何循环节:它就是 32767/32768,而 32768 是 2 的幂,所以十进制展开在第 15 位处终止,写全了就是 0.999969482421875

0.1 装不进 Q15

放大一下,问题立刻可见:0.1 × 32768 = 3276.8。定点数只能存整数,总得有一方让步。

按四舍五入到最近,存下来的字是 3277,十六进制 0x0CCD。这个字代表的值是:

3277 ÷ 32768 = 0.100006103515625

量化误差就是你要的值和这张网格能给的值之间的差:

0.100006103515625 - 0.1 = +0.000006103515625

大约是六百万分之一,或者说五分之一个 LSB。在音量控制里无害。在一个每秒把它累加一千次的积分器里就不无害了:累加值每秒会稳定漂移大约 0.006。

定点误差是均匀的,浮点误差不是

Q15 这样的定点小数在 [-1, 0.999969482421875] 上铺了一张 65536 个点的网格,点距精确地是 1/327680.9 附近的间距和 0.0001 附近的间距一样大,所以最坏情况下的绝对误差在任何地方都是半个 LSB。这让误差分析变得很无聊,而这种无聊恰恰是好事:你可以用一段初级工程师就能复核的算术,把滤波器的本底噪声界定住。

IEEE 754 反其道而行。它保留固定数量的有效位,让指数去动,所以相邻两个 double 之间的绝对间隔随数量级增大,而相对误差基本保持不变。在 1.0 附近这个间隔大约是 2.2e-16,在 1e12 附近大约是 0.0001220703125。

两套系统在这里犯的是同一种错,只是形状不同。double 同样装不下 0.1。它存的是 0.1000000000000000055511151231257827021181583404541015625,这正是 0.1 + 0.2 会返回 0.30000000000000004 的原因。那个案例在姊妹篇浮点数精度丢失:为什么 0.1 + 0.2 不等于 0.3里有完整推演。定点数并没有解决「二进制表示十进制小数」这个问题,它只是让误差的大小变得可预测。

三种舍入模式,彼此差一个 LSB

舍入规则是数据契约的一部分,不是实现细节。两个各自都正确的实现,只要在舍入上不一致,产出的测试向量就会在最低位上永远对不上,而追查这种问题是件苦差事。

模式规则10911.744-3276.8
四舍六入五成双(ties to even)取最近的网格点,正好一半时取偶数10912-3277
向零截断(truncate)直接丢掉小数部分,绝对值只会变小10911-3276
向负无穷取整(floor)在数轴上一律往下走10911-3277

两列并排就能看出为什么一个例子永远不够。对正数来说,截断和 floor 结果一致。对负数,它们差整整一个 LSB,因为截断把 -3276.8 往零的方向拉,而 floor 把它往下推。

所有人都会算错的那个例子

0.333 在 Q15 里是个好测试用例,因为它离边界很近。0.333 × 32768 = 10911.744

  • 截断:10911,读回来是 0.332977294921875
  • 四舍五入到最近:10912,读回来是 0.3330078125

老一点的教程直接印出 10911 就往下讲了,不说这是哪条规则得出的。把这个数拿进一个编码器做舍入的代码库,你的黄金向量第一次跑就会挂,而且是一个「差 1」,看起来像笔误,实际是策略不一致。

负数才是三条规则分道扬镳的地方。-0.1 × 32768 = -3276.8,截断给 -3276(值 -0.0999755859375),floor 或最近舍入给 -3277(值 -0.100006103515625)。截断和最近舍入对两个符号一视同仁,分别给出 ±3276±3277,所以一对对称的系数编完还是对称的。floor 不是:它把 +0.1 送到 3276,却把 -0.1 送到 -3277,这一对系数就歪了整整一个 LSB。

你用的语言默认怎么做

这些默认行为没有哪个是错的。它们只是不一样,而且都不会主动告诉你。

  • C/C++:从浮点类型强转到整数类型是向零截断。(int16_t)(0.333f * 32768) 得到 10911
  • Python:内置的 round() 用的是五成双,所以 round(3276.8)3277round(2.5)2
  • JavaScriptMath.round 在正好一半时朝正无穷方向取,并不对称。Math.round(2.5)3,但 Math.round(-2.5)-2
  • 硬件:很多 DSP 的乘累加通路会在移位时做舍入,并把最近偶数舍入做成一个模式位,于是参考 C 模型和芯片一直对不上,直到有人想起去读那个模式寄存器。

挑定一条规则,把它写在格式规格里 W 和 F 的旁边,再让测试向量把它一起带上。

溢出:饱和还是回绕

Q15 溢出时只会发生三件事之一:拒绝这个值、把它钳到 0.999969482421875,或者把它回绕成 -1.0。你拿到的是哪一种,取决于工具链替你选的策略,而第三种会悄悄把符号反过来。

1.0 来说。放大得到 1.0 × 32768 = 32768,而 16 位有符号整数的最大值是 32767。正好超出范围 1。

策略存储的字读回的值下游看起来是什么样
报错转换被拒绝动静大、可捕获,对工具类程序通常是对的
饱和0x7FFF = 327670.999969482421875耳朵和眼睛都分辨不出它和 1.0 的区别
回绕0x8000 = -32768-1.0满量程符号反转

饱和那一行丢掉 0.000030517578125,没人会察觉。回绕那一行把一个满量程的正样本变成满量程的负样本,在音频通路上这是一声隔着一个房间都能听见的爆音。在控制回路里,它是一条方向反了的满量程指令。

回绕最危险的性质在于,它产出的是一个看起来完全合法的字。0x8000 就是 -1.0 的合法 Q15 编码。下游没有任何环节能把它和一个真正的 -1.0 样本区分开,所以事后没有任何特征可以 grep;你只能在溢出发生的那个点上加插桩,才能把它找出来。

DSP 芯片为什么自带饱和指令

饱和正是信号处理想要的行为,所以处理器干脆把它做进硬件,而不是留给一个分支去处理。ARM 有 QADD/QSUB 以及 SSAT/USAT 这组饱和移位指令,NEON 有 VQADD 等一系列,x86 SSE 有 paddsw 这样的打包饱和加法。TI 的 C6000 和 C55x 系列则把饱和做成累加器通路上的一个模式位。

在滤波器或混音器里,偶尔一个被削顶的样本只是局部的小失真。而一个回绕的样本是一处能量遍布整个频谱的不连续点。硬件默认选了「稍微错一点」而不是「灾难性地错」——但前提是你把它打开了,而同一块芯片上普通的 C 整数运算照样回绕。

Q15 相乘为什么要右移 15 位

把两个 Q15 字当整数相乘,结果是对的,但它已经不是 Q15 了。小数位在乘法中会相加:Q15 × Q15 得到 Q30。

0.5 × 0.5 走一遍,正确答案显然是 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

268435456 当成 Q15 来读,你会读出 8192.0,差了 32768 倍。这个倍数就是整个 bug 的全部,它也解释了为什么那些「差不多能跑」的定点滤波器经常正好差 2 的某个幂。

乘积还需要地方放。两个 16 位值相乘最多得到 32 位,所以中间结果必须是 int32_t。要累加很多个乘积则需要更多余量,这就是 C55x 这类芯片上 DSP 累加器有 40 位的原因。

移位要带舍入,别只是把低位丢掉

光写 >> 15 会把低 15 位扔掉,对有符号值来说这是向负无穷截断。先加上输出侧精度的半个 LSB,它就变成了四舍五入到最近:

#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;
}

cc -std=c11 -Wall -o q15 q15.c && ./q15 编译运行,输出是:

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

第三行就是前面提到的 -1 那个情形。-32768 × -32768 = 1073741824,在 Q30 里是 1.0,对 Q15 来说超出范围,所以 sat_q15 把它钳到 32767。去掉这个钳位,强转到 int16_t 会把它回绕成 -32768,于是 -1 × -1 变成了 -1

最后两行是舍入带来的差别。3277 × 3277 = 10738729,直接移位得到 3270.009979248046875),带舍入的移位得到 3280.010009765625)。真实乘积是 0.01,带舍入的版本离它近了一倍多。代价只是多一次加法。

关于移位本身还有一点要注意:在 C23 之前,对负的有符号整数做右移属于实现定义行为,尽管你能遇到的每一个编译器都会执行算术移位。如果这让你不放心,就除以 32768 让编译器自己去生成移位指令,或者先加偏置再在无符号类型上做移位。移位和掩码更完整的机制,位运算完全指南:AND、OR、XOR、移位与位掩码实战里有讲。

加法之前先把 Q 值对齐

乘法对 Q 值的改变是可预测的。加法则完全不容忍不匹配:把一个 Q7 字加到一个 Q15 字上得到的是垃圾,因为两个操作数不共享同一个刻度。

先用移位把它们对齐。0.5 在 Q7 里是 6464 << 816384,也就是 Q15 里的 0.5。移位量就是小数位数之差,15 - 7 = 8

向上移位是精确的,但要付出余量的代价,因为一个提升到 Q15 的 Q7 值需要更宽的容器。向下移位有损,而且需要和乘法一样的舍入决策。无论走哪条路,都要在注释里写清每一个中间结果的 Q 值。刻度只活在作者脑子里的定点代码,一个月内就没法维护了。

Q15 定点还是 IEEE 754 浮点:怎么选

Q15格式和 IEEE 754 浮点都是二进制的位值制系统,所以原理上谁也没有精度优势。选择取决于目标硬件要向你收多少费,以及你需要什么样的保证。

问题倾向定点倾向浮点
有没有硬件 FPU?没有 FPU,或者只有软浮点库有硬件 FPU,运算单周期完成
动态范围有多宽?已知且有界,比如归一化后的音频跨好几个数量级
传输格式本身定了刻度吗?协议或寄存器定死了二进制刻度字段本身就是一个真正的浮点数
结果需要跨构建逐位一致吗?需要,整数在哪儿都能复现可以接受随 FMA 和优化而变化
内存或带宽紧张吗?16 位样本比 32 位浮点省一半不是约束
谁来维护这段代码?团队已经熟悉 Q 记法混合团队,刻度错误才是更大的风险

最后一行不是玩笑。定点数把误差从运行期挪到了设计期,只有当真的有人在做那份设计工作时,这才是笔划算的交易。在带硬件 FPU 的 Cortex-M4F 上,单精度浮点往往既更快也更安全,而伸手就去抓 Q15 的传统反射,是从那些早已不再主流的芯片上带过来的习惯。

什么时候该交给 IEEE 754

当那个字本身携带的是符号、指数和有效数,而不是一个固定刻度时,就该用浮点。那一刻 Q 格式就不再适用了:没有单一的 2^F 可以拿来除,因为指数是逐值变化的。

这两种表示在实践中经常打照面。传感器数据以 Q15 寄存器字的形式到达,被提升成 float 做一长串计算,再变回 Q15 送给 DAC。要逐位查看这条通路上浮点的那一半,IEEE 754 转换器:浮点数转十六进制/二进制会把一个值拆成符号、指数和尾数,并打印出精确的存储十进制值,和 Q 格式转换器对固定刻度做的事情一样。

两者踩的是同一块地基

Q 格式、IEEE 754 和普通整数,读的都是同一批比特,区别只在于小数点落在哪里、以及它能不能动。如果位值制这部分你还觉得不踏实,或者你想在不掏计算器的情况下更快地把 0x0CCD 读成 0000 1100 1100 1101进制转换完全指南:二进制、十六进制、八进制与十进制互转把两种格式共同依赖的底子讲清楚了。

Q15 定点数常见问题

Q15 是什么意思?

在常见的 DSP 约定下,Q15 是一个带 15 位小数的有符号 16 位补码字:一位符号位加 15 位小数位。刻度是 2^15 = 32768,分辨率是 1/32768 = 0.000030517578125,范围是 -1 到 32767/32768。由于 Q 标签在不同文档之间说法不一,别只信标签,要确认总位宽和符号性。

Q15 和 Q1.15 是一回事吗?

它们通常描述的是同一种有符号 16 位布局,Q1.15 里的那个 1 数的就是符号位。但这套记法并不通用,有的作者会把符号位加在 m 之外,而不是算在 m 里面。可靠的描述方式是符号性加总位数 W 加小数位数 F:对这种布局来说就是 signed、W=16、F=15。

0.5 在 Q15 里是多少?

0.5 在 Q15 里就是存储整数 16384,十六进制字是 0x4000。算式是 0.5 × 32768 = 16384,本身就是整数,所以没有舍入,也没有量化误差。解码验证一下:16384 ÷ 32768 = 0.5,精确成立。

Q15 的最大值和最小值是多少?

最小值是 -1,而且是包含在内的,因为 -32768 ÷ 32768 精确等于 -1。最大值是 32767/32768 = 0.999969482421875。这两端都取得到,所以范围就是 [-1, 0.999969482421875],落在外面的是 1.0。那些把下界写成开区间的资料是错的,而这个错误正好掩盖了 -1 × -1 的溢出情形。

Q15 溢出时会发生什么?

取决于当前生效的策略。报错策略会拒绝这次转换。饱和会钳到最近的端点,于是 1.0 变成 0.999969482421875,损失一个 LSB,通常听不出来。回绕则按 2^16 取模,1.0 放大后是 32768,读回来是 -32768,也就是 -1.0,符号整个反了过来。DSP 硬件默认走饱和正是因为这个原因,而普通的 C 整数运算是回绕的。

两个 Q15 数相乘后为什么要右移 15 位?

因为小数位会相加。Q15 × Q15 产出的是 Q30 乘积,整数结果里带的是 30 位小数而不是 15 位。右移 15 位把它重新对齐回 Q15:16384 × 16384 = 268435456,而 268435456 >> 15 = 8192,解码出来正是 0.25。移位前先加上 1 << 14 就是四舍五入到最近而非截断,同时把中间结果放在 int32_t 里,32 位乘积才不会溢出。

什么时候该用 Q 格式而不是 IEEE 754?

当协议、寄存器映射、DSP 算法或编解码器已经为一个整数字定死了二进制刻度时,用 Q 格式,因为刻度是接口的一部分,轮不到你选。当数值需要很宽的动态范围、目标平台有硬件 FPU,或者字段本身确实存的是符号、指数和有效数时,用 IEEE 754。如果只是不带刻度的纯进制变换,两者都不适用,那只是普通的进制转换。

长话短说

Q15 定点数就是一次乘法、一个舍入决策和一次范围检查。进去是 raw = round(value × 32768),出来是 value = raw ÷ 32768。公式很简单,失败全都藏在没人写下来的那些地方。

所以就把它们写下来。在每个 Q 标签旁边写上符号性、W 和 F,别假设标签本身就够用。舍入模式也写在同一处,因为截断和 floor 在负数上差整整一个 LSB。溢出到底是报错、饱和还是回绕,也要明说,因为回绕会把 1.0 变成 -1.0,而且不留下任何证据。

当一个寄存器字和一张表格对不上时,拿一个已知值到 Q 格式定点数转换器里解码一次。它会把存储整数、精确十进制值、量化误差和可表示范围并排列出来,全部在浏览器里算完。通常这就足够看出是谁对 W 和 F 的假设错了。

标签: fixed-point dsp embedded q-format number-representation