浮点与定点计算深度解析:嵌入式算法落地的精度与效率权衡
2026/8/31 22:53:26 网站建设 项目流程

搞嵌入式和数字信号处理的朋友,一定绕不开这个选择题:项目里到底用浮点(floating point)还是定点(fixed point)计算。我最早接触这个概念是在做音频算法移植的时候,MATLAB里跑得好好的浮点滤波器,一到MCU上要么慢得让人抓狂,要么内存不够用,后来才真正理解定点存在的意义。这篇文章不打算只讲概念,我会把两种表示方式的底层差异、量化落地方法、案例对比和常见坑都整理出来,希望给正在做算法落地或性能优化的你一个可以直接参考的思路。

1. 认识浮点和定点:两种不同的数字世界观

1.1 浮点的表示方式:用指数撑开动态范围

先说浮点。现代处理器上最常见的浮点格式是IEEE 754标准,单精度float占32bit,由1位符号位、8位指数位、23位尾数位组成,实际运算时尾数还隐藏了一个默认的1,所以有效位数是24位。它的本质是用指数来“缩放”,换来的好处是动态范围极大。单精度float能表示从大约±1.18e-38到±3.38e38的数,这在实际控制、音频、通信系统里几乎可以覆盖所有量级。

浮点精度是“相对精度”。什么意思?就是说不管数值是0.0001还是10000,它都能保持大约小数点后7位有效数字的相对精度。这对数学运算特别友好,尤其是算法里存在中间量级差异很大的情况,比如先计算一个很小的增益,再乘一个很大的信号幅度,浮点能优雅地跟进这个跨量级的运算而不会出问题。

1.2 定点的表示方式:用固定分辨率换简单硬件

定点就完全是另一种思路。它本质上就是整数运算,只不过我们人为约定小数点固定在某一位。比如Q1.15格式,1位符号位加15位小数,能表示的数值范围是[-1, 0.99997],分辨率是2^-15 ≈ 3.05e-5。Q31、Q7.8这些格式都是一个道理,只是小数点和整数部分的比例不同。

定点没有指数来动态调节量级,所以它的精度是“绝对精度”。在整个表示范围内,最小分辨率是固定的,也就是说如果信号幅度跌到比分辨率还小很多,信息就丢了;如果信号幅度超过最大表示范围,直接溢出。这点和浮点的差别是本质性的,也决定了两种方案的适用场景完全不同。

1.3 两种体系的本质差异表

对比项浮点(float32)定点(Q1.15)
数据宽度32bit(更常见还有64bit)通常16bit或32bit
动态范围极大,约±3.4e38固定,取决于整数位个数
精度类型相对精度,跨量级稳定绝对精度,固定分辨率
信噪比单精度约150dB动态范围16bit定点约92dB
硬件成本需要专用FPU,逻辑面积大整数ALU即可,乘法器简单
运算速度有FPU时快,无FPU时极慢普遍快,适合实时处理
开发调试难度较低较高,需要处理溢出和量化
适用场景PC、浮点DSP、算法原型MCU、低成本DSP、FPGA、通信基带

这里提醒一句,表格里的“信噪比”只是一个理想量化信噪比,因为量化为16bit时有6.02×15+1.76 ≈ 92dB。实际算法跑起来,经过乘累加、截断、反馈环路之后,噪声积累会比这个值差不少,后面我会专门讲。

2. 为什么嵌入式团队总在为精度和效率较劲

2.1 浮点带来的性能/资源成本从哪来

浮点运算之所以贵,是因为硬件要做的事情太多了。一个浮点乘法需要拆符号位、对齐指数、乘法、规格化、舍入、检查溢出等步骤;加法就更麻烦,两个数指数差太多时先要对阶,把尾数移来移去。这些步骤全都转换成了门电路资源和时钟周期。

在FPGA上这个差异非常直观。一个定点8×8乘法器可能只需要一个DSP slice区域,而一个浮点乘法IP核往往要占用几百个LUT和FF,综合后的时序频率还会下降。在MCU上如果内核没有FPU,比如Cortex-M0,编译器只能生成软浮点调用库,一个浮点乘法可能消耗上百个周期。一开始做项目时,我把一个自适应滤波算法整个用float写,放在M0上跑,中断里面根本来不及算完,逼得我只能全部改定点。

就算芯片自带FPU,也不能随便挥霍。Cortex-M4F虽然一条浮点乘法指令能出结果,但浮点单元的功耗和面积还在那里。低成本芯片上用FPU会把成本拉上去,对量产产品的性价比是很大的打击。

2.2 定点开发最痛的分寸感:精度、溢出、性能的三角博弈

定点的麻烦在于,你要在一开始就把数据范围算得明明白白。Q格式选得太保守,整数位留多了,小数分辨率被挤占,信号底噪变大;整数位留少了,信号一来就溢出,输出直接变成刺耳的爆音或者奇怪的跳变。这个“分寸感”真的是定点开发最痛的地方。

打个比方,浮点就像你开一辆有自动挡的车,不用管路况,油门刹车踩好就行;定点就像开手动挡,每个坡都得提前判断该挂几档,挂错了轻则顿挫,重则熄火。更麻烦的是信号处理算法不是单一运算,一个滤波器的系数、中间累加器、最终输出,每个环节的位宽可能都不一样,你得分别去设计。这也是为什么很多人说定点实现是手艺活,同样的算法结构,不同工程师做出来的性能能差出一个数量级。

这个环节没有捷径,但也不是完全没有方法论。后面我讲量化落地时,会给出完整的操作步骤,照着走能省很多冤枉路。

3. 从浮点到定点的量化方法论:完整落地步骤

3.1 第一步:摸清数据动态范围

不论从哪里拿到算法,第一步永远不是写代码,而是分析数据范围。先想清楚三件事:系统输入的最大幅值是多少、中间计算节点的理论范围是多少、输出需要达到什么精度。

以音频采集为例,ADC是16bit时,输入样本范围就是[-32768, 32767],归一化到[-1, 0.99997]对应Q15。如果前面还有自动增益控制(AGC),信号幅度可能会被放大到接近满刻度,这个也要纳入分析。中间节点就更需要逐个检查,比如一个二阶IIR滤波器的状态变量可能出现比输入大得多的值,如果状态变量和系数都塞进Q15,极容易溢出。遇到这种情况我一般先在浮点模型里把所有关键节点打点,跑几段最极端的测试信号,看每个节点实际最大值是多少,再决定Q格式。测试信号宁愿取得比实际更苛刻一些,这一步偷懒,后面调试就是地狱。

3.2 第二步:确定Q格式和中间位宽

在MATLAB里可以用似定点对象去模拟各种位宽,动手写硬件或C代码前先算清楚:

  • 输入的Q格式,比如Q15;
  • 系数的Q格式,通常也是Q15,但要先判断系数的绝对值是否都小于1;
  • 乘累加中间结果用多少位宽的累加器。

算式比较直观:如果输入是Bx位,系数是Bc位,做N个累加,那么累加器需要 Bx + Bc + ceil(log2(N)) 位才不容易溢出。比如16bit输入乘16bit系数,再做64个累加,就是16 + 16 + 6 = 38bit。很多DSP的累加器直接做40bit,正是这个原因。实际设计时我会把公式算完后再留2bit余量,省得边界情况翻车。

系数本身转成定点的过程也有讲究。假设系数是0.3678,在Q15下应写成 round(0.3678 × 32768) = 12053。注意是“四舍五入”而不是直接截断,直接截断会产生负的直流偏置,对反馈系统尤其致命。如果发现系数最大值接近1甚至超过1,比如浮点模型中某个系数是1.2,那就只能调整Q格式或先做整体缩放,否则没有任何兑换空间。

3.3 第三步:处理除法和饱和这两个头号大坑

定点世界里除法和浮点完全不是一个量级。浮点处理器一条指令就完成,而定点往往要查表、牛顿迭代,或者直接用移位近似。常见处理策略是尽量避免除法,能改成乘倒数就乘倒数;不能避免时,用查找表配合线性插值,精度足够且时间可控。

饱和处理是另一个核心点。定点的整数加法直接回绕(wrap-around),在信号处理里回绕通常意味着输出变成完全错误的值。正确做法是在关键输出点前加饱和限幅。C语言里可以这样写:

static inline int16_t sat16(int32_t x) { if (x > 32767) return 32767; if (x < -32768) return -32768; return (int16_t)x; }

但不要无脑每一步都饱和。饱和本质上是非线性操作,用多了会把本来正常的信号削波,产生高次谐波。一般原则是:有明确位宽收窄的地方才饱和,中间累加器宁可位宽给足,也不要频繁做饱和。

3.4 第四步:用C代码做定点模型验证

我强烈建议所有定点算法在进FPGA或DSP之前,先在PC上用C语言搭一个“bit-true”模型,也就是每个中间变量的位宽、舍入、饱和都和硬件完全一致。这样一来可以在PC上跑大规模测试向量,也可以和MATLAB浮点模型做逐样本对比。

我用过一个很土但有效的对比方式:让浮点和定点版本同时处理同一段正弦扫频信号,输出各存一份,然后逐点算误差绝对值,取最大值和均方根。最大值超过预期SNR就要回去检查是溢出、截断还是Q格式选错。这个过程自动化以后,每改一个参数都能立刻知道对精度的影响,比闷头看波形高效多了。

4. 一个可复现的实战案例:FIR滤波器从浮点降成定点

4.1 浮点原型设计

这部分我以一个16阶FIR低通滤波器为例,采样率48kHz,截止频率8kHz,窗函数用的汉明窗。先在PC上设计好浮点系数,然后用16bit正弦信号测试,确认浮点模型的输出符合预期。这个原型的作用就是作为后续定点实现对照的“金标准”。

浮点版本很简单,C代码大概是:

#define N 16 float coef_f32[N]; float buf_f32[N]; float fir_float(float input) { float acc = 0.0f; int i; for (i = N - 1; i > 0; i--) { buf_f32[i] = buf_f32[i - 1]; } buf_f32[0] = input; for (i = 0; i < N; i++) { acc += coef_f32[i] * buf_f32[i]; } return acc; }

4.2 定点化实现细节

把同样的滤波器改成Q15定点时,需要注意三件事。一是系数转Q15并检查是否越界;二是乘累加的位宽给足;三是在结果右移回Q15时用四舍五入代替直接截断。

实现是这样的:

#include <stdint.h> #define N 16 static int16_t coef_q15[N] = { // 由浮点系数乘32768后四舍五入得到 }; static int16_t delay_q15[N]; int16_t fir_q15(int16_t input) { int32_t acc = 0; int i; // 延迟线更新 for (i = N - 1; i > 0; i--) { delay_q15[i] = delay_q15[i - 1]; } delay_q15[0] = input; // 乘累加:Q15 * Q15 -> Q30,N=16累加需要36bit for (i = 0; i < N; i++) { acc += (int32_t)coef_q15[i] * delay_q15[i]; } // 加上1<<14再右移15位,实现四舍五入 acc = (acc + (1 << 14)) >> 15; // 饱和输出 if (acc > 32767) acc = 32767; if (acc < -32768) acc = -32768; return (int16_t)acc; }

这段代码就是我说的bit-true雏形,后面扔到任何DSP或FPGA里,逻辑都和这个C版本等价。编译器最好开优化,乘累加尽量用DSP指令,不过在不改变语义的前提下编译器通常都能处理好。

4.3 两种实现结果对比

我在PC上把两种实现跑了一遍,对比数据大概是这样的:

指标浮点版本定点版本
最大绝对误差(满量程1.0)基准约0.0005
输出SNR超过130dB约88dB
Q15系数越界检查不需要已通过
溢出饱和次数(测试10秒扫频)偶发1次后饱和
相对执行时间(无FPU的M0估算)1约0.12

从应用角度,88dB的SNR对于音频应用是足够的,人耳几乎感知不到这个噪声。但注意测试中偶发了一次饱和情况,说明如果输入信号在极端场景下再大一点,定点版本就可能削波。实际项目里要统计这个余量,不能只看平均信噪比。

5. 选型决策树:什么时候用浮点,什么时候用定点

5.1 处理器平台决定一半结论

选型的第一判断依据是平台。如果芯片本身有硬件FPU,比如Cortex-M4F、M7、一些浮点DSP,那么浮点未必慢很多,代码开发速度还快得多。如果目标平台是Cortex-M0、M3或者低端FPGA,定点几乎是唯一选择。我在M3上跑过一个自适应滤波算法,浮点版本优化后中断还是来不及,改成Q15之后不仅实时完成,还省下了大量内存,这个对比让我彻底改掉了“浮点至上”的思维。

FPGA上还要看算法的数据通量。高速通信基带里的FIR、信道均衡、FFT,基本都是定点,因为DSP slice的乘法器性能和功耗太优秀了。浮点IP核虽然也存在,但资源使用率通常比定点高一个量级,时序也更难收敛。

5.2 从应用场景反推需要的精度

选型另一个角度是从信号里反推。先估算整个系统的动态范围需求。举个例子,如果输入是16bit ADC,信噪比天然上限就是92dB左右,那么用Q15定点做主处理并不吃亏,浮点的更高精度并不会提升整体性能。反过来,如果一个算法要处理范围跨越6个数量级的坐标数据,比如惯性导航里的四元数更新和位置积分,定点想覆盖这么大的范围就需要给整数位留很多bit,这会严重挤压小数分辨率,这时候浮点的优势就体现出来了。

运算复杂度也很关键。如果算法里有大量除法、三角函数、指数函数、开方,定点的实现代价会迅速上升。常见做法是避免直接算这些函数,改用查表或多项式逼近,但逼近误差和大范围适应性都比浮点原生指令差不少。所以我的经验是:纯滤波类、相关类、能量统计类算法适合定点;导航解算、控制律动态补偿这类有大量非线性数学运算的,尽量选带FPU的平台,用浮点。

5.3 混合方案是我最推荐的一条路

很多时候项目并不是非黑即白。一块SoC里完全可以定点为主、浮点为辅。比如音频流处理用定点DSP核跑,保证实时性和功耗,而系统控制、协议栈、AGC参数计算这些低频率运算用浮点核,保证编程方便。我在一个语音处理项目里就是这么干的:前端回音消除和降噪全部走定点的DSP逻辑,后端的音量管理和音频路由在MCU上浮点算完再定点化下发,稳定性和开发速度都兼顾了。

算法内部也可以混用。FFT主体用定点,但归一化那一步用浮点单元算增益,精度和速度都拿得住。关键是要分开设计接口,定点和浮点之间转换的截断/饱和规则要统一,不然很容易在边界处引入噪声。

6. 常见问题与排查技巧实录

6.1 定点滤波器为何输出比浮点低一截

这里最常见的原因是忘了右移或右移位数不对。两个Q15数相乘,结果自动变成Q30,想回到Q15就得右移15位。如果忘了这一步,输出直接是真实值的1/32768,自然小得可怜。我见过不少人调了半天代码,最后发现是系数转换时乘了32768,但乘累加后没把结果移回去,整个滤波器的通带增益几乎变成一条直线。

还有个小坑是系数没有做归一化。FIR系数最大值如果只有0.05,但依然乘32768转成Q15,结果只在很小的区间里活动,中间累加器的大量动态范围被白白浪费。实际上应该先整体缩放系数,让最大值尽量接近1,再转Q15,这样输出信噪比最好。

6.2 定点算法跑久了为什么会漂

反馈结构中尤其是IIR滤波器,定点截断误差会不断积累。用直接截断的方式每次损失一点点负向偏置,经过高Q值极点放大后,输出可能会慢慢偏离理论值,甚至变成直流偏置。

解决办法首先是改用四舍五入,再就是误差反馈。误差反馈的思路不复杂:把每次缩放到Q15时被丢弃的低位部分存下来,下一次缩放前加回去。实际操作中可以借鉴所谓的fraction saving,用一阶噪声整形把量化噪声推到高频,效果很明显。我处理一个级联双二阶滤波器时就是这么解决的,否则输出末端总会有一层明显的低频鼓包。

6.3 溢出后的数据像爆音/雪花,如何快速定位

定点溢出如果没有饱和处理,会在输出里形成大幅跳变,听起来是刺耳的爆音,图像处理里就是雪花点。定位这类问题最快的办法是做一个单位脉冲测试:往算法里输入一个只有中间位置为1、其他位置为0的序列,用浮点模型和定点模型分别记录输出。脉冲响应的每一个样本都可以对账,一旦某个样本差得离谱,就往回查是乘累加溢出、系数转换错误,还是某个中间变量位宽不够。

我自己的排查顺序一般是:先查系数是否全部小于1,再查累加器位宽有没有按公式留够,最后查所有位宽收窄点是否都做了饱和或正确的舍入。三步走完,问题基本都能水落石出。要留意一点,不同C编译器在不同优化等级下,对整型提升和右移行为的处理可能会不一样,所以bit-true仿真时最好把编译选项和实际硬件一致。

6.4 工具链推荐

手动分析量化误差虽然可行,但效率不高。MATLAB的Fixed-Point Designer可以自动化扫描数据范围、生成不同Q格式下的误差报告,适合做算法原型的定点迁移研究。FPGA的HLS工具比如Vitis HLS也支持ap_fixed类型,定义好位宽后自动化处理舍入和饱和逻辑,省去了很多手写位宽管理的痛苦。在MCU场景里,CMSIS-DSP库提供了现成的q15、q31版本FIR、IIR、FFT函数,功能和性能已经优化得很好,能直接调用就尽量调用,别自己重复造轮子。

说点我自己踩过的坑。最开始做定点优化,我总想追求最小位宽,觉得这才叫优化。后来发现省出来那2bit,换来的是连续几天的边界调试。做工程不是做数学题,留够余量、保住稳定性,比省几个比特重要得多。现在我的习惯是先把位宽放宽,甚至用32bit累加器跑通全链路,再根据仿真结果慢慢收紧,一次只改一个参数,每一步都有数据支撑。定点这个方向,做好了完全不是“替代方案”,而是让低成本产品真正量产落地的那把钥匙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询