ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA定点数位宽优化与截取策略:从算法到RTL的工程实践

FPGA定点数位宽优化与截取策略:从算法到RTL的工程实践 1. 定点数运算的底层逻辑与位宽问题根源1.1 为什么FPGA里绕不开定点数做FPGA开发的人迟早会撞上定点数这个坎。你在MATLAB或者Python里跑算法默认都是双精度浮点数据范围大、精度高写起来毫无顾虑。但一旦要把算法搬到FPGA上情况就完全变了。FPGA内部的DSP Slice、寄存器、Block RAM这些硬资源都是按位宽来消耗的浮点运算单元要么根本没有要么代价高得离谱。所以绝大多数FPGA项目里算法最终都要转成定点数实现。定点数的本质其实很简单用固定的小数点位置来表示实数。比如Q格式Q1.15表示1位符号位加15位小数位总共16位能表示的范围是[-1, 1-2^-15]精度是2^-15。这个表示方法在FPGA里极其高效因为加法和乘法都可以直接用整数运算单元来完成不需要额外的指数对齐逻辑。但问题也随之而来位宽到底选多少选少了精度不够算法性能下降甚至发散选多了资源浪费时序跑不上去功耗也上去了。更麻烦的是在运算过程中间位宽会不断增长——两个16位数相乘得到32位累加N次又需要额外的log2(N)位。如果每一级都保留完整位宽资源会迅速膨胀到不可接受的程度。这就引出了位宽优化和截取策略这个核心话题。1.2 位宽膨胀的数学本质要搞清楚位宽优化先得弄明白位宽是怎么膨胀的。我拿最常见的乘累加运算来举例。假设你有两个定点数分别是Q1.15格式的a和b位宽都是16位。它们相乘的结果数学上需要32位才能无损表示——因为1位符号位乘1位符号位还是1位符号位但15位小数乘15位小数变成30位小数加上符号位总共31位实际实现中通常取32位对齐。如果你要做N次这样的乘累加每次结果相加位宽还要再增长。N次累加最坏情况下需要额外log2(N)位来防止溢出。举个例子N256时log2(256)8也就是说32位的结果加上8位总共需要40位才能保证任何情况下都不溢出。但实际中你的数据可能根本不会同时达到最大值所以40位里有很多位是浪费的。这就是位宽优化的切入点如何在保证算法性能的前提下尽可能压缩位宽。这里有个经验公式可以参考对于N次累加如果输入数据是随机分布的实际需要的额外位宽大约是log2(N)/2。但这个公式不能盲信具体还要看你的数据分布特征。我在实际项目中一般会先用仿真跑一遍完整数据统计中间结果的实际最大值再据此确定位宽这样比纯理论推算靠谱得多。1.3 截取策略为什么这么关键位宽优化的另一半是截取策略。当你确定了中间结果的位宽从高位宽截到低位宽时怎么截就很有讲究了。最粗暴的做法是直接截断低位也就是truncation。这种做法实现最简单综合出来就是几根线的事但引入的误差是有偏的——它总是朝着负无穷方向偏。如果你的算法对直流偏置敏感比如滤波器或者累加器这种偏差会不断累积最终导致输出严重偏离预期。稍微好一点的做法是四舍五入也就是rounding。它在截断之前先加上半个LSB然后再截断。这样误差的期望值接近零不会产生累积偏差。但代价是多了一个加法器而且加法本身可能产生进位溢出需要额外处理。还有一种做法是收敛舍入也叫round half to even。它在两个可表示值正中间时选择偶数那个避免统计上的偏差。这种策略在音频处理、图像处理里用得比较多因为人眼和人耳对周期性偏差比较敏感。选择哪种截取策略取决于你的应用场景。控制类算法通常对偏差敏感建议用四舍五入图像处理类算法对单次误差不敏感但对累积误差敏感也要用四舍五入或者收敛舍入而一些对资源极度敏感的场合如果算法本身有反馈回路能自动纠正偏差截断也不是不能用。2. 位宽优化的核心方法论2.1 从算法层面做位宽预算位宽优化不是一上来就调代码而是要从算法层面先做预算。我一般的做法是分三步走。第一步确定输入数据的动态范围。这个不能拍脑袋要拿实际数据或者合理的仿真数据来统计。比如你做音频处理输入是16位ADC采样那动态范围就是明确的。但如果你做的是通信基带处理信号经过滤波、混频之后动态范围可能变化很大就需要在算法链路的每个节点都做统计。第二步逐级推算位宽需求。从输入开始每经过一级运算根据运算类型推算位宽增长。加法增长1位乘法增长到两个操作数位宽之和累加增长log2(N)位。把整条链路的位宽需求都列出来形成一个位宽预算表。第三步根据系统资源约束做取舍。如果你的FPGA资源充裕那就按预算表来保证精度。如果资源紧张就要在某些节点做位宽压缩压缩的原则是对最终输出影响小的节点优先压缩有反馈回路的节点谨慎压缩靠近输入的节点尽量保留精度。这个预算表看起来麻烦但实际做起来很快而且能帮你避免后期反复调试的痛苦。我见过太多项目一开始不做预算写到一半发现资源不够回头改位宽结果整个算法性能全变了又得重新调参数浪费的时间远超做预算的时间。2.2 中间结果的位宽压缩技巧在实际写RTL的时候中间结果的位宽压缩有几个常用技巧。第一个技巧是利用数据的实际分布。理论最大位宽是按最坏情况算的但实际数据往往远达不到最坏情况。比如一个累加器理论上N次累加需要log2(N)位额外位宽但如果你的输入数据有正有负实际累加结果的绝对值远小于理论最大值。这时候可以用仿真统计实际最大值然后留一定的余量来确定位宽。第二个技巧是分段处理。如果一个运算链路很长可以把它分成几段每段结束后做一次位宽压缩。这样虽然每段内部保留了完整位宽但段与段之间的位宽被压下来了整体资源消耗反而更优。分段的位置要选在数据动态范围较小的节点这样压缩带来的精度损失最小。第三个技巧是利用饱和运算代替溢出回绕。在某些场合如果数据超出范围与其让它回绕产生完全错误的结果不如让它饱和在最大值或最小值。饱和运算虽然也引入了误差但误差是有界的不会产生灾难性的错误。而且饱和逻辑比回绕逻辑更容易做时序优化。2.3 位宽优化中的常见误区做了这么多年FPGA我见过不少位宽优化中的误区这里列几个典型的。误区一位宽越大精度越高。这句话在数学上没错但在FPGA实现中位宽增大意味着资源消耗增加、时序变差、功耗上升。而且很多时候算法本身的数值精度瓶颈不在位宽上而在算法结构上。你盲目加位宽可能只是浪费资源性能并没有提升。误区二所有中间结果都用统一位宽。有些开发者图省事整条链路都用32位觉得这样肯定没问题。但实际上不同节点的位宽需求差异很大统一位宽要么在某些节点浪费要么在某些节点不够。精细化的位宽管理才是正道。误区三忽略截取策略的影响。很多人把注意力都放在位宽选择上对截取策略随便选一个截断了事。结果算法仿真性能很好上板之后性能严重下降查了半天才发现是截断偏差累积导致的。截取策略和位宽选择同等重要不能偏废。3. 截取策略的工程实现与对比3.1 直接截断的实现与误差分析直接截断是最简单的截取方式Verilog里就是直接取高位// 从32位截到16位直接取高16位 wire signed [15:0] truncated data_in[31:16];这种方式的误差范围是[0, 2^16-1]以整数为例误差期望值是2^15也就是半个LSB的偏差。这个偏差是单向的总是让结果偏小对于正数或偏大对于负数因为补码的关系。在什么情况下可以用直接截断如果你的算法有反馈回路而且反馈回路能自动纠正这种偏差那可以用。比如一个自动增益控制环路增益会自适应调整截断带来的微小偏差会被环路纠正。但如果你的算法是开环的比如一个固定的滤波器系数乘法截断偏差会直接体现在输出上就要谨慎了。还有一个场景可以用截断当你的数据本身就只有低位有效高位是符号扩展的时候。比如你从一个16位寄存器读数据但实际有效数据只有12位高4位是符号扩展那截断高4位完全没问题。3.2 四舍五入的实现细节四舍五入的实现比截断复杂一点但也不算麻烦// 从32位四舍五入到16位 wire signed [31:0] rounded data_in 32sd32768; // 加上半个LSB wire signed [15:0] result rounded[31:16];这里32768就是2^15也就是半个LSB。加上之后再截断误差期望值就接近零了。但这里有个坑加法可能溢出。如果data_in是接近最大值的正数加上32768之后可能溢出变成负数。所以实际实现中通常要先做符号扩展用更宽的位宽来做加法然后再截断。或者用饱和加法溢出时饱和到最大值。另一个坑是四舍五入在正数和负数上的行为不完全对称。对于正数加上半个LSB再截断效果是四舍五入。但对于负数由于补码的表示方式同样的操作效果略有不同。如果你对对称性有要求需要额外处理。3.3 收敛舍入的适用场景收敛舍入round half to even在FPGA里实现起来比四舍五入更复杂一些需要判断低位是否恰好等于半个LSB如果是还要看保留的最低位是奇数还是偶数。逻辑大概是这样// 收敛舍入的简化实现 wire [15:0] lower_bits data_in[15:0]; wire half_lsb (lower_bits 16h8000); wire lsb_is_odd data_in[16]; wire round_up (lower_bits 16h8000) || (half_lsb lsb_is_odd); wire signed [31:0] rounded data_in (round_up ? 32sd32768 : 32sd0); wire signed [15:0] result rounded[31:16];这种实现多了一些比较逻辑资源消耗比四舍五入大。但它在统计上更优不会引入周期性偏差。什么时候值得用收敛舍入音频处理是一个典型场景。人耳对周期性偏差比较敏感如果每次舍入都偏向同一个方向会产生可听的谐波失真。收敛舍入能避免这个问题。图像处理里也有类似的需求尤其是做多次迭代的算法比如迭代去噪偏差累积会导致图像出现规律性纹理。3.4 三种截取策略的对比与选择我把三种策略的关键指标整理成表格方便对比策略资源消耗误差期望误差范围适用场景直接截断最低半个LSB有偏[0, 1LSB]有反馈纠正、低位无效数据四舍五入中等接近零[-0.5, 0.5]LSB通用场景、控制算法收敛舍入较高接近零[-0.5, 0.5]LSB音频、图像、迭代算法选择的时候先看你的算法对偏差的敏感程度。如果偏差会累积就必须用四舍五入或收敛舍入。如果偏差不会累积或者有反馈纠正截断也可以用。然后再看资源约束资源紧张就选简单的资源充裕就选统计性能好的。还有一个实际经验在FPGA里四舍五入和收敛舍入的资源差异其实没有想象中那么大因为现代FPGA的LUT资源相对充裕多几个比较器和加法器通常不会成为瓶颈。所以除非你的设计极度资源受限否则我建议优先用四舍五入省心。4. 实战案例从算法到RTL的完整位宽规划4.1 案例背景一个64阶FIR滤波器我拿一个实际做过的项目来演示完整的位宽规划过程。这是一个64阶FIR滤波器输入是16位有符号数采样率100MHz系数也是16位有符号数要求在FPGA上实现资源尽量省。第一步分析输入数据动态范围。输入是16位ADC采样满量程对应±1.0所以输入范围是[-1, 1-2^-15]Q格式是Q1.15。第二步分析系数。系数也是16位Q1.15格式范围[-1, 1-2^-15]。第三步分析乘法结果。两个Q1.15相乘结果是Q2.30格式需要32位。但实际系数通常不会同时达到最大值所以实际动态范围可能小一些。不过为了保险先按32位算。第四步分析累加结果。64次累加理论上需要额外log2(64)6位所以累加器需要32638位。但实际系数有正有负累加结果的绝对值远小于理论最大值。我通过仿真统计实际累加结果的最大绝对值大约是理论最大值的1/8所以额外3位就够了累加器取35位。第五步输出截取。输出要求16位从35位截到16位需要截掉19位。这里用四舍五入加上2^18再截断。最终位宽规划如下节点位宽格式说明输入16Q1.15ADC采样系数16Q1.15滤波器系数乘法结果32Q2.3016x16乘法累加器35Q5.3064次累加留3位余量输出16Q1.15四舍五入截取这个规划比全部用38位省了3位在64个乘法器的情况下省下来的资源相当可观。4.2 乘法器的位宽处理在RTL实现中乘法器的位宽处理有几个细节要注意。第一个细节是符号扩展。两个有符号数相乘Verilog会自动做符号扩展但你要确保两个操作数都声明为signed。如果一个是signed一个是unsignedVerilog会把signed转成unsigned结果就错了。我见过不少新手在这里翻车。// 正确的有符号乘法 wire signed [15:0] a, b; wire signed [31:0] product a * b; // 错误的写法b被当成无符号数 wire signed [15:0] a; wire [15:0] b; wire signed [31:0] product a * b; // 结果可能不对第二个细节是DSP Slice的映射。现代FPGA的DSP Slice通常支持18x18或25x18的乘法16x16乘法可以完美映射到一个DSP。但如果你用了32位乘法就需要多个DSP级联资源消耗成倍增加。所以在位宽规划时尽量把乘法控制在DSP原生支持的位宽内。第三个细节是乘法器的流水线。100MHz的时钟频率下16x16乘法通常需要打一拍才能满足时序。如果你要做64阶FIR64个乘法器并行工作流水线设计就很重要。我一般会在乘法器后面加一级寄存器这样时序压力小很多。4.3 累加器的位宽与溢出保护累加器的位宽规划是FIR滤波器设计中最容易出问题的地方。理论计算需要38位但实际用35位这中间的3位余量是怎么来的我是这么做的先用MATLAB或者Python生成一段典型的输入数据跑一遍浮点算法记录每个累加周期的中间结果。然后统计这些中间结果的最大绝对值。在我的案例中最大绝对值是理论最大值的1/8左右所以3位余量足够了。但这里有个风险如果实际输入数据和仿真数据分布不同可能会溢出。所以我在累加器后面加了饱和逻辑一旦溢出就饱和到最大值或最小值。这样即使出现意外情况也不会产生灾难性的错误。饱和逻辑的实现// 35位累加器带饱和保护 wire signed [34:0] acc; wire signed [34:0] acc_sat; assign acc_sat (acc 35sh7FFFFFFFF) ? 35sh7FFFFFFFF : (acc -35sh800000000) ? -35sh800000000 : acc;这段逻辑会消耗一些LUT但相比溢出导致的算法崩溃这点代价完全值得。4.4 输出截取的实际效果验证输出截取用四舍五入从35位截到16位。实现如下// 35位到16位四舍五入 wire signed [34:0] acc_sat; wire signed [35:0] acc_rounded {acc_sat[34], acc_sat} 36sd131072; // 加2^17 wire signed [15:0] output_data acc_rounded[34:19];这里131072是2^17因为我们要截掉19位半个LSB就是2^18但这里先符号扩展一位再做加法所以加的是2^17。这个细节容易搞错我当初就在这里调了半天。验证的时候我用MATLAB跑了一遍浮点参考再用Verilog仿真跑了一遍定点实现对比两者的输出。在大多数情况下误差在1个LSB以内偶尔有2个LSB的偏差这是四舍五入的正常表现。信噪比测试下来定点实现的SNR大约是90dB对于16位输出来说已经足够了。5. 常见问题与排查技巧实录5.1 位宽不够导致的溢出问题溢出是定点数运算中最常见的问题表现是输出突然出现大幅度的跳变或者算法完全失效。排查溢出的第一步是定位溢出发生的节点。我一般会在每个中间节点加一个溢出标志位仿真时如果标志位置起就说明该节点溢出了。综合的时候这些标志位会被优化掉不影响最终资源。第二步是分析溢出原因。常见原因有三种一是位宽预算不足理论计算就错了二是数据分布超出预期仿真数据不够典型三是截取策略不当截断偏差累积导致溢出。针对第一种原因重新做位宽预算该加位就加位。针对第二种原因换更典型的数据重新仿真或者加饱和保护。针对第三种原因换四舍五入或收敛舍入。我个人的经验是在算法开发的早期就把位宽预算做足宁可多留一点余量也不要后期反复改。因为后期改位宽往往牵一发而动全身调试成本很高。5.2 截断偏差累积的隐蔽表现截断偏差累积的问题很隐蔽因为单次截断的误差很小可能只有半个LSB但经过成百上千次累积就会变成明显的偏差。表现之一是输出的直流偏置。比如一个滤波器理论上对直流信号的增益是1但实际输出有微小的直流偏置。这个偏置可能只有几个LSB但在高精度应用里是不可接受的。表现之二是输出的周期性纹波。如果截断偏差和信号的周期性相关会产生规律性的纹波在频域上表现为谐波失真。排查这种问题我一般会做两件事一是用长数据序列仿真观察输出的长期趋势二是做频谱分析看有没有异常谐波。如果发现偏差累积就把截断改成四舍五入通常能解决问题。5.3 时序不收敛与位宽的关系位宽增大不仅消耗资源还会恶化时序。因为位宽越大组合逻辑的延迟越长尤其是加法器和乘法器。我遇到过一个案例一个32位累加器在100MHz时钟下时序总是差一点。后来把累加器拆成两级流水线每级16位时序就满足了。虽然多了一级寄存器但整体性能反而提升了。所以位宽优化不只是省资源也是改善时序的手段。在时序紧张的时候除了传统的流水线切割还可以考虑降低中间结果的位宽。当然降位宽之前要确认精度损失在可接受范围内。5.4 常见问题速查表问题现象可能原因排查方法解决方案输出大幅度跳变累加器溢出加溢出标志仿真增加位宽或加饱和输出有直流偏置截断偏差累积长序列仿真看趋势改用四舍五入输出有周期性纹波截断偏差与信号相关频谱分析改用收敛舍入时序不满足位宽过大导致逻辑延迟长时序报告分析流水线切割或降位宽资源超限位宽预算过松资源报告分析精细化位宽管理仿真与上板结果不一致截取策略在综合时被优化对比综合前后网表加综合属性防止优化5.5 几个实用的调试技巧第一个技巧用SystemVerilog的断言做位宽检查。在仿真阶段可以用断言监控每个节点的位宽使用情况一旦接近溢出就报警。这样能在早期发现问题。第二个技巧保留一个浮点参考模型。不管你的定点实现怎么优化始终保留一个浮点版本的算法作为参考。每次修改定点实现都跟浮点参考对比确保性能没有明显下降。第三个技巧分段验证。不要等整个链路都写完再验证每写完一个模块就单独验证。比如乘法器写完先用随机数验证乘法的正确性累加器写完再验证累加的正确性。这样问题定位容易得多。第四个技巧注意综合工具的优化。有些综合工具会把你的截取逻辑优化掉尤其是当你截掉的是常数或者无效位的时候。如果你发现仿真结果和上板结果不一致先检查综合后的网表看看截取逻辑还在不在。6. 进阶话题自适应位宽与动态截取6.1 自适应位宽的基本思路自适应位宽是指根据数据的实际动态范围动态调整运算位宽。这种技术在通信、雷达等信号动态范围变化很大的场景里很有用。基本思路是用一个检测器实时监测数据的幅度根据幅度大小动态选择位宽。幅度小的时候用窄位宽省资源幅度大的时候用宽位宽保精度。实现上可以用一个多路选择器根据幅度检测结果选择不同的数据路径。但这样会增加控制逻辑的复杂度而且位宽切换的瞬间可能产生毛刺需要仔细处理。我个人的经验是自适应位宽在资源极度受限且信号动态范围确实很大的场景下才值得用。大多数场景下固定位宽加饱和保护已经足够了没必要引入额外的复杂度。6.2 动态截取策略的选择动态截取是指根据数据特征动态选择截取策略。比如当数据变化平缓时用截断变化剧烈时用四舍五入。这种策略在音频处理里有一些应用因为人耳对平稳信号的偏差更敏感。实现上可以用一个简单的梯度检测器根据相邻样本的差值来判断数据变化快慢然后选择截取策略。但同样这会增加逻辑复杂度而且效果不一定比固定用四舍五入好。我的建议是除非你有明确的证据表明动态策略能带来显著收益否则还是用固定的四舍五入或收敛舍入。简单可靠的方案往往比复杂精妙的方案更实用。6.3 位宽优化与算法协同设计最后想强调的是位宽优化不是孤立的它应该和算法设计协同进行。有些算法天生对位宽不敏感比如一些鲁棒的估计算法有些算法对位宽极其敏感比如高精度的积分器。在算法选型阶段就应该考虑定点实现的可行性。如果一个算法需要极高的数值精度才能工作那它在FPGA上可能就不是一个好选择。反过来如果你知道FPGA的位宽限制可以在算法设计时就做一些妥协比如用衰减因子代替纯积分用限幅代替饱和等。我在实际项目中通常会先用浮点快速验证算法可行性然后尽早做定点化仿真看看性能下降多少。如果定点化之后性能下降太多就要回头改算法而不是一味加位宽。加位宽是有上限的算法改进的空间往往更大。这个协同设计的过程说起来简单做起来需要经验和耐心。但一旦你养成了这个习惯FPGA算法实现的成功率会高很多调试时间也会大幅缩短。
返回列表