ARTICLE DETAIL

资讯详情

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

Xilinx FPGA除法器IP核在Vivado中的配置与仿真调试指南

Xilinx FPGA除法器IP核在Vivado中的配置与仿真调试指南 没有哪个做FPGA的人能绕开除法。做图像处理要算坐标比例做通信要解符号判决做控制要算PID增量随便一个应用场景除法运算总是像回了趟家一样必然出现。但真用Verilog写一个assign q a / b的时候综合工具一看变量除法没法优化成移位只能给你摊出一大片组合逻辑路径延迟长到发指跑个100MHz都费劲。这也是为什么Xilinx专门提供了一个叫Divider的IP核——它把除法从“组合逻辑噩梦”变成了一个带流水的硬件模块。这篇文章就围绕Xilinx FPGA除法器IP核Divider在Vivado里的使用把配置、仿真、集成、调通整条链路拆开讲。写这东西适合什么人两种一种是刚用Vivado不久被除法IP核的Latency、RFD、VALID这些信号绕得头晕的新手另一种是已经会用但遇到商值延迟不对、时序收不住、被零除等奇怪问题想要避坑的工程师。不管哪种看完这篇都能少走不少弯路。1. 为什么非要用除法器IP核直接写“/”不行吗1.1 综合器看到的除法和你心里想的不是一回事Verilog里的/运算符如果是两个变量相除综合工具并不会像软件编译那样直接给出一条除法指令它会把这种动态除法映射成一套由查找表和进位链搭出来的组合逻辑网络。被除数、除数位数稍大一点比如16位除以16位组合逻辑的扇出和级联深度基本上就是灾难级别。我曾经试过在工程里直接写了个32位变量除法综合器给出的逻辑级数直接飚到六十多级时序报表里一大堆红色的违例路径place设计里挤得满满当当。那换一种思路是不是可以把除法拆成多次减法或者移位来实现理论上可以但组合逻辑做的减法器阵列依然是纯组合电路只要输入一变输出就要重新算一遍根本没法通过寄存器流水来切细时序路径。高频设计下这种结构完全站不住脚。而Xilinx的Divider IP核采用的是恢复余数法这一类算法并在IP内部把每一次迭代映射到寄存器和DSP单元上天然就是流水化的时序收敛的难度一下就降下来了。1.2 什么时候用IP核什么时候不需要用IP核不等于所有除法都要用它。如果除数是个常数或者等于2的整数次幂那直接用移位和加法就够了比如除以8就是右移三位完全没有必要动用IP核。如果被除数是常数那综合器甚至可以直接算出结果也不需要IP。真正需要用IP核的是被除数和除数都在运行时动态变化的场景。比如图像处理里的像素坐标映射、浮点定点转换中的刻度计算、通信协议里的符号判决这类场景下用Divider IP是性价比最高的选择。另外还有一点要注意很多FPGA里的DSP48单元本身并不包含除法指令它擅长的是乘加运算。Xilinx的除法器IP之所以能用DSP资源快速实现除法依赖的是把迭代运算映射为乘加结构再加上控制逻辑。所以在资源评估时千万不要以为除法器只是占一点LUT它的DSP占用和Latency设置直接相关配置不好会挤占其他模块的DSP预算。1.3 和直接用HLS相比Divider IP核有它的优势有人会说我用Vivado HLS或者Vitis HLS写个x/y综合出来的除法儿不也挺好么这话对但也不全对。HLS生成的除法模块通常带有自己的接口协议、时钟使能和握手信号耦合度较重而且在RTL集成时需要适配它的接口和控制状态机。而IP核的好处是接口标准化Vivado里点两下就能生成一个干净的模块通过AXI或简单的握手信号就能接进现有数据通路。对于那些仅需要偶尔一次除法、不需要整个算法层面重写的场景IP核的侵入性更低集成更安全。2. Divider IP核配置逐项拆解每个参数背后都是一套硬件2.1 算法选择Radix-2、Radix-4和Radix-8到底改了什么Vivado的Divider IP核在配置界面里会让你选算法的radix默认是Radix-2还可以选Radix-4和Radix-8。这个参数很多人不理解以为只是速度和面积的调整。实际上它决定的是除法器内部每次迭代处理的位数。可以这样类比Radix-2的除法器就像学校里人工列竖式一样每次只处理一位商控制简单占用的硬件少但完成整个除法需要的周期数比较多Radix-4则相当于一次处理两位商速度提升了一倍但要生成三倍除数、五倍除数这类中间结果需要更多的加法器和逻辑资源Radix-8一次处理三位速度更快资源消耗也更大。实操中的选择逻辑一般是系统频率不高、数据吞吐要求不紧张的场景用Radix-2就够了如果后续数据流的处理延迟要求苛刻比如多条流水线都要等除法的结果那就用Radix-4来降低延迟但一定要在资源预算里留出余地。Radix-8在多数FPGA工程中不太常见因为它会对DSP48和进位链的资源占用提出不小压力高速大位宽设计里容易把时序逼到死角。2.2 数据位宽和符号位位宽到底影响什么配置界面里的Dividend Width和Divisor Width分别指被除数和除数的位宽必须根据实际数据的最大值来定。需要注意的是这里的位宽不是随意给的它会直接影响IP内部迭代次数、DSP资源的复用数量以及最终输出商和余数的位宽。如果位宽配置不够可能出现高端数据被截断、商值和预期完全对不上的情况。符号位的设置同样关键。无符号除法只把数据当正整数处理有符号除法则需要把最高位当成符号位。在这方面踩过坑的人不少最常见的问题就是把有符号的被除数接成了无符号接口导致负数输入直接变成一个巨大的正数商的符号就完全错了。所以配置IP之前第一件事是把你的数据规格想清楚——是不是有负数、负数怎么表示、结果需要保留哪些位。2.3 Remainder Type你需要的到底是余数还是小数部分这是配置环节里最容易犯迷糊的一项。Divider IP核除了输出商Quotient还提供了一个输出叫Remainder或Fractional。如果你选择Remainder模式得到的是整数除法的余数类似于C语言里的%操作。如果你选Fractional模式那么这个输出给出的就是商的低位小数部分通过它可以把商和分数拼成定点小数结果。举个例子如果我要算100 / 7整数商是14余数是2。如果用Fractional模式预设小数位宽为8位那么IP会输出一个表示约0.285156252/7近似值的9位或指定宽度的数值。怎么拼呢一般做法是把整数商左移小数位宽再和分数部分相加得到定点结果。需要注意Fractional模式下额外增加的小数位宽会带来额外延迟和资源如果只是判断能否整除用Remainder模式就足够没必要增加硬件开销。2.4 Latency选项自动、手动和延迟可变的坑Latency是除法器IP核配置里最值得花时间理解的一项也是使用中灾难高发区。选择自动模式时IP核会根据时钟频率、算法类型和被除除数的位宽自动算出合理延迟。选择手动模式你可以指定一个延迟范围但必须保证不低于IP内部算法所需的计算时间。设置低了IP输出商值在时序上就会乱套仿真里看到的结果往往是东一块西一块的碎片数据。这里有一个非常重要的细节除法器IP的延迟并不是固定不变的。它和输入数据的具体数值有关尤其是Radix-2模式下商数变化越频繁内部处理周期数可能就越多。IP核通过RFD信号来表示自己当前是否准备好接收新数据。这就意味着你不能像用普通流水寄存器组那样以为输入和输出之间就是一个固定的拍数。正确做法是数据进来时同步记录或缓存后续控制信号直到输出的VALID拉高再采走结果。相比死等固定延迟这种“结果有效”式的握手方式才是安全的。2.5 接口信号RFD、VALID和CLK之间的微妙关系Except熟悉IP核以后你会发现和除法器打交道主要就是看RFD和VALID这两个信号。RFD是“Ready For Data”的缩写它拉高时表示IP可以接收一个新的被除数和除数它拉低时你喂进去的数据会被忽略。VALID则在输出端拉高一个周期告诉下游当前商和余数有效可以采走。很多人第一次学这个IP时会下意识认为只要输入持续有效输出也应当持续有效形成像FIR滤波器那样的流水输出。但除法器不一样它是“部分握手”式的工作方式前一个除法可能算了10个周期后一个可能算了7个周期因为数值不同导致内部迭代步数不同所以VALID的间隔是不均匀的。设计数据通路时必须在输出端处理这种可变延迟。我的习惯是在除法器前面用一个简单的FIFO缓存输入数据后面再用VALID信号选通输出结果让整个数据流在外部看起来依然是连续稳定的。3. 仿真与集成实操从字符级波形看懂除法器的异步握手机制3.1 一个最小可仿真工程怎么搭在Vivado里生成一个Divider IP核非常简单双击IP Catalog里的Divider配置好位宽、算法和Latency生成即可。为了讲解方便我以一个8位无符号、Radix-2、自动Latency的除法器为例展示它的基本接法。顶层RTL里最核心的代码其实不超过二十行wire s_axis_dividend_tready; wire s_axis_divisor_tvalid divisor_valid; wire [7:0] s_axis_dividend_tdata dividend; wire [7:0] s_axis_divisor_tdata divisor; wire m_axis_dout_tvalid; wire [15:0] m_axis_dout_tdata; div_gen_0 u_div ( .aclk(clk), .s_axis_dividend_tvalid(s_axis_dividend_tvalid), .s_axis_dividend_tready(s_axis_dividend_tready), .s_axis_dividend_tdata(s_axis_dividend_tdata), .s_axis_divisor_tvalid(s_axis_divisor_tvalid), .s_axis_divisor_tready(s_axis_divisor_tready), .s_axis_divisor_tdata(s_axis_divisor_tdata), .m_axis_dout_tvalid(m_axis_dout_tvalid), .m_axis_dout_tdata(m_axis_dout_tdata) );这段代码里的div_gen_0是IP核生成的例化模板整个接口是AXI4-Stream风格的。实际使用中我们需要关心的是s_axis_dividend_tvalid和s_axis_dividend_tready这对握手信号以及m_axis_dout_tvalid输出有效信号。如果不想用AXI接口也可以在IP配置里选“Non-Function Block”模式那样接口会更裸一些类似dividend、divisor、quotient、rfd、valid这样的端口。3.2 手把手仿一个100除以7在Vivado里新建一个仿真testbench把时钟和输入数据准备好。以100 / 7为例令dividend100divisor7拉高s_axis_dividend_tvalid观察波形。你会在仿真开始后看到s_axis_dividend_tready拉高数据被IP接受随后经过若干个周期m_axis_dout_tvalid才拉高m_axis_dout_tdata输出一个16位数据其中高8位是商14低8位是余数2。如果你不是做定点的场景这里低8位的余数可能并不需要直接取高8位即可。这个仿真看起来顺理成章真正坑爹的地方在于如果你在第二个除法还没结束时就再次拉高了输入有效信号而IP的tready恰好拉低你的新数据会被直接丢弃。所以设计数据源时不能想当然地一直送数必须用tready做握手判断。推荐写一个简单的状态机tvalid的拉高由tready控制这样才不会丢数。3.3 输出对接怎么安全地把结果写进FIFO或BRAM拿到m_axis_dout_tvalid后就要把数据接进自己的数据通路。最常见的一种做法是直接接到一个异步FIFO的写使能上数据过来就写进去。但这里有个隐藏细节m_axis_dout_tdata因为是拼接的商和余数位宽是商宽余宽。如果你只关心商不要直接对整条总线做截断而是明确取出对应高位避免位序搞错。另一个细节是除法器输出的VALID信号通常只有一个周期有效如果后端缓存模块要求更长的写使能需要自己用寄存器把VALID展宽一拍。在更复杂的系统中除法器后面常常跟上定点缩放、四舍五入、饱和处理等逻辑。建议不要在除法器输出后立刻做太多组合逻辑否则容易把IP好不容易优化好的时序再次拉坏。正确的做法是除法器结果先打一拍进寄存器再做后续处理。你可以把这次打拍看作给后级逻辑一个稳定的数据窗口。3.4 用ILA还是仿真波形排查怎么选调板子的时候很多人习惯直接在Vivado里用ILA抓信号。对除法器这类时序敏感、延迟不固定的模块ILA抓波形时要重点观察两组信号的相对关系输入端tvalid/tready的握手节奏和输出端tvalid的拉高时刻。只要这两组信号在时间轴上的顺序是对的商值基本不会出错。如果抓出来的商值偶尔对、偶尔错先不要急着怀疑算法先看看是不是上游数据没等握手信号、下一次数据把上一次覆盖了。仿真阶段如果发现结果不对我更推荐用脚本化testbench来批量验证。比如写个循环生成10000组随机被除数和除数自动比对IP输出和参考模型比如C语言的整数除法结果哪个不对就打印出来。我在实际项目中做过这种验证最后抓出来的问题全是位宽截断和VALID时序不匹配没有一例是IP核本身算错了。用随机测试向量验证比盯着一两个波形找问题效率高几个量级。4. 常见问题排查与调试技巧实录4.1 商和余数总是错位问题出在哪儿遇到这个现象我先按优先级排序排查第一步确认位宽配置查看输出总位宽是否按“商位宽余数位宽”拼接第二步确认输入数据对齐有符号数要先扩展到IP要求的位宽不能直接接未定义符号的原始总线第三步查看VALID信号时序。我见过一个很典型的错误有人把IP的输出直接接到寄存器数组在每个时钟沿无条件采样结果由于除法器本身延迟会变化采到的值经常是上一轮或下下轮的旧数据。正确的做法是用m_axis_dout_tvalid来作为寄存器的写使能而不是无条件打拍。另外如果使用Fractional模式要特别注意小数位宽和商位宽在输出总线中的位置。很多IP会把Fractional结果放在低位商放在高位拼接规则和Remainder模式并不一样。所以拿到用户手册后第一件事不是看波特图而是看输出总线的位定义表。4.2 被零除会发生什么为什么结果看起来像全1FPGA的除法器IP核在数学上不会检查除数是否为零。仿真里如果divisor0输出的商可能变成一个近似全1的最大值或者不定态这取决于内部算法实现反正绝不会是你期望的结果。这里要记住一个原则IP核不负责业务逻辑上的错误处理它只负责把输入当成合法的数值来算。所以在上游加一个divisor0的判断一旦发现除数为零就绕开除法器直接输出预设的安全值这样系统才不会在异常数据面前崩溃。我在做图像处理时遇到过这类坑某帧图像的坐标缩放比例因子是外部寄存器配置的现场调试时寄存器写入异常变成了0整个图像输出直接变成一片惨白。后来我在除法器之前加了阈值保护除数为零时把结果置为一个固定倍率问题才算从根上解决。4.3 配置了Latency很多为什么时序还是收不住有些同学觉得Latency设高一点相当于给了布局布线更多余量时序就一定好。这个想法里有对的成分但不全面。Latency高意味着IP内部寄存器链更长数据路径确实被切得更碎时序容易收敛。但如果整个系统工作频率很高除法器输出的这一长串寄存器的扇出也会变大驱动后级逻辑的路径依然可能成为关键路径。此时需要考虑的是在输出端做寄存器隔离也就是再打一拍更彻底的做法是降低除法器的位宽或改用Radix-2算法。这两个方法都是直接从资源消耗和逻辑级数上做减法比盲目调高Latency更有效。如果设计中多个模块共享同一个除法器还要注意仲裁逻辑带来的额外路径延迟那种情况下给除法器输出端加一个小型寄存器堆缓存结果能明显改善时序。4.4 仿真通过上板后偶尔出错大概率是X态问题仿真环境里大家习惯把复位信号拉一下然后开始正常流程X态并不总能看到。上板后如果信号没有有效的初始值除法器的握手逻辑就可能进入一个不确定的状态导致tready、tvalid的时序在某个周期突然异常。尤其在跨时钟域场景下输入FIFO的读使能、空标志这类信号如果有X态传播进来除法器的输入侧数据就会变成不定态。我自己的习惯是仿真testbench里故意模拟一次缺失复位的场景让所有信号初始化为X看设计是否还能自动恢复。如果设计从异常状态回不到正常状态就说明缺少了必要的复位依赖。另外Vivado里打开综合选项中的-flatten_hierarchy时有时某些寄存器的初始化值会被忽略导致综合前后行为不同遇到这类问题可以在IP核配置里把复位策略设成同步复位避免跨时钟域的异步复位释放问题。4.5 一条又快又稳的调试流程建议跑通一个带除法器IP核的模块我的顺序是先在纯仿真环境里用随机向量验证功能确认输出拼接和VALID时序符合预期然后上板用ILA抓真实的握手波形放在数据量较小、异常概率较高的场景下跑最后做全速长时间压力测试观察是否存在偶发性错误。随机向量验证阶段一般就能抓出八成的配置和位宽问题ILA阶段主要抓集成问题压力测试则是看时序余量和握手逻辑是否真的稳健。调试时再分享一个小技巧在testbench里加一个“结果比较器”每收到一个VALID就把当前输出和软件模型计算出的期望值做一次比较不一致就打印出当时的输入、输出和序号。这个模块用几行Verilog就能写完却能省掉大量手动对波形的时间。用自动比对代替肉眼观察这句话是FPGA调试中少走弯路的万能钥匙。4.6 除法器IP核还能怎么扩展除了一步一步按需调用工程中还可以把除法器封装在自己写的功能模块后面做成一个“带使能的定点除法单元”。做法是在内部例化一个Divider IP外部对自己定义好start、din_ready、dout_valid这样的自定义接口这样上层模块就完全感知不到IP核本身的握手细节了。后续如果要把Radix-2换成Radix-4或者把被除数和除数的位宽扩展只需要修改内部封装即可上层逻辑一行不用动。如果把这种封装思维再往前推一步还可以在封装层做基于“查倒数表一次牛顿迭代”的高速近似除法用来处理对精度要求不高的场景。虽然Vivado自带Divider IP已经很好用但在系统频率非常高、除法器成为瓶颈的时候定制一条y x * (1/d)的数据通路往往比硬啃IP核更灵活。这些都是后话先把基础IP核玩透再谈定制不迟。
返回列表