FPGA 定点数、小数与负数的加减乘除原理
FPGA 定点数、小数与负数的加减乘除原理面向 Verilog/SystemVerilog、Vivado 和 FPGA DSP 硬件实现的定点数入门与工程实践文档。重点讨论二进制小数、补码负数、加法、减法、乘法、除法、位宽、溢出、舍入、饱和以及 DSP48 推断。1. 为什么 FPGA 中通常使用定点数FPGA 中表示实数有两种主要方式浮点数定点数浮点数能够表示很大的动态范围但硬件代价较高通常需要浮点加法器、乘法器、规格化和舍入逻辑。定点数把小数点的位置固定下来。例如整数 7 定点数 7.25在硬件中二者本质上都是一组二进制位。区别只是如何解释这些位以及小数点位于哪里。因此定点数的乘加、滤波、CORDIC、NCO 和 ADC 数据处理通常可以使用加法器减法器移位器DSP 乘法器BRAM 查找表比较器而不需要完整的浮点运算单元。2. 定点数的基本表示方法2.1 Q 格式定点数常用Qm.n表示m整数部分位数n小数部分位数总位宽通常为m n如果采用有符号补码有时还需要单独考虑符号位工程中不同资料对Qm.n的符号位定义可能略有不同。为了避免歧义本文统一采用S(W,F)表示一个总宽度为W位、其中F位为小数位的有符号补码定点数。例如S(16,12)表示总宽度16 位小数位12 位整数加符号区域4 位缩放因子2^12 4096对于原始补码整数raw真实值为real_value raw / 2^F反过来真实值转换为定点原始码raw round(real_value × 2^F)2.2 定点小数点位置以S(8,4)为例位号 7 6 5 4 . 3 2 1 0 权重 -8 4 2 1 . 1/2 1/4 1/8 1/16这里最高位 bit[7] 是补码符号位bit[6:4] 表示整数部分bit[3:0] 表示小数部分最小分辨率为2^-4 1/16其数值范围为最小值 -2^(8-4-1) -8 最大值 2^(8-4-1) - 2^-4 7.9375所以S(8,4)的范围是[-8, 7.9375]2.3 常见定点格式格式总位宽小数位分辨率大致范围S(8,4)840.0625-8 7.9375S(16,8)1680.00390625-128 127.996S(16,12)16120.00024414-8 7.9998S(18,16)18160.00001526-2 1.99998S(24,20)24200.0000009537-8 7.999999格式选择的核心是平衡动态范围 ↔ 精度 ↔ 资源 ↔ 时序3. 补码负数原理FPGA 中的有符号定点数通常使用二进制补码表示。3.1 正数正数直接使用二进制表示。例如在S(8,4)中3.25 × 16 52 52 0011_0100因此3.25 的定点码 0011_01003.2 负数负数的补码计算步骤取正数的二进制按位取反加 1例如-2.52.5 × 16 40 40 0010_1000取反1101_0111加 11101_1000因此-2.5 的 S(8,4) 定点码 1101_1000将其看作有符号整数1101_1000 -40恢复真实值-40 / 16 -2.53.3 补码的取值范围对于总宽度为W的有符号补码整数[-2^(W-1), 2^(W-1)-1]对于S(W,F)定点数最小值 -2^(W-F-1) 最大值 2^(W-F-1) - 2^-F4. Verilog 中正确声明有符号数4.1 推荐写法logic signed [15:0] a; logic signed [15:0] b; logic signed [16:0] sum;或者reg signed [15:0] a; wire signed [15:0] b;关键字signed非常重要。4.2 常见错误logic [15:0] a; logic [15:0] b; logic signed [16:0] sum; assign sum a b;这里a和b被声明为无符号数。即使sum是有符号数表达式a b也可能先按照无符号方式计算导致负数运算错误。应改为logic signed [15:0] a; logic signed [15:0] b; logic signed [16:0] sum; assign sum $signed(a) $signed(b);4.3 拼接会丢失 signed 属性下面的拼接结果通常被视为无符号assign y {a[15], a};更稳妥的写法是assign y $signed({a[15], a});或者直接使用符号扩展assign y {{1{a[15]}}, a};并在表达式中显式使用assign y $signed({{1{a[15]}}, a});5. 定点数加法原理5.1 加法的前提小数位必须相同两个定点数只有在小数位数相同时才能直接相加。设A A_raw / 2^F B B_raw / 2^F那么A B (A_raw B_raw) / 2^F因此硬件中只需对原始码做整数加法sum_raw A_raw B_raw小数点位置保持不变。5.2 示例定点加法设A 1.25 B 2.50 格式S(8,4)转换为原始码A_raw 1.25 × 16 20 B_raw 2.50 × 16 40二进制A_raw 0001_0100 B_raw 0010_1000相加0001_0100 0010_1000 ------------ 0011_1100十进制60 / 16 3.75所以1.25 2.50 3.755.3 加法位宽两个W位有符号数相加建议使用W1位保存结果localparam int W 16; localparam int F 12; logic signed [W-1:0] a; logic signed [W-1:0] b; logic signed [W:0] sum_ext; assign sum_ext $signed(a) $signed(b);为什么需要增加一位例如无符号数7 1 8如果只有 3 位111 001 000结果溢出。有符号补码也存在同样问题只是溢出的判断规则不同。5.4 有符号加法溢出判断对于两个有符号数a b y发生溢出的条件是a 和 b 符号相同但 y 的符号与它们不同Veriloglogic signed [15:0] a; logic signed [15:0] b; logic signed [15:0] y; logic overflow_add; assign overflow_add (~(a[15] ^ b[15])) (y[15] ^ a[15]);更安全的工程方法是先扩展到W1位再根据最高两位判断是否溢出根据需要进行饱和处理6. 不同小数位数的加法如果两个操作数的小数位不同需要先对齐小数点。设A 为 S(WA, FA) B 为 S(WB, FB)如果FA FB则将B_raw左移B_aligned B_raw (FA - FB)如果FB FA则将A_raw左移A_aligned A_raw (FB - FA)然后使用较大的小数位数作为结果的小数位。6.1 示例A 1.25格式 S(8,4) B 2.5格式 S(8,2)原始码A_raw 1.25 × 16 20 B_raw 2.5 × 4 10统一到 4 个小数位B_aligned 10 2 40相加20 40 60 60 / 16 3.756.2 硬件实现logic signed [7:0] a_s4; logic signed [7:0] b_s2; logic signed [9:0] a_ext; logic signed [9:0] b_ext; logic signed [9:0] sum_ext; assign a_ext {{2{a_s4[7]}}, a_s4}; assign b_ext $signed({{2{b_s2[7]}}, b_s2}) 2; assign sum_ext a_ext b_ext;注意扩展前必须保留符号对有符号数移位时要使用算术左移/右移语义实际工程中应根据最大范围进一步增加保护位7. 定点数减法原理减法本质上是加上被减数的补码A - B A (-B)因此硬件通常使用加法器完成减法。7.1 示例正数减正数A 3.5 B 1.25 格式S(8,4)原始码A_raw 3.5 × 16 56 B_raw 1.25 × 16 20相减56 - 20 36 36 / 16 2.25结果3.5 - 1.25 2.257.2 示例正数减负数A 1.5 B -2.25则A - B 1.5 - (-2.25) 1.5 2.25 3.75以S(8,4)表示A_raw 1.5 × 16 24 B_raw -2.25 × 16 -36硬件计算24 - (-36) 60 60 / 16 3.757.3 减法位宽和溢出减法也建议增加一位logic signed [15:0] a; logic signed [15:0] b; logic signed [16:0] diff_ext; assign diff_ext $signed(a) - $signed(b);例如最大值 - 最小值可能超出原始位宽因此必须进行截断饱和或扩大输出位宽8. 定点数乘法原理乘法是定点计算中最容易出现小数位错误的地方。设A A_raw / 2^FA B B_raw / 2^FB则A × B (A_raw × B_raw) / 2^(FAFB)因此两个定点数相乘后小数位数等于两个输入小数位数之和。8.1 相同格式相乘两个S(W,F)数相乘A × B乘积原始位宽为2W乘积小数位为2F结果格式S(2W, 2F)8.2 示例乘法A 1.5 B -0.75 格式S(8,4)原始码A_raw 1.5 × 16 24 B_raw -0.75 × 16 -12整数乘法P_raw 24 × (-12) -288乘积有 8 个小数位P -288 / 256 -1.125因此1.5 × (-0.75) -1.1258.3 SystemVerilog 乘法示例localparam int W 16; localparam int F 12; logic signed [W-1:0] a; logic signed [W-1:0] b; logic signed [(2*W)-1:0] product_full; assign product_full $signed(a) * $signed(b);这里product_full 格式S(32,24)如果希望恢复成与输入相同的小数位F12需要右移F位logic signed [W-1:0] product_scaled; assign product_scaled product_full F;原因是S(16,12) × S(16,12) S(32,24)右移 12 位后S(32,12)再根据需要截断或饱和到S(16,12)。8.4 乘法后的位宽处理完整乘积不应直接粗暴截断。推荐流程输入扩展 ↓ 完整乘法 ↓ 舍入 ↓ 按小数位右移 ↓ 溢出判断 ↓ 饱和或截断 ↓ 输出例如logic signed [31:0] product_full; logic signed [31:0] product_round; logic signed [31:0] product_shifted; assign product_full $signed(a) * $signed(b); assign product_round product_full (32sd1 (F-1)); assign product_shifted product_round F;上面的简单舍入方式只适用于正数。对于负数工程中应明确选择向零舍入向下舍入四舍五入收敛舍入对称舍入不能默认所有舍入方式都对正负数完全对称。9. 定点数除法原理除法的核心是先扩大被除数使结果保留所需的小数位。设A A_raw / 2^FA B B_raw / 2^FB希望结果采用FR个小数位Y A / B推导Y (A_raw / 2^FA) / (B_raw / 2^FB) A_raw × 2^FB / (B_raw × 2^FA)如果直接得到结果原始码Y_raw要求Y Y_raw / 2^FR因此Y_raw (A_raw (FB FR - FA)) / B_raw这就是通用定点除法公式。9.1 同格式除法当FA FB F并且希望结果仍然有F个小数位时Y_raw (A_raw F) / B_raw9.2 示例除法A 3.0 B 1.5 输入格式S(16,8) 输出小数位8原始码A_raw 3.0 × 256 768 B_raw 1.5 × 256 384计算Y_raw (768 8) / 384 196608 / 384 512恢复真实值512 / 256 2.0所以3.0 / 1.5 2.09.3 除法的硬件代价FPGA 中直接使用/可能推断出较大的组合除法器导致LUT 使用量大组合路径很长Fmax 降低功耗增加时序难以收敛常见替代方案除以 2 的幂使用算术右移常数除法乘以倒数再右移变量除法使用迭代除法器高吞吐场景使用流水线除法器非线性计算使用 LUT、CORDIC 或 Newton-Raphson低速控制路径允许多周期计算9.4 除以 2 的幂对于有符号数Y X / 2^N可以使用算术右移assign y x N;注意是算术右移左侧需要是 signed。如果x未声明为 signed可能发生逻辑右移负数高位补零结果错误。9.5 除法中的截断问题整数除法会丢弃余数。例如7 / 3 2 余 1对于定点数直接截断会产生量化误差。可以通过增加舍入项改善quotient (numerator denominator/2) / denominator但是对于负数需要明确舍入规则不能简单地无条件加上denominator/2。9.6 除零处理硬件必须明确处理除数为零的情况。常见策略除数为 0 输出最大正数 或输出最大负数 或输出 0 或拉高 divide_by_zero 标志示例always_comb begin if (b 0) begin y 0; divide_by_zero 1b1; end else begin y numerator / b; divide_by_zero 1b0; end end在控制系统和信号处理系统中建议同时输出结果值 异常标志而不是静默地产生一个看似正常的数。10. 定点数的符号扩展与零扩展10.1 有符号数扩展有符号补码扩展时复制符号位正数高位补 0 负数高位补 1例如8b0001_1000 → 12b0000_0001_1000 8b1110_1000 → 12b1111_1110_1000SystemVerilogassign y {{4{x[7]}}, x};10.2 无符号数扩展无符号数扩展时高位补 0assign y {4b0, x};10.3 定点小数扩展如果只是增加小数位应该在右侧补零相当于左移S(W,F) → S(WN,FN)例如1.5 的 S(8,4) 原始码 24转换到 6 个小数位24 2 96 96 / 64 1.511. 截断、舍入和饱和11.1 截断截断是直接丢弃低位assign y x[MSB -: OUT_W];优点逻辑简单资源少时序容易缺点有量化误差可能产生直流偏置信号处理中的噪声性能较差11.2 舍入常见的正数四舍五入保留高位 被丢弃部分最高位例如保留N位rounded (x 2^(N-1)) NSystemVerilog 示例logic signed [31:0] x; logic signed [31:0] x_round; assign x_round (x (32sd1 (SHIFT-1))) SHIFT;但对于有符号数应根据项目要求采用明确的对称舍入策略尤其是音频、通信和高精度滤波场景。11.3 饱和如果结果超出可表示范围饱和处理会将其限制在最大值或最小值超过最大值 → 最大值 低于最小值 → 最小值 范围内 → 原值以S(8,4)为例最大值 7.9375 最小值 -8.0如果结果为9.2 → 7.9375 -10.0 → -8.0饱和比直接截断更适合音频ADC/DAC控制算法图像像素通信基带FIR 输出11.4 饱和逻辑示例下面示例将较宽的S(17,12)结果压缩为S(16,12)module sat_s16_f12 ( input logic signed [16:0] din, output logic signed [15:0] dout, output logic overflow ); localparam logic signed [15:0] MAX_VAL 16sh7fff; localparam logic signed [15:0] MIN_VAL 16sh8000; always_comb begin if (din 17sh07fff) begin dout MAX_VAL; overflow 1b1; end else if (din -17sh08000) begin dout MIN_VAL; overflow 1b1; end else begin dout din[15:0]; overflow 1b0; end end endmodule实际工程中建议使用参数化模块避免手写常量造成位宽错误。12. 定点运算总公式设A总宽度 WA小数位 FA B总宽度 WB小数位 FB12.1 加法前提FA FB结果Fresult FA原始码Y_raw A_raw B_raw建议结果位宽Wresult max(WA, WB) 112.2 减法前提FA FB结果Fresult FA原始码Y_raw A_raw - B_raw建议结果位宽Wresult max(WA, WB) 112.3 乘法结果Fresult FA FB原始码Y_raw A_raw × B_raw完整结果位宽Wresult WA WB12.4 除法希望输出有FR个小数位Y_raw (A_raw (FB FR - FA)) / B_raw同格式时Y_raw (A_raw F) / B_raw13. FPGA 中的乘法与 DSP48 推断对于 Xilinx 7 Series乘法和乘加通常应尽量映射到 DSP48E1而不是全部使用 LUT。推荐代码结构module fixed_mult #( parameter int A_W 16, parameter int B_W 16, parameter int F 12 ) ( input logic clk, input logic signed [A_W-1:0] a, input logic signed [B_W-1:0] b, output logic signed [A_WB_W-1:0] p ); logic signed [A_W-1:0] a_r; logic signed [B_W-1:0] b_r; logic signed [A_WB_W-1:0] p_r; always_ff (posedge clk) begin a_r a; b_r b; p_r a_r * b_r; end assign p p_r; endmodule工程上需要关注输入是否为signed乘法两侧位宽是否明确是否使用了异步复位是否存在复杂 enable是否把乘法、加法和输出压在一个时钟周期是否使用 DSP48 内部寄存器是否需要在乘法后增加流水线典型的高性能结构是输入寄存器 ↓ DSP 乘法器 ↓ 乘积寄存器 ↓ 加法/累加器 ↓ 输出寄存器DSP48 外挂寄存器未被吸收或乘加路径未分级时可能出现明显时序违例。应结合综合报告检查 AREG、BREG、MREG 和 PREG 的使用情况并用固定点模型验证延迟和数值误差。14. FPGA 中的除法实现方式14.1 常数除法如果除数是常数可以将除法改写为乘法x / C ≈ x × (1/C)例如x / 10 ≈ x × 0.1将0.1转换为定点常数后使用 DSP 乘法器再进行右移。14.2 除以 2 的幂x / 2^N x N这是最简单、最快的除法形式。14.3 迭代除法器变量除数可使用restoring divisionnon-restoring divisionradix-2 divisionradix-4 divisionSRT division特点资源较少延迟较长适合低吞吐率场景14.4 流水线除法器适用于每拍都需要处理一个数据的场景输入一组数据 → 经过多个时钟周期 → 每拍输出一组结果特点吞吐率高延迟固定资源较多需要严格对齐 valid、tag 和数据14.5 CORDIC 或倒数迭代对于1 / x可以先求倒数再乘法a / b a × (1/b)倒数可以使用LUTCORDICNewton-RaphsonGoldschmidt适用于高吞吐率和高精度场景但需要额外的范围归一化和误差分析。15. 定点乘加中的小数位管理FIR 和 MAC 中经常出现y x0*h0 x1*h1 x2*h2 ...如果输入和系数分别为xS(Wx,Fx) hS(Wh,Fh)每个乘积的格式为S(WxWh, FxFh)所有乘积的小数位相同可以直接相加。但是累加时必须增加保护位。如果有N项相加理论上至少需要增加ceil(log2(N))个保护位。例如16 项乘积累加至少建议增加ceil(log2(16)) 4位保护位。实际设计还应考虑输入最大幅度系数绝对值之和信号峰值是否允许过载是否使用缩放系数是否在每一级进行饱和16. 一个完整的 Q 格式计算例子设x 1.25 h -0.5 格式S(16,12)16.1 转换为原始码x_raw 1.25 × 4096 5120 h_raw -0.5 × 4096 -204816.2 原始整数相乘p_raw 5120 × (-2048) -1048576016.3 乘积格式输入都是S(16,12)乘积格式 S(32,24)16.4 恢复到 12 个小数位y_raw p_raw 12 -256016.5 还原真实值y -2560 / 4096 -0.625验证1.25 × (-0.5) -0.62517. 位宽设计建议17.1 加法器W_out max(W_a, W_b) 1如果最终必须回到原位宽应增加溢出检测饱和逻辑或明确的截断规则17.2 乘法器完整乘法W_product W_a W_b不要只保留输出所需的低位除非已经明确分析过符号位小数位量化噪声溢出范围17.3 累加器对于N项相加W_acc ≈ W_product ceil(log2(N))如果数据可能同向叠加建议再增加额外保护位。17.4 除法器除法前应增加分子位宽numerator A_raw scaling_bits其中scaling_bits FB FR - FA否则结果的小数精度会不足。18. 负数右移的注意事项对有符号补码数-3 1通常得到-2这是算术右移负数高位补 1。但数学上的-3 / 2 -1.5如果输出仍为整数硬件必须定义舍入方向向负无穷取整-2向零取整-1四舍五入可能为-2对称舍入根据余数决定因此并不自动等价于你想要的数学除法规则。19. Verilog 定点运算模板19.1 定点加法module fixed_add #( parameter int W 16 ) ( input logic signed [W-1:0] a, input logic signed [W-1:0] b, output logic signed [W:0] y ); always_comb begin y $signed(a) $signed(b); end endmodule19.2 定点乘法并恢复小数位module fixed_mult_scaled #( parameter int W 16, parameter int F 12 ) ( input logic signed [W-1:0] a, input logic signed [W-1:0] b, output logic signed [W-1:0] y ); logic signed [(2*W)-1:0] product_full; logic signed [(2*W)-1:0] product_scaled; always_comb begin product_full $signed(a) * $signed(b); product_scaled product_full F; y product_scaled[W-1:0]; end endmodule注意此版本没有饱和处理仅适合作为基础演示。工程版本应增加舍入、溢出检测和饱和。19.3 同格式定点除法module fixed_div #( parameter int W 16, parameter int F 12 ) ( input logic signed [W-1:0] a, input logic signed [W-1:0] b, output logic signed [W-1:0] y, output logic div_zero ); logic signed [(2*W)-1:0] numerator; logic signed [(2*W)-1:0] quotient; always_comb begin if (b 0) begin numerator 0; quotient 0; y 0; div_zero 1b1; end else begin numerator $signed(a) F; quotient numerator / $signed(b); y quotient[W-1:0]; div_zero 1b0; end end endmodule该写法适合说明原理。对于高速数据通路建议使用专用流水线除法器或将除法转换为乘以倒数。20. Python 定点模型硬件设计前建议先建立 Python 参考模型。fromdataclassesimportdataclassdataclassclassFixedFormat:width:intfrac:intpropertydefscale(self):return1self.fracpropertydefraw_min(self):return-(1(self.width-1))propertydefraw_max(self):return(1(self.width-1))-1defencode(self,value:float)-int:rawround(value*self.scale)returnmax(self.raw_min,min(self.raw_max,raw))defdecode(self,raw:int)-float:returnraw/self.scaledeffixed_add(a_raw:int,b_raw:int)-int:returna_rawb_rawdeffixed_sub(a_raw:int,b_raw:int)-int:returna_raw-b_rawdeffixed_mul(a_raw:int,b_raw:int,frac:int)-int:producta_raw*b_rawreturnproductfracdeffixed_div(a_raw:int,b_raw:int,frac:int)-int:ifb_raw0:raiseZeroDivisionError(fixed-point division by zero)numeratora_rawfracreturnint(numerator/b_raw)fmtFixedFormat(width16,frac12)afmt.encode(1.25)bfmt.encode(-0.5)sum_rawfixed_add(a,b)mul_rawfixed_mul(a,b,fmt.frac)print(a ,fmt.decode(a))print(b ,fmt.decode(b))print(ab ,fmt.decode(sum_raw))print(a*b ,fmt.decode(mul_raw))Python 模型可用于生成 testbench 激励计算 golden reference对比 RTL 输出统计最大误差验证溢出和饱和评估不同位宽的资源/精度折中21. 定点误差的主要来源定点误差通常来自以下几个方面21.1 量化误差真实值无法精确表示为有限小数位0.1 × 256 25.6只能选择25 或 26因此会产生量化误差。21.2 截断误差乘法、除法和移位后丢弃低位会产生误差。21.3 溢出误差结果超过表示范围后直接截断可能回绕饱和会限制到边界过载可能产生严重非线性失真21.4 系数误差滤波器系数、CORDIC 常数、NCO 增量等也需要量化。21.5 累加误差多级累加会放大量化噪声舍入误差过载风险22. 推荐的定点设计流程1. 确定输入信号的最大幅度 2. 确定允许的最小分辨率 3. 选择整数位和小数位 4. 推导每个运算节点的位宽 5. 计算乘法后的总小数位 6. 为累加器增加保护位 7. 明确截断、舍入和饱和规则 8. 建立 Python/Matlab 参考模型 9. 编写 RTL 和 testbench 10. 对比 RTL 与参考模型 11. 检查综合后的 DSP/LUT 映射 12. 检查时序、资源和数值误差 13. 使用边界值、随机值和负数回归测试23. 常见错误清单错误 1忘记声明 signedlogic [15:0] a;如果a表示负数这是错误或高风险写法。错误 2乘法后忘记右移S(16,12) × S(16,12)乘积有 24 个小数位。如果要恢复为 12 个小数位必须右移 12 位。错误 3加法前没有对齐小数点不同F值的数据不能直接相加。错误 4截断前没有分析符号位随意选择位段可能破坏符号和数值比例。错误 5累加器没有保护位多项累加很容易溢出。错误 6除法没有扩大分子直接做y a / b;通常不能得到期望的小数精度。错误 7负数使用逻辑右移x N如果x未被正确声明为 signed负数高位可能补零。错误 8没有定义除零行为硬件必须明确除数为 0 时输出什么 是否产生异常标志错误 9只看仿真数值不看综合结构定点运算正确不代表硬件实现合理还必须检查DSP 是否被推断LUT 是否过多乘加路径是否过长内部寄存器是否使用是否满足 Fmax24. 总结FPGA 定点数的本质是有符号整数 固定的小数点解释规则最重要的四条规则是加法小数位相同原始码直接相加减法小数位相同原始码直接相减乘法原始码相乘小数位数相加若要恢复到原来的小数位乘积算术右移输入小数位数除法若结果需要FR个小数位Y_raw (A_raw (FB FR - FA)) / B_raw在实际 FPGA 设计中还必须同时考虑signed 位宽 符号扩展 小数点对齐 保护位 截断 舍入 饱和 除零 DSP 推断 流水线 时序收敛定点数不是简单地“把浮点数乘一个常数”而是一套贯穿整个数据通路的数值格式设计方法。每一个模块都应明确记录输入格式 输出格式 小数位位置 最大值 最小值 溢出行为 舍入方式 延迟周期只有这样定点算法才能从仿真模型可靠地落地到 FPGA 硬件。