FPGA 入门 43:定点数基础——Q 格式、量化、截位与饱和
更新时间:2026-09-12。本文是
fpga/入门层第 45 篇。硬件基础:数据类型与运算、DSP48 初认识;应用:FIR 滤波入门;进阶:DSP48 与算术。
本文要回答的问题
- 为什么说定点数本质是「整数存储 + 设计者维护的量纲」?量纲跟踪表为什么能防止 90% 的小数点错位?
- 补码有符号数的符号扩展、算术右移、乘法位宽规则分别是什么?哪些 Verilog 写法会静默生成无符号逻辑?
- Q 格式的范围和精度如何按应用预算:控制环、音频、传感器校准、神经网络一层各自大概需要多少位?
- 量化误差的统计特性是什么?截断、就近舍入、收敛舍入的偏差和硬件成本怎么权衡?
- 加法、乘法、累加、除法/倒数、查表分别怎么定点化?为什么定点设计里除法要尽量变成乘法移位?
- 饱和逻辑的正确实现细节:保护位取几位、符号异常怎么判、饱和要不要打拍?
- 浮点 IP 在什么时候才值得用?block floating point(块浮点)解决什么问题?
读完本文,你应当能独立给一个中等复杂度算法做完整定点预算:从浮点参考模型出发,确定每个节点的 Q 格式、位宽、舍入和饱和策略,并能在仿真中逐节点对比量化误差。
一、本质:小数点只存在约定里
真实小数 x → 整数存储 N = round(x × 2^n)
运算全按整数走(加法器/DSP48) → 解读时 x ≈ N × 2^(-n)
小数点位置不占任何硬件位,只由你的 Q 格式约定决定| 浮点 float | 定点 Qm.n | |
|---|---|---|
| 硬件 | 需复杂浮点单元/IP | 直接用整数加乘(DSP48) |
| 速度/资源 | 慢、占资源 | 快、省 |
| 范围 | 自动换指数、范围大 | 固定,需自己留位 |
| 精度 | 相对精度 | 绝对 LSB=2^(−n) |
| 设计者负担 | 低 | 要管对齐/截位/溢出 |
二、Q 格式
- Qm.n:m 位整数部分、n 位小数部分(有符号补码,总位宽 m+n)。
- 真实值 = 整数值 × 2^(−n);分辨率 LSB = 2^(−n)。
- 范围:[−2^m, 2^m − 2^(−n)]。
| 格式 | 总位宽 | 约范围 | LSB 分辨率 |
|---|---|---|---|
| Q1.15 | 16 | −1.0 ~ 0.99997 | 3.05e-5 |
| Q8.8 | 16 | −128 ~ 127.996 | 3.9e-3 |
| Q3.13 | 16 | −8 ~ 7.9999 | 1.22e-4 |
权衡:先按信号最大幅值定整数位(含余量),其余位给小数换精度。
三、量化(小数 → 定点)
# Python 概念示例:量化到 Q1.15
def q15(x):
v = int(round(x * (1 << 15)))
return max(-32768, min(32767, v)) # 顺手做饱和量化误差 ≤ ±0.5 LSB;位数越少误差越大、信噪比越低。系数表、ADC 采样都要先量化。
四、运算规则与位宽增长
加减:Q 格式必须相同(不同先移位对齐);和可能 +1 位防溢出
乘法:Qm.n × Qa.b = Q(m+a).(n+b) → 16×16 = 32 位
回精度:乘积右移 n 位取高位;截位前可加 2^(n-1) 做四舍五入// 有符号 16x16 -> Q1.15:乘积 Q2.30,右移 15 回到 Q1.15
wire signed [31:0] prod = a_s * b_s; // Q2.30
wire signed [15:0] out = prod[30:15]; // 截位取高(按符号调整边界)
// 舍入版:先加 0.5 LSB
wire signed [31:0] prod_r = prod + (1 << 14);- 截断:恒向一个方向偏,多级累积产生系统误差。
- 舍入:加 2^(n−1) 再右移,误差更小、更白噪。
五、溢出与饱和
// 16 位有符号饱和:扩位加,再判断符号是否异常
wire signed [16:0] sum_ext = a_ext + b_ext; // 多 1 保护位
always @(*) begin
if (sum_ext > 17'sd32767) y = 16'sd32767; // 钳上限
else if (sum_ext < -17'sd32768) y = -16'sd32768; // 钳下限
else y = sum_ext[15:0];
end- 补码回绕会让 +满幅 突变成 −满幅,产生灾难性尖峰。
- 饱和削顶是“可接受的降级”。
- N 抽头累加器:保护位至少 ⌈log2(N)⌉ 位,或逐级饱和。
六、补码运算的硬件细节
定点有符号数用二进制补码表示,理解三条硬件规则才能写对代码。符号扩展:窄数加宽时,高位全部复制原符号位,数值不变。Verilog 中对 signed 信号直接拼接或加法会自动扩展,手写拼接线则要用 {{(W-N){a[W-1]}}, a} 这种重复符号位的形式;漏做符号扩展是负数变大正数的经典 bug。算术右移:有符号数除以 2 的幂要用算术右移(高位补符号位),Verilog 里只有右移操作数都声明为 signed 时 >>> 才做算术移位,否则是逻辑移位补零,负小数直接错位。乘法位宽:M 位有符号乘 N 位有符号,完整结果是 M+N-1 位有符号(习惯存 M+N 位,最高位是重复符号位);DSP48 的 25×18→48 正是按这个规则设计的。
哪些写法会静默退化为无符号?①端口或中间变量漏写 signed,整个表达式按无符号处理;②位选/拼接结果永远是无符号,哪怕内容来自有符号信号,要重新 $signed();③常量参与运算时写 16'hC000 是无符号十六进制数,负数常量要用 -16'sd16384 这类有符号十进制形式;④$signed 只改变解释方式,不改变比特。把这些规则在代码评审里当 checklist,能消除定点模块里最难查的一类错误——它们不报 warning,但波形里数值莫名其妙。
七、量纲跟踪表:定点设计的地图
专业的定点设计不是边写边猜,而是先列表。每个内部节点一行:节点名、物理含义、最大可能幅度(从参考模型统计)、需要的信噪比或容差、选定的 Qm.n、总位宽、来源节点、运算类型、溢出策略、舍入策略。例:FIR 输入 Q1.15、系数 Q1.15、单乘积 Q2.30(32 位)、N=64 抽头累加结果 Q8.38 需要 47 位(整数位 2+log2(64)=8),最后舍入回 Q1.15 输出。这张表直接推出三件事:加法器位宽、DSP 级联方式、饱和比较器在哪。
量纲表配合「浮点黄金参考」使用:Python/MATLAB 里先写浮点算法,再写一个逐节点量化的定点参考模型(纯软件、按你计划的位宽和舍入模拟),两个模型跑同一批数据,输出误差曲线;误差超标就回到表上调整某节点的 n 位。等软件定点模型达标,再照表写 RTL——RTL 与软件定点模型应该比特级一致,这就把「算法误差」和「编码错误」彻底分开了。没有这一步,上板后你无法分辨听到的杂音是量化太粗还是截位写错。
八、位宽预算:不同应用大概要多少位
经验范围(具体仍以误差分析为准):简单控制环(PI/PID、PWM 调制),16 位 Q 格式加几个保护位通常足够,系数 16 位、积分器比输入宽 4~8 位防长期溢出;音频处理,16 位输入、24~32 位累加是行业惯例,人耳对直流偏移和谐波极敏感,中间路径宁宽勿窄;传感器校准与简单测量,按传感器分辨率加 3~4 位裕量即可;通信收发机(滤波、同步)常见 12~16 位数据、16~18 位系数;CNN 推理,INT8 权重在大量网络上够用但训练后量化(PTQ)要统计激活分布,某些层(归一化、softmax 前)仍需 16 位;FFT,蝶形运算每级可能增长一位,大点数要按级扩位。共同原则:存储位宽看精度需求,中间运算位宽看增长数学,两者分开预算。
九、舍入方法的精度与成本对比
直接截断(truncate)零成本但引入平均为负的偏差(约 −0.5LSB),多级级联后直流偏移显著。就近舍入(round half up):截位前加 2^(n−1),即「加半 LSB 再截」,偏差均值接近零,硬件只多一个加法器,是 FIR/控制里的默认选择。收敛舍入(round half to even,银行家舍入):中间值向偶数靠,消除就近舍入在 0.5 处的统计偏置,音频高精度链路和浮点默认模式都用它,代价是要看被截位是否恰为中点,多几级比较。收敛舍入加抖动(dither):音频里还会在舍入前加伪随机低幅噪声,把量化失真从谐波变成白噪,听感更好。
选择建议:控制环和一般 DSP 用加半 LSB;多级 IIR 或高保真音频值得用收敛舍入;截位级数多时,优先保证每级舍入而不是只在输出舍一次——但舍入噪声会累加,若精度紧张应该减少截位次数、中间保持全精度(DSP48 的 48 位累加器就是为此设计)。另外注意截位和右移对负数的行为,RTL 写完要用正负两种输入对照软件模型验证。
十、除法、倒数与非线性:定点化的硬骨头
定点加法几乎免费、乘法一块 DSP、除法又慢又贵。策略是在算法层面消灭除法:固定除数转成乘倒数再移位,如除以 100 等于乘 0.01 的 Q16 定点近似(655/65536 量级,注意误差);归一化分母变化慢时,定期算一次倒数(牛顿迭代几次,每次只有乘法和移位)后全用乘法;PID 的「除以常数」全部编译期转乘法移位。必须做真除法时,用逐位恢复/不恢复除法或 Xilinx Divider IP,流水化但有十几周期延迟,且要在架构里接受。
非线性函数(sin、exp、sqrt、sigmoid)的标准做法是查表加插值:在 ROM(BRAM)里存均匀或分段非均匀采样点,相邻点线性插值,精度由表深和插值阶数决定;查表前先做范围折叠(sin 折到四分之一周期、指数拆整数加小数)。tanh/sigmoid 在神经网络量化里常用分段线性近似直接落到几个乘加。角度计算用 CORDIC:只用移位加减迭代旋转,能算 sin/cos/atan/幅度,是无乘法器时代的经典遗产,FPGA 上依然省资源。所有这些方法的共同点:用存储和乘法换掉通用运算单元,这也是定点思维与软件思维最大的不同。
十一、饱和实现的几个细节
饱和逻辑的正确姿势:①运算全程在扩展位宽下完成(guard bits 至少覆盖最坏情况,不确定时用参考模型统计真实峰值加 2 位);②比较必须在有符号扩展值上做,边界常量也要同等宽度;③饱和选择是一级组合 MUX,处在数据通路上要注意时序,大位宽比较可提前一拍做、与寄存后的结果对齐;④饱和事件应该引出一个 sticky 标志(置位后保持,直到软件清除),它是调试和精度监控的重要信号——正常工作时饱和标志必须永不翻转,翻转说明量纲表错了。
// 40 位累加结果饱和回 16 位的判断模式
wire overflow_hi = acc[39:15] != {25{1'b0}}; // 正数超上限
wire overflow_lo = acc[39:15] != {25{1'b1}}; // 负数超下限判断本质是看被截掉的高位是否全相等(全 0 或全 1),不全相等就说明值超出 16 位表示范围。音频里饱和削顶虽不致命但可闻,通信控制里饱和可能彻底破坏数据帧,因此越关键的路径越要让饱和标志参与错误统计,而不是静默削掉。
十二、块浮点与什么时候才上浮点
块浮点(Block Floating Point)是定点与浮点之间的折中:一组数据(如 FFT 的一帧、一层网络的一个通道)共享一个指数(由这组数据的最大值动态确定),组内每个元素仍是定点整数。硬件只需在组边界做归一化移位,内部全是定点乘加,却获得了浮点式的动态范围,雷达/信号处理机历史上大量采用。它适合「同组数据幅度相近」的场景,组间差异大时收益下降。
真正的 IEEE-754 浮点在 FPGA 上的成本:一个 float32 乘加大约 2~4 个 DSP 加较多 LUT、3~5 级流水(Xilinx 浮点 IP 或新版 DSP 硬核的浮点模式可大幅改善),吞吐和密度约为定点的几分之一。值得用浮点的情形:算法动态范围极大且无法预测(某些矩阵运算、科学计算)、迭代收敛对舍入敏感、团队没有时间做定点预算。数据通路一旦选定浮点,系数存储、AXI-Stream 位宽、仿真参考模型都要同步。多数学生/控制/普通 DSP 场景,学好定点的收益远大于依赖浮点 IP。
十三、一个完整定点化工作流
把全文串成可执行流程:①浮点参考模型,准备真实数据集;②统计每个节点的幅度分布和输出误差敏感度;③建量纲表,定 Q 格式与位宽;④软件写逐节点比特精确模型,验证误差达标;⑤照表写 RTL,全 signed、统一舍入和饱和、sticky 标志;⑥testbench 用同一批数据,比对 RTL 与软件定点模型比特一致,再比对浮点黄金模型误差;⑦确认乘法进 DSP、累加位宽满足增长;⑧上板后长期统计饱和标志和误差寄存器。这套流程把「玄学的算法移植」变成流水线工程,也是 FIR 入门 到进阶 定点 DSP 设计 的共同骨架。定点设计还有一个常被忽略的好处:它逼你真正理解算法。浮点编程时一个减号、一次隐式转换的错误可能被巨大的指数范围掩盖,而在定点系统里,任何量纲错误都会立刻表现为离谱的数值,这让问题无处藏身。很多算法工程师第一次做 FPGA 移植后反映,虽然写代码慢了,但对算法数值行为的理解比纯软件仿真时深得多。这种「约束带来清晰」的现象在工程中反复出现:有限的位宽强迫你回答每个信号的物理范围和精度需求,而这些问题本来就该回答,只是浮点把它们推迟了。从这个角度看,学定点不只是学一种实现技术,更是在训练一种对数值诚实的工程思维。最后把定点和整个 FPGA 知识体系接起来:它向前承接数据类型和有符号运算(见 数据类型),向后是所有 DSP 算法(FIR、FFT、控制环、神经网络)落地的必经步骤,也是算法和硬件两个岗位对话的语言——算法关心浮点指标,硬件关心位宽和资源,定点预算表就是双方的合同。把本文的量纲表、舍入、饱和、工作流完整做一遍,哪怕只用一个简单的二阶滤波器练手,也胜过读十篇讲定点原理的文章。最后送一个排查定点 bug 的口诀:数值离谱查量纲(Q 格式对不对、小数点位置);数值差一倍查移位(截位位置);时对时错查符号(signed 和符号扩展);大信号才错查溢出(保护位和饱和);小信号就偏查舍入(截断偏差)。这五类覆盖了定点代码里绝大多数错误,配合与软件定点模型逐节点比对,任何数值问题都能在很短时间内定位到具体的那一级运算。最后强调一个文档习惯:每个定点模块的 README 或头注释里都应附上量纲表——每个端口和关键中间信号的 Q 格式、位宽、允许范围、饱和策略。这份表花半小时写,却能让任何接手者(包括三个月后的你自己)不用逐行反推小数点位置。定点设计最怕「知识只存在作者脑子里」,因为一旦量纲靠口头传承,一次人员变动就会产生一批静默错误。最后把本文和面试考点对接:定点题最常见的考法是给一段 C 浮点代码,要求写出定点方案。标准答题结构是——先问信号范围和精度要求,再画量纲表说明每个中间节点的 Q 格式和位宽,然后讲乘法后的舍入和加法的保护位,最后给出与浮点模型比对误差的验证方法。按这个结构回答,哪怕没有写完整代码,面试官也能判断你真的做过定点移植,而不是只背过 Q15 的定义。再补充一个容易忽视的实践细节:系数量化和数据量化要分开评估。系数是编译期常量,可以用更高精度(比如 18 位)只增加 ROM 和乘法输入位宽;数据通路上的实时信号才受存储和带宽限制,常见做法是 16 位数据加 18 位系数、48 位累加,恰好和 DSP48 的端口宽度匹配。把系数和数据一视同仁地都压到 16 位,是新手频响指标达不到时最常见的原因之一。再讲一个增益定标的实用细节:一组滤波器或控制环里,各级增益有正有负,中间结果可能远小于或大于输入,Q 格式不应该全链路统一,而要逐级设计——输入 Q1.15、第一级输出可能 Q4.12、累加后 Q9.23。每一级都明确「这一拍之后,小数点在哪里」,并在 RTL 注释里写明,是防止定点系统变玄学的唯一办法。浮点写作者习惯让精度「自动流动」,定点工程师必须显式掌握每次移位和扩展,这种显式控制正是 FPGA 实现确定延迟和确定资源的前提。最后把定点设计里最容易混淆的「精度」和「分辨率」两个概念讲清:分辨率是 LSB 对应的最小步进,只由位数和满量程决定;精度是测量或计算值与真实值的接近程度,还受噪声、非线性和舍入误差影响。16 位定点不代表有 16 位精度,实际有效位数可能只有 12~14 位。写指标承诺和做误差分析时,这两个词必须严格区分,否则很容易在系统联调时出现「位数够了但指标达不到」的尴尬。最后给一个新手最常踩的坑:直接把浮点 C 代码逐行翻译成 Verilog,每个变量都先随便给 16 位,跑不通再逐位加宽。这种做法几乎必然陷入量纲混乱。正确顺序永远是先定点后 RTL:先在软件里用整数类型把定点模型写对、误差测准,再逐节点翻译成硬件,中间任何数值异常都能立刻定位到具体一级运算。多花在定点模型上的时间,会在联调阶段十倍地省回来。最后再点破一个思维转变:软件浮点工程师习惯问「这个变量是什么类型」,定点工程师要习惯问「这个信号在这一拍的物理范围和最小有效分辨率是多少」。前者是语言层面的声明,后者是物理量层面的契约。当你开始用范围和分辨率而不是类型名思考信号,定点设计就从一堆移位规则变成了自然的建模活动,这也是从「会写定点代码」到「会做数值架构」的关键一跃。掌握本文的量纲表、舍入饱和规则和「软件定点模型先行」的工作流,任何需要把模拟世界搬进 FPGA 的任务——滤波、控制、推理、测量——都有了同一套可复用的方法骨架。沿着这个骨架,你可以把任何物理量传感器的数据稳定地搬进数字系统,这正是大量工业与测量类 FPGA 岗位的核心工作内容。掌握了这套方法,再复杂的算法移植,本质上都变成了「量纲先行、模型验证、定点落地、误差回归」四步可管理的工程活动。
十四、浮点到定点的迁移检查单
把一个浮点算法搬上 FPGA 时,建议按下面的检查单逐项走,它本质上是把「软件能跑」和「硬件数值正确」之间的差距显式化。第一类是量纲问题:每个输入、系数、状态变量和中间节点是否都有明确的 Q 格式,加减两端的小数点是否对齐,负反馈和减法的符号扩展是否到位,任何一处对不齐都会以「数值离谱但不报错」的形式出现。第二类是增长问题:每一级加法器的位宽是否按最坏情况增长,累加器有没有保护位,乘法后是取全精度还是立即截位,截位发生在几级运算上。第三类是边界问题:系数全为负、输入满幅正负切换、长时间零输入后突然满幅这三种极端激励下是否饱和、是否丢符号,随机小信号长时间运行后舍入偏差是否累积到可观测。第四类是接口问题:模块输出的 Q 格式、饱和标志、有效信号是否在文档中写明,下游能不能不看源码就正确使用。
检查单的每一项都应该能在 testbench 里找到对应的定向测试,而不是停留在纸面。一个实用做法是让软件定点模型和 RTL 共用同一份测试向量文件,仿真结束后逐样本 diff,任何一比特不一致都报错。这样当你以后重构数据通路、换一种舍入方式或压缩位宽时,立刻能知道改动破坏了哪个节点。定点系统最危险的不是写不出代码,而是代码看起来能跑、数值却在边界条件上悄悄出错;检查单加比特级比对,就是把这类静默错误挡在联调之前的两道闸门。
十五、典型应用的位宽经验与误差预算
给一组入门阶段可以直接参考的经验值,但要记住它们是起点不是教条。通用控制环路(电机、电源、温度)的数据通路 16 位、系数 16 到 18 位、累加器 32 位通常足够,重点误差来源是反馈通道的采样噪声而不是量化噪声。音频处理追求约 96 分贝动态范围,数据 24 位、内部运算 32 位、滤波器系数 18 位以上,舍入必须用收敛偶舍或带补偿的舍入,否则 dither 不够时小信号会产生可闻的周期噪声。传感器测量和仪表类应用先按测量精度反推:要求万分之一的相对精度,有效位数至少 14 位,数据通路要留两到三位保护位,即 16 到 18 位起步。通信解调里的相关器和累加器是增长大户,相关长度上千点时累加宽度要按对数增长逐位算清楚,省一位就可能在弱信号下整片丢失相关峰。
误差预算的正确分配方式是从系统指标倒推:先确定输出允许的总误差(比如相对满幅千分之一),再把它切成系数量化误差、中间运算舍入误差、饱和削顶误差、输入本身的量化噪声四块,各留一定比例后才开始定点化。新手常犯的错误是所有节点统一 16 位,结果误差预算被某一级的粗糙截位吃光,其他级的精度全浪费了。合理的预算像时间预算一样,应该花在误差最敏感的节点上:对输出影响大的累加和反馈路径保留全精度,影响小的末梢节点大胆截位。做完后用浮点模型做一次「逐节点加噪」实验——在每个节点单独注入该级预期量化噪声,观察输出变化,就能定量知道哪一级值得多花几位,这比凭感觉加宽位宽科学得多。
常见坑对照
| 坑 | 后果 | 建议 |
|---|---|---|
| 小数位/整数位不匹配就相加 | 结果错位 | 先移位对齐 Q |
| 乘法不截位直接取低 16 位 | 数值完全错 | 取高位、右移 n |
| 只截断不舍入 | 累积直流偏差 | 加 2^(n−1) 舍入 |
| 加法不防溢出 | 正负回绕尖峰 | 扩位 + 饱和 |
| 无符号/有符号混用 | 符号错误 | 统一 signed、补码 |
| 整数位留太少 | 大信号削底 | 按幅值+余量定 m |
相关与延伸
硬件:数据类型与运算、DSP48 初认识;实战:FIR 滤波入门。 深入:进阶 DSP48 与算术(乘加/FIR/推断/级联)。
一句话总结
FPGA 里的定点数就是“把小数乘 2^n 取整当整数存、小数点位置只存在 Q 格式约定里”,从而直接用整数加法器和 DSP48 乘法器高速省资源地运算,代价是要自己管范围、精度、对齐和溢出。Qm.n 表示 m 位整数、n 位小数(有符号补码),真实值=整数×2^(−n),分辨率 LSB=2^(−n),范围 [−2^m, 2^m−2^(−n)],如 Q1.15 为 16 位表示约 ±1、分辨率 3.05e-5——总位宽固定时范围与精度需权衡。量化误差最大 ±0.5LSB。运算上加减必须 Q 格式相同(先移位对齐、结果可能扩 1 位),乘法 Qm.n×Qa.b=Q(m+a).(n+b) 使位宽翻倍(16×16→32),需右移取高位回到目标 Q 格式,并优先“加 0.5LSB 再截”的舍入以减小累积偏差;加法和 N 抽头累加必须扩保护位并做饱和(钳到 ±满幅),否则补码回绕会产生正负翻转的灾难性尖峰。先保证不溢出、再谈截位保精度,是 FIR/控制/神经网络等所有定点 DSP 设计的基本准则。
上一篇:45 FPGA 供电与散热(电源轨/上电顺序/功耗) | 下一篇:47 FIR 滤波器入门(抽头延迟线/乘加/DSP48) | FPGA 系列目录