ARTICLE DETAIL

资讯详情

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

FPGA除法器IP的Vivado实战:从算法选型到时序收敛

FPGA除法器IP的Vivado实战:从算法选型到时序收敛 如果让我在Vivado里选一个“看着简单、真正用起来全是细节”的IPDivider Generator一定排前三。第一次需要做整数除法时我的第一反应是手写比较器、减法器、移位寄存器一个状态机跑N拍似乎也不难。结果综合一出来LUT烧掉一大片WNS还是负的。后来认真把Divider Generator IP从Radix2模式一路用到Fractional模式才发现这个IP从算法选型到位宽延迟都藏着一堆门道。这篇东西就是给正在配这个IP、被算法类型和延迟搞得晕头转向的人写的。我默认你用的是Vivado 2019.1以上的版本Divider Generator IP版本通常在5.1左右。不同版本GUI长相差不太多核心选项是稳定的。下面所有配置截图我没办法贴出来但会把每个选项的位置和含义写清楚你照着点就行。更重要的是我会讲清楚每个选项背后的“为什么”——这才是网上多数教程没写透的部分。1. 除法器不是你想的“循环减”先把资源账算清楚很多工程师第一次接触除法时都有同样的错觉除法就是循环减法和移位。对于纯软件思维来说没错但放到FPGA里这个“简单”会迅速变成资源灾难和时序灾难。1.1 手写移位减法的真实代价先看一个经典的非恢复余数除法思路。假设A是被除数D是除数N是被除数的位宽基本流程就是把除数左移对齐后不断与被除数比较、相减、记商// 伪代码思想示意结构不代表可综合实现 for (i 0; i N; i i 1) begin if (remainder divisor_shifted) begin remainder remainder - divisor_shifted; quotient {quotient[N-2:0], 1b1}; end else begin quotient {quotient[N-2:0], 1b0}; end divisor_shifted divisor_shifted 1; end这段逻辑的核心问题在于N位的除法至少需要N个时钟周期串行迭代而且每个周期都要做一次比较和一次减法。如果你把它做成组合逻辑N位比较器加N位减法器瞬间会撑爆一个中等规模FPGA的LUT如果你做成状态机延迟不可控吞吐率上不去主频稍微拉高一点关键路径上的比较器加减法器就会让时序直接红掉。我实测过一个很常见的场景32位被除数、16位除数用移位减法状态机实现在Artix-7上大概只能跑到180MHz左右LUT消耗接近2000个。换成Divider Generator IP之后同样条件下LUT消耗降了大概40%主频轻松上200MHz而且吞吐率稳定在一拍一个不需要自己操心状态机。1.2 Divider Generator IP把活干到了哪一步Divider Generator的核心理念是把你需要串行迭代的除法过程改造成流水线结构。它内部会把除法运算切成很多级流水级每一级只做一部分运算结果逐级往后传。这样虽然单个除法的输出延迟仍然存在但只要流水线填满系统可以做到每个时钟周期都接受一组新的除法请求输出也是每个周期都吐一个结果。这正是FPGA最擅长的做派用面积和流水深度换吞吐。IP核内部具体是恢复余数还是非恢复余数是高位优先还是低位优先这些对使用者来说是黑盒你不需要关心。你需要关心的只有几件事算法模式、位宽、数据格式、延迟、以及接口握手。把这些选对IP自己会处理好一切细节。这也是我为什么强烈建议能用IP就别手写。1.3 整数除法IP与浮点除法IP的边界这里必须先泼一盆冷水Divider Generator做的是整数除法或定点小数除法不是IEEE 754浮点除法。如果你要做的是两个float32相除那应该用Floating-Point IP而不是Divider Generator。这两个IP在Vivado IP Catalog里都很容易搜到但用途完全不同千万别搜到Divider Generator就直接往上怼。Divider Generator处理的是固定位宽的二进制补码数或无符号数。它支持小数输出的Fractional模式本质上也是定点小数的思想而不是浮点数。需要浮点、需要动态指数范围、需要NaN或Infinity处理请右转Floating-Point IP。这个边界搞清楚了后面才不会被精度问题折磨。2. 算法模式选择的底层逻辑Radix2、Radix4与FractionalDivider Generator配置界面最显眼的就是Algorithm Type下拉框。里面通常有Radix2、Radix2 with Remainder、Radix4、Radix4 with Remainder、Fractional这么几个选项。很多新手看到这里就懵了我逐个拆开讲。2.1 Radix2到Radix4每拍多算一位Radix2模式是基础中的基础。它的含义是每个流水级处理1位商。如果有N位的被除数大致就需要N个左右的时钟周期延迟才能出结果。Radix2对资源要求相对低时序也相对好收敛是大多数整数除法场景的默认选择。Radix4模式则是每个流水级处理2位商相当于把迭代步数砍了一半左右延迟相应缩短但每一级的组合逻辑会更复杂面积会涨。这里有一个非常典型的权衡如果延迟是你系统的硬指标Radix4往往比Radix2更合适如果你的逻辑已经很挤、时序很紧Radix2反而更容易收时序。不要无脑选Radix4。2.2 with Remainder 后缀到底改了啥Radix2 with Remainder和Radix4 with Remainder从名字上就能看出区别这两个模式在输出商的同时会把余数也带出来。不是说你选了带Remainder模式才需要算余数而是说IP会把余数放到输出总线上一起返回给你。这里就涉及一个关键问题在无余数模式下商和余数其实都算出来了只是余数被丢弃输出总线只给出商。在带余数模式下输出总线的位宽会变大商和余数拼接在一起具体哪一段是商、哪一段是余数要参考生成的例化模板和demo_tb来确定。千万不要凭记忆去拼接不同配置下连法不一样。后面第7章我会专门讲怎么一次实验搞定这个拼接问题。2.3 Fractional模式小数除法不是“约等于”Fractional模式是最容易被误解的。它不做整数除法而是做定点小数除法。什么意思呢比如10除以4如果被除数和除数都是整数整数除法会得到商2余数2。这没错。但有时候你想要的答案是2.5而不是商2余2。Fractional模式就是干这个的在配置时指定一个小数位宽F输出结果会分成整数部分和小数部分两部分小数部分以2的F次幂为分母来表示一个小数。本质上这就是定点数的思想。10 / 4 2.5如果小数位宽设成8那小数部分就是 0.5 * 256 128用8位二进制表示就是0x80。输出总线的高D位是整数2低8位是128合起来看就是2.5。这个模式没有余数概念因为它已经把余数换算成小数部分输出了。Fractional模式的延迟要比Radix2长不少大致在D F拍左右而且F越大资源、延迟、时序压力都会同步上涨。所以配置小数位宽的时候要克制够用就行不是越多越好。2.4 延迟、位宽、资源一张表说清楚我用一个粗略的经验表来总结这几种模式的差异。注意这些数据是经验值不是精确数据准确延迟要以IP生成后的参数为准。算法模式适用范围经验延迟量级输出内容资源压力Radix2通用整数除法资源优先约D拍商低Radix2 with Remainder需要商余数约D拍商 余数中低Radix4延迟敏感型整数除法约D/2 常数拍商中Radix4 with Remainder延迟敏感型商余数约D/2 常数拍商 余数中Fractional定点小数除法约D F拍整数部分 小数部分高D是被除数位宽F是小位数宽。“经验延迟量级”只是帮你估算真正写代码时千万别按这个固定周期去采样数据要用输出端的tvalid信号来同步。这个习惯极其重要在第4章我会用代码说明。3. 配置界面逐项拆解这些选项你真的看懂了吗双击IP Catalog里的Divider Generator弹出的配置界面看起来选项不少但真正需要仔细斟酌的就集中在几个地方。3.1 Algorithm Type与Remainder TypeAlgorithm Type的位置在Configuration页第一个下拉框就是上一章讲的那些模式。选完后如果选了带Remainder的模式界面上会出现一个Remainder Type下拉框里面有Remainder和Rem Modulo两个选项。这个选项非常容易被忽略但它直接影响余数的语义。Remainder是普通意义上的“商乘除数加余数等于被除数”的那个余数余数符号跟随被除数Rem Modulo则是模运算意义上的余数结果始终落在0到|除数|-1的范围内符号跟随除数。这两种余数在软件里对应的就是C语言的%和数学上的mod很多做算法移植的人在这里吃过亏。举一个具体例子-7除以3普通Remainder结果是-1因为 -7 -2 * 3 (-1)而Rem Modulo结果是2因为 -7 -3 * 3 2。如果你要把C代码里的%运算搬到FPGA上记得检查它的语义很多C编译器的%是截断型对应的是Remainder。3.2 数据格式、位宽与小数位宽Data Format选项选择Signed或Unsigned这决定了输入数据按二进制补码解释还是按普通无符号数解释。选Signed的时候最高位是符号位这一点要刻在脑子里。举个例子你设Divident Width为16那么实际有符号数范围是-32768到32767而不是0到65535。很多人只改位宽不改Data Format结果输入负数直接算错。位宽设置上Divisor Width和Dividend Width是分开的二者可以不一样。输出位宽在Radix2等模式下会根据算法自动计算一般不用手动指定但你要心里有数商的位宽基本和被除数位宽对齐余数的位宽和除数位宽相关。如果被除数位宽设小了商溢出时IP不会给你任何报错只会悄悄截断这个坑我后面会展开讲。Fractional模式下会额外出现一个Fractional Width输入框它决定输出小数部分的位宽。这个值填得越大精度越高但延迟和资源也越高。一般场景下10到24位就非常够用了只有做高精度数控类需求才需要32位以上。3.3 Latency与Optimization自动和手动的取舍Latency选项通常有Automatic和Manual两种。Auto是官方推荐IP会根据算法模式、位宽、小数位宽以及你选的目标器件计算出一套合理的流水级数。Manual是让你手动指定期望的最大延迟选Manual之后你可能会看到是否需要更多流水级的提示。我的建议是第一版设计老老实实用Automatic。等系统跑通了、发现除法延迟影响了整体时序或流水调度再回来考虑Manual和Radix4这时候你才有足够的上下文判断该怎么压缩延迟。上来就手动拉延迟往往会在时序和资源之间找到错误平衡点。Optimization Goal选项一般是Area或Speed两种倾向。Area偏资源优化Speed偏时序优化。如果你的设计离时序收敛差得不多选Speed可能就救回来了代价是LUT或FF稍微多消耗一点。如果整个工程资源本来就很紧张优先Area。3.4 Reset与AXI Stream选项配置界面还会让你选择是否生成aresetn复位信号。Divider Generator的复位是低有效同步复位建议始终勾上因为你不确定上游逻辑会不会需要复位来清握手信号。复位释放时建议至少满足两个时钟周期的低电平再拉高保证内部状态机干净启动。AXI Stream相关选项里有个比较关键的是Enable tlast。如果打开输入端的两个通道都需要同时发送tlast输出端在包的最后一个数据上拉高tlast。如果你只是做一个单次除法、没有包的概念可以不勾tlast少接几个信号省心。另一种需要关注的是Enable tuser勾上后输出会多一个m_axis_dout_tuser信号这个信号在除数为0时会拉高是个极其实用的功能后面详聊。4. Radix2整数除法完整实例从生成到对拍接下来我们实际走一遍。以最常用的Radix2无符号除法为例被除数16位除数8位除法结果输出商。4.1 最小化配置16 / 8 无符号除法在IP Catalog里搜Divider Generator双击打开按以下方式配置Component Namediv_gen_0Algorithm TypeRadix2Divisor Width8Dividend Width16Remainder Type这里因为没选with Remainder模式不出现Data FormatUnsignedLatencyAutomaticOptimization GoalArea其他选项保持默认。点OK生成后Vivado会生成div_gen_0的例化模板。你可以在Sources面板里展开IP源文件找到div_gen_0.veo里面就是官方给的例化模板直接复制到你的顶层模块里改个名字即可比手敲端口安全得多。4.2 例化代码与端口说明一个最小化的例化代码大致长这样div_gen_0 u_div ( .aclk(clk), .s_axis_divisor_tvalid(s_divisor_tvalid), .s_axis_divisor_tready(s_divisor_tready), .s_axis_divisor_tdata(s_divisor_tdata), // 8-bit .s_axis_dividend_tvalid(s_dividend_tvalid), .s_axis_dividend_tready(s_dividend_tready), .s_axis_dividend_tdata(s_dividend_tdata), // 16-bit .m_axis_dout_tvalid(m_dout_tvalid), .m_axis_dout_tready(m_dout_tready), .m_axis_dout_tdata(m_dout_tdata) // 16-bit );这里两个输入通道是独立的AXI Stream接口s_axis_divisor和s_axis_dividend。它们可以接收不同位宽的数据但在发起一次除法时两个通道必须在同一个周期同时拉高tvalid并保持各自的数据稳定。换句话说被除数和除数是配对送入的不能先送被除数、下一拍再送除数。4.3 不要数延迟用tvalid采样很多初学者在TB里会干这样的事根据第2章的经验公式算出延迟然后数固定周期去读m_axis_dout_tdata。这种方式极其脆弱。一旦你改了位宽、换了算法模式、或者用Manual调整了延迟数好的周期就全部作废而且排查起来非常痛苦。正确做法是用输出端的tvalid作为数据有效的标志。它拉高时m_axis_dout_tdata上的数据就是当前这次除法的结果。代码写起来也很简单always (posedge clk) begin if (m_axis_dout_tvalid m_axis_dout_tready) begin q_result m_axis_dout_tdata; result_valid 1b1; end endtvalid和tready同时为高表示一次成功的输出握手数据被可靠接收。这个习惯建好之后后面无论怎么调整IP配置你的验证逻辑都不用动。4.4 验证结果分析我习惯写TB时做一次全遍历或随机约束测试。对于16位无符号被除数、8位无符号除数可以随机生成10000组数据用Verilog的/运算符算出期望商再和IP输出的商比对。这里有个细节既然你是做随机验证就不要把输入数据当成固定常量每拍都发。更真实的方式是用一个简单的valid发生器随机拉高tvalid中间偶尔拉低模拟真实系统中的反压场景。这样能顺便验证你的握手逻辑是否正确处理了tready拉低的情况。实测中Radix2模式16位被除数的输出延迟大概在17拍附近和理论值很接近。你把tvalid采样做好之后完全不用关心这个数字但心里有数对调试有帮助。5. Fractional模式实战2.5是怎么算出来的Radix2部分做好之后最值得深入玩的就是Fractional模式。这一节我们做一个小实验计算10除以4期望得到2.5。5.1 Fractional背后的定点数思想Fractional模式的输出不叫余数而叫小数部分。它的数学含义是结果 整数部分 小数部分 / 2^FF就是你在GUI里填的Fractional Width。比如F8那么小数部分128就代表128/2560.5F8时小数部分最小单位就是1/256约等于0.00390625。所以F越大分辨率越高。这和我们平时说的定点数Q格式是一个思路。只是Divider Generator把整数部分和小数部分拼在同一个输出总线上你不用自己去实现定点缩放和对齐IP全给你算好了。这个模式特别适合那些“被除数和除数都是整数但期望结果是带小数的比例值”的场景比如频率分频比计算、PID系数标定、坐标归一化。5.2 配置Fractional除法实例新建一个IP配置按下面设置Component Namediv_gen_fraAlgorithm TypeFractionalDivisor Width8Dividend Width8Fractional Width8Data FormatUnsignedLatencyAutomatic生成后你会在例化模板里发现m_axis_dout_tdata的位宽变成了16位而不是Radix2模式下的8位。这16位怎么拆高8位是整数部分低8位是小数部分。这是Fractional模式的关键也是很多人拿到数据后一脸懵的地方。5.3 输出拼接与真值换算喂入被除数108h0A、除数48h04等待tvalid拉高后输出应该是16h0280。我来拆解一下高8位0x02也就是十进制2这是商的整数部分低8位0x80也就是128小数部分真值是128/2560.5合起来2 0.5 2.5在TB里做换算时我习惯把整数和小数分别提取出来再转成real类型方便做自动比对wire [7:0] int_part dout_tdata[15:8]; wire [7:0] frac_part dout_tdata[7:0]; real result_real; always (*) begin result_real int_part (frac_part * 1.0) / 256.0; end如果你要做自动比对期望值是expected_real 10.0 / 4.0。这里要注意浮点比较的精度问题建议比较时允许一个最小单位的误差也就是1.0/256.0否则浮点计算的舍入可能会让你的比对意外失败。5.4 什么时候适合Fractional什么时候不适合Fractional模式虽然方便但它有个特点输出总位宽是整数位宽加小数位宽在数据量大的场景下位宽扩张很快。比如你用一个32位被除数、32位除数、再配32位小数位宽输出就是64位两个这样的IP就可能把总线带宽和存储资源吃掉一大截。如果只是单点计算、数据率不高Fractional完全够用。但如果你在做一个高吞吐的流式处理每个周期都要来一次除法而且对精度要求很高我建议你先看看能不能用移位近似处理掉或者把数据先定标到合适范围再用Radix2软件后处理不要一上来就开Fractional。除法IP在FPGA里从来都不是免费的位宽越大越贵这是硬道理。6. 接口握手、复位与时序收敛的细节配置和功能都跑通之后真正决定项目成败的是接口细节和时序。这一章把最容易出问题的几个点说透。6.1 AXI Stream Valid/Ready握手的易错点AXI Stream协议的关键规则是tvalid拉高后数据必须保持稳定直到tready拉高完成握手tready拉低时上游必须等待如果tvalid和tready同时为高本拍完成一次数据传输。在Divider Generator这里输入端的特殊之处在于有两个通道。我见过不少人只拉高s_axis_dividend_tvalid忘了拉高s_axis_divisor_tvalid结果IP就是不工作数据堵在输入口。这两个通道必须同时有效IP才会真正接收这一次除法请求。调试的时候如果发现输入侧tvalid已经拉高但tready一直不拉高第一反应就应该去查另一个通道的tvalid是否也拉高了。输出侧的反压也要小心。m_axis_dout_tready是下游给你的反压信号。如果你把tready一直拉高问题不大但如果你在下游做了FIFO缓冲FIFO满时tready拉低此时tvalid可能已经被拉高那么输出总线上的数据必须继续保持有效直到tready重新拉高完成握手。很多人在这个环节会忘记保存数据导致丢数。6.2 tlast和tuser边界与除零检测当你打开Enable tlast之后输入端的两个通道都要在包的最后一个数据周期拉高tlast。由于除法器的被除数和除数是配对的两个通道的tlast也应该在同一拍拉高。如果你只给其中一个通道发tlast输出侧m_axis_dout_tlast的行为会变得不符合预期下游在解析包边界时就会出现错位。tuser是另外一个值得打开的功能。在配置界面勾选Enable tuser后输出会多一个m_axis_dout_tuser信号。这个信号就是除零检测标志位。当除数为0时tuser会拉高提醒你这次除法的结果是无效的。实际使用中即使有了tuser你在数据面也要把除零结果屏蔽掉不能让无效结果参与后面的计算否则错误会一路传导下去。把tuser当作一个错误标记接到状态机里去处理是比较规范的做法。6.3 反压场景处理与复位时序在真实系统里不可避免会遇到下游处理不过来、上游数据又不断到达的情况。这时你需要仔细设计反压路径。以输入端为例如果s_axis_dividend_tvalid和s_axis_divisor_tvalid同时拉高但tready没有立即拉高上游数据要保持到握手完成。这意味着你的上游数据源需要有机制来“暂停”数据发送比如一个FIFO的valid和ready逻辑。复位方面aresetn是低有效同步复位。手册上要求复位信号相对于aclk建立和保持时间满足要求。我在项目中的做法是用片上的复位管理IP生成至少4拍的低电平复位脉冲确保IP内部所有流水级都被复位干净。复位释放后不要立刻送数据等上两个周期观察tready状态再启动数据流这样最容易稳定。6.4 时序收敛与资源优化如果发现使用了除法IP后WNS变差了先别急着优化逻辑按以下顺序排查查除法IP的输出位宽是否过大位宽每增加一点关键路径上比较器和加法器的级联就会变长。查算法模式Radix2时序通常比Radix4好收因为每级逻辑少。查是否选择了Speed优化目标没选的话可以改选Speed重新综合看时序。查输入输出侧是否存在组合逻辑直接驱动tdata。强烈建议在除法IP的输入之前和输出之后都插入寄存器把组合逻辑与IP隔离开这样最有利于时序收敛。资源上Fractional模式因为输出总线宽往往比Radix2多消耗一部分寄存器。如果你发现FF消耗很多可以看看是不是小数位宽设置过大适当缩减F值通常能显著减少资源占用。7. 实战中绕不开的坑与我的解决思路最后这部分是我在多个项目里踩过的坑每一个都花过时间排查。写出来帮大家少走点弯路。7.1 除数为0tuser帮你报警但结果还是要自己拦之前讲过tuser可以报告除零。但我不建议只依赖tuser因为除零发生时即使你做了报警IP输出的商数据本身是没有意义的。有些配置下除零结果会是全1有些是保持上一个结果具体行为大概率不是你想要的值。我在工程里的做法是在送入除法器之前加一个除数是否为0的判断一旦为0就不发起除法请求直接输出一个安全值同时置一个错误标志。这样数据面永远是确定的tuser只是用来做兜底监控防止逻辑误判漏检了0值。守护上下两条线比任何单项保护都可靠。7.2 带余数输出的总线拼接一次实验定乾坤带Remainder模式下输出总线里商和余数到底谁在高位谁在低位不同版本的IP甚至不同配置下都可能有差异。我见过有人凭记忆把高8位当商结果抓出来的数据全错排查了一整天才发现是高低段搞反了。最稳妥的办法是做一个最小实验输入被除数20、除数6理想结果是商3、余2。把输出抓出来观察数据段。如果输出总线是商在低位你会看到低段是3高段是2如果商在高位正好反过来。一次实验就能确认以后就再也不用瞎猜了。在做这种验证时记得用tvalid对齐数据别去数延迟周期。7.3 BD集成时容易忽略的连接在Block Design里用除法IP比纯RTL方便但有个细节经常被忽略在BD里拖入Divider Generator后两个输入通道的tready会被封装成独立的信号。你用AXI Stream接口连接上游时如果上游只有一个数据通道可能只连了被除数通道除数通道悬空了。结果仿真时发现除法器永远不干活。我都忘了自己第一次在BD里连除法器时花了多长时间才意识到两个s_axis输入要分别连接两个数据源或两个寄存器切片。正确做法是两个输入通道都要连接到有效的数据源并且保证它们的数据在同一拍有效。如果是固定除数可以用一个常量寄存器驱动s_axis_divisor_tdata然后把它的tvalid拉高、tready悬空或接个真值。这样除数是固定值时整个除法器对外看就像一个单输入IP用起来非常省心。7.4 位宽截断与小数精度损失最后一个坑是位宽截断。很多人在配置时被除数位宽按实际最大输入值选了个“差不多够用”的值没考虑到中间计算过程。比如你的输入范围是0到10000选14位被除数够用了但如果除数是1商最大就是1000014位勉强够。一旦实际输入偶尔到15000又没做饱和处理商就会溢出输出数据悄悄截断结果完全错误。我建议位宽按理论上最极端情况留好余量。被除数最大是多少就按它的位宽来别贪省那1到2个bit。而Fractional模式下小数位宽决定了精度F每增加一位精度翻倍但代价也翻倍。做控制类项目时可以先算出允许的误差反推最小F值再加2到3位的安全余量这是性价比最高的做法。我用Divider Generator这几年最大的体会是这不是一个“配完就忘”的IP它的每一个配置项背后都对应着一套硬件权衡。算法模式影响延迟和面积位宽影响精度和时序握手影响整个数据流的稳定性。把这些点逐个捋清楚你的除法链路基本不会再出幺蛾子。
返回列表