ARTICLE DETAIL

资讯详情

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

FPGA除法器IP核使用指南:Vivado Divider Generator配置与调试

FPGA除法器IP核使用指南:Vivado Divider Generator配置与调试 做FPGA时间久了会发现一个很有意思的现象加法、减法、乘法在Vivado里都有非常成熟的IP核随手一拖就能用偏偏到了除法这儿很多人宁愿自己写移位减法的状态机也不愿意去碰Divider Generator这个IP。我早些年也这样觉得除法器IP核配置界面复杂延迟参数看不懂接口时序还容易踩坑结果自己撸的除法器又慢又费资源还经常因为时序收敛问题被后端同事吐槽。后来花了两三个项目把Vivado里的除法器IP核彻底吃透之后才明白这东西其实一点都不难关键是你得搞清楚它的算法本质、参数含义以及AXI4-Stream接口的握手规则。这篇文章我会从工程应用的角度把Xilinx FPGA的除法器IP核Divider Generator在Vivado里的配置、调用、仿真到板级调试整个链路完整拆一遍。内容包括算法类型选型、位宽和延迟设置、接口时序分析、RTL代码示例以及我实际调试中遇到的各种坑和排查方法。适合刚入门FPGA但已经被除法折磨过的朋友也适合已经用了IP核但始终没搞明白某些信号和参数为什么会这样表现的老手。1. 为什么非要专门用除法器IP核背景与方案选型1.1 在FPGA里做除法没那么简单数字电路里的加法、减法、乘法本质上都是若干时钟周期内可以完成的组合逻辑或者简单流水线操作。但除法不一样它的计算过程天然是迭代的跟笔算除法非常相似每一位商都取决于上一步的余数与除数比较的结果。这意味着如果直接用组合逻辑实现除法延迟会非常大而且位宽稍微上去一点路径延迟立刻爆炸时序根本收敛不了。FPGA工程师最常用的替代方案是移位减法也就是在状态机里反复做“余数左移一位、减去除数、判断够不够减”这三个动作。这个方案好在资源占用少坏处是延迟和位宽成正比而且代码写起来麻烦需要考虑符号处理、余数修正、除数为零保护等一堆边界条件。实测下来一个32位无符号除法用状态机实现最坏情况需要32个时钟周期中间还要处理各种中间变量的位宽对齐调试起来非常痛苦。Xilinx的除法器IP核Divider Generator就是把上面这些麻烦事全部封装好了。它可以根据你选择的算法类型自动生成一个或者多个除法器的硬件实现提供AXI4-Stream接口配合简单的握手信号就能实现流水线除法。你不需要关心内部是Radix-2迭代还是查表近似只需要配置好位宽和延迟把数据从TDATA送进去等若干个时钟周期后从输出端把结果取出来就行。1.2 除法器IP核和手写状态机的真实差距用实际数据说话我曾经维护过一个图像处理项目里面需要对每个像素做归一化归一化公式里有一个被除数是16位、除数是8位的无符号除法。早期版本是前辈用移位减法状态机实现的时钟跑到100MHz没问题但要占用接近200个LUT和一组状态寄存器而且每算完一个除法需要卡住流水线等待16拍整个图像数据的吞吐率被拉低了接近一半。换成Divider Generator IP核之后同样位宽的除法选择Radix-2算法In AXI接口延迟配置为默认值资源占用反而更少而且因为IP核内部是流水线结构一旦灌入第一批数据后面每个时钟周期都能进一个新的除法运算吞吐率直接打满。从工程角度看这么做节省的不只是开发时间还有FPGA内部宝贵的逻辑资源以及流水线的连续性问题。当然我并不是说手写除法就完全没价值。如果项目只需要极偶尔的除法操作比如每秒一次参数更新那确实不需要引入IP核状态机反而更灵活。但只要你确认系统里有持续的除法运算需求或者说希望代码风格统一、便于后期维护用Divider Generator绝对是更理性的选择。1.3 Divider Generator和其他算术IP的配合方式有一点需要特别提醒Divider Generator虽然叫除法器但它输出的是商和余数而不是浮点数。实际工程里经常需要把除法结果转成定点数再参与后续计算所以它往往和定点数处理逻辑、AXI数据通路绑定在一起。比如你在做电机控制或者电力电子控制算法时经常要算类似于“电压值乘以比例系数再除以测量量程”的式子这种场景下除法器IP核输出的原始商值还需要你自己做一次定点缩放才能保证数值精度。所以在规划阶段你就要想清楚整个数据链路上定标Q格式是怎么设计的。是被除数、除数都以Qm.n格式输入然后商也按某种Q格式解释还是用整数除法再把余数通过浮点或者定点转换模块处理这个决定了你IP核界面里Fractional Width参数要不要设、设多少后面我会专门讲这个问题。2. Divider IP核配置细节在Vivado里一次点对的5个关键参数2.1 算法类型选型Radix-2、High-Radix与LUTMult打开Vivado的IP Catalog搜索Divider会看到Divider Generator这个IP核。双击进入配置界面后第一栏Algorithm Type有几种选择最常用的三个是Radix-2、High-Radix和Radix-2 LUTMult。很多人直接默认选Radix-2就不管了其实这三者的适用场景差别很大。Radix-2是逐比特迭代的算法类似我们手写二进制除法的过程每个周期完成一位商的计算。它的核心优势是逻辑简单、布线友好时序容易收敛时钟频率可以跑得比较高。缺点是延迟会随位宽线性增长比如32位除法延迟就是几十拍起步。适合大多数通用场景尤其是位宽不太高、频率有要求的项目。High-Radix高基数算法本质是在一个周期内处理多位商比如Radix-4就是一拍算2位商。基越高延迟越短但组合逻辑越复杂频率会掉下来。这个模式适合位宽非常大比如64位以上而且你又对延迟特别敏感的场景。我个人在图像处理里很少用到但在通信基带算法里见过别人用它做高吞吐的定点除法。Radix-2 LUTMult则比较特殊它是用查找表和乘法器资源来加速除法的一种算法在IP核内部会用到DSP48。延迟很低适合对延迟要求极其苛刻的场景。代价是资源消耗相对高而且对位宽有一定限制。通常除法位宽在16位以内时这个模式效果很明显一旦超过24位资源膨胀很快需谨慎。选型的核心逻辑就一条在满足时序收敛的前提下优先选Radix-2因为它最稳如果延迟不能满足系统要求再考虑LUTMult或者High-Radix。我建议新手直接用Radix-2先把功能跑通再根据实际资源时序数据去微调。2.2 设置数据位宽和Fractional Width别搞混了配置界面里的Dividend Width是被除数的位宽Divisor Width是除数的位宽。这两个确定后输出的商位宽默认等于被除数位宽余数位宽默认等于除数位宽。这个规则其实很容易理解用n位二进制数除以m位二进制数商最多还是n位余数一定小于除数所以需要m位表示。很多人在这一步会忽略Fractional Width这个参数。这是个什么东西呢它表示在被除数的最低位后面还挂着多少位小数位。比如你设置被除数位宽16Fractional Width是8那实际上输入数据的解释方式是Q16.8也就是说16位里面整数部分占8位小数部分占8位。除法器IP核在做除法的时候会先把被除数左移Fractional Width位再做整数除法输出的商其实相当于左移过之后的定点结果低Fractional Width位就是小数部分。举个例子你想算3.5除以2但没有浮点单元。可以把3.5表示成Q8.8的整数形式即3.5乘以256等于896。然后除数是2按整数除法896除以2等于448。448在Q8.8格式下解释为448除以256等于1.75。但如果你直接用普通整数除法3除以2只会得到1小数部分直接丢了。所以Fractional Width本质上是通过预缩放把定点小数的精度保住。做图像归一化、控制算法时这个参数几乎是必用的。我踩过的坑是明明在IP核里设了Fractional Width但输出端忘记把结果的低几位当成小数来用结果数值总是差一个固定的倍数查了好久才反应过来是定标没有匹配上。建议在RTL注释里明确写下Q格式方便后续接手的人也不会搞错。2.3 延迟Latency的计算与管理在配置界面里有一个重要参数叫Latency表示从输入数据被采样的那个时钟周期开始到输出数据有效需要经过多少个时钟周期。不同算法类型、不同位宽下默认延迟值是不一样的。你可以选择使用默认值也可以手动指定一个值只要它小于最大值且大于最小值就行。延迟这个东西如果你的系统是先用除法结果再去控制其他数据流就必须保证大家的时间对齐。很多初学者在写代码时直接从TDATA接进数据等到第N拍去读TResult结果N算错了读出来的结果完全是乱的。一个很笨但很有效的办法是在仿真的时候用一个计数器从数据有效开始计数等到Latency值等于你配置的延迟数时去采样输出信号看对不对。如果对不上优先怀疑延迟配置。这里还要提醒一点在IP核里你看到的Latency数值包含输入寄存器那一拍。也就是说如果你在逻辑里额外打了一拍那实际对齐的延迟还要再加1。这一点很多人会漏掉导致仿真通过但上板时序错位。3. AXI4-Stream接口与握手信号这才是高效应用的核心3.1 从S_AXIS_A到M_AXIS每个信号是干什么的Divider Generator IP核最常用的接口是AXI4-Stream它有一组从端S_AXIS_A用于被除数S_AXIS_B用于除数和一组主端M_AXIS_DOUT用于输出商和余数M_AXIS_DOUT还有一组可选的状态标志。这套接口的核心不是数据本身而是TVALID和TREADY这一对握手信号。先说S_AXIS_A_TVALID和S_AXIS_A_TREADY。当你准备好给除法器提供被除数数据时把S_AXIS_A_TVALID拉高同时把数据放到S_AXIS_A_TDATA上。除法器IP核内部如果准备好了会把S_AXIS_A_TREADY拉高这时候TVALID和TREADY同时为高数据就被成功采样进去了。这个握手规则和AXI总线完全一致简而言之就是谁都不能拉着对方干等两边必须同时在“有效”状态数据才算真正传输。S_AXIS_B_TVALID和S_AXIS_B_TREADY同理只是用来传输除数。你们可能会问为什么被除数和除数要分开两个通道因为除法器需要保证在采样到被除数和采样到除数时它们是配对出现的如果只有一个通道还得内部做对齐逻辑很容易出错。分开通道可以让IP核内部的流水线自然对齐两路数据。输出端M_AXIS_DOUT_TVALID表示输出结果有效M_AXIS_DOUT_TREADY信号表示你能不能在当前拍接收结果。如果你一直拉高M_AXIS_DOUT_TREADY那相当于输出是自由流动的每个时钟周期只要结果算出来了就直接推给你。整个数据通路中还有一个ACLKEN信号这是时钟使能信号当它为低时IP核内部所有寄存器暂停更新。一般来说不使用这个信号把它恒拉高即可。如果你项目里需要动态暂停流水线就可以通过这个信号来冻结IP核状态。3.2 完整仿真示例看懂数据通路的拉齐关系这里分享一段我用在工程里的实际调用代码模块是一个简单的除法计算模块把连续输入对的被除数和除数做除法商和余数直接输出。先看IP核的例化接口我在Vivado里把IP核生成为div_gen_32_16配置是32位被除数、16位除数、Radix-2算法、无符号数、默认延迟。module div_wrapper ( input wire clk, input wire rst_n, input wire [31:0] dividend, input wire [15:0] divisor, input wire din_valid, output wire [31:0] quotient, output wire [15:0] remainder, output wire dout_valid ); wire s_axis_divisor_tvalid; wire s_axis_divisor_tready; wire s_axis_dividend_tvalid; wire s_axis_dividend_tready; wire m_axis_dout_tvalid; div_gen_32_16 u_div ( .aclk(clk), .aresetn(rst_n), .s_axis_dividend_tvalid(din_valid), .s_axis_dividend_tready(s_axis_dividend_tready), .s_axis_dividend_tdata(dividend), .s_axis_divisor_tvalid(din_valid), .s_axis_divisor_tready(s_axis_divisor_tready), .s_axis_divisor_tdata(divisor), .m_axis_dout_tvalid(m_axis_dout_tvalid), .m_axis_dout_tready(1b1), .m_axis_dout_tdata({quotient, remainder}) ); assign dout_valid m_axis_dout_tvalid; endmodule这里把商和余数直接从TDATA的高位和低位拆开来使用。实际使用中TDATA的位宽等于商位宽加余数位宽商在高位余数在低位。如果你配置了Divide by Zero选项最高位可能还有一个除零标志那就需要更细致地按位段拆不能直接用拼接。这一点下面调试章节会再展开。仿真时需要注意的是你给激励时被除数和除数必须在同一个周期同时拉高TVALID否则IP核会认为它们是两个独立的数据对产生错误的配对。手动写状态机时也要保证这一点不要先发被除数再等一个周期发除数那样就错了。3.3 吞吐率优化与多路除法轮询大部分场景下我们只需要把数据连续地送进除法器然后连续地从输出端取结果这种模式叫流水线模式。只要保证输入端的TVALID持续为高且内部没有反压TREADY一直为高除法器就可以做到每周期处理一个新的除法和输出一个结果。延迟多少拍并不影响吞吐率影响吞吐率的是你灌数据的节奏。如果你的系统需要交替计算多路数据的除法比如通道A和通道B各自有除法任务你可以在每个时钟周期切换送A和B的数据输出端按同样的节拍去区分结果属于哪个通道。这个方法我在一个多通道数据采集卡上用过用同一套除法器IP核处理了4个信号通道的归一化省掉了4个IP核的资源开销。唯一的代价是输出端需要一个小的FIFO或者状态机按照周期来路由结果到对应通道的后续逻辑。如果在某几拍你暂时没有数据要算就需要把S_AXIS_A_TVALID和S_AXIS_B_TVALID同时拉低这样输入数据不会被采样。需要注意的是即使输入端没有新数据输出端之前已接收的数据依然会在延迟往后推出结果所以你不应该通过“输出端有没有结果”来判断当前是否有新计算结果而应该额外维护一个计数或者标志。4. 调试实录常见问题与排查技巧4.1 除数为零到底会发生什么除数为零在数学上是未定义操作但在除法器IP核里它不会让你的FPGA崩溃也不会输出NaN这种特殊值。实际仿真时当除数为零时除法器会输出商为全1也就是最大值余数等于被除数本身。听起来很奇怪但其实内部逻辑就是这么实现的因为除数每一位都是0比较器判断“够减”的条件一直不满足商的所有位都被置为1余数当然一直等于被除数。如果你配置了Divide by Zero Detection选项输出端会多出一个标志信号通常在高位。有了这个标志你就可以在系统层面做保护。我在一个测控项目里就遇到了除数为零的情况当时上位机发来的控制参数里碰巧填了0导致后续算法结果全是饱和值电机差点失控。后来就是在除法器后面加了一个判断检测到除零标志时直接把结果强制置为0同时拉高告警信号问题就控制住了。4.2 延迟对齐错误仿真数据总是错位怎么办这类问题太常见了十次用除法器八次都出在延迟对齐上。表现形式是仿真波形里输入端的第一个数据是正确的但输出端第一个正确结果总是不在预期的那一拍出现或者连续输入多个数据时结果和数据错位看起来像“第一个结果和第三个输入是一对”。排查方法很简单我用得最多的是直接在仿真里写一个抛数据计数器。输入有效时计数加一输出有效时计数加一对比两个计数器的差值就能一眼看出实际延迟是多少拍。有了实际延迟再回头检查IP核配置里的Latency以及自己代码里有没有额外打拍。这里有个容易混淆的点IP核界面显示的Latency是从数据被采样的那个时钟到输出有效的时钟差而你在仿真里看的“输入有效到输出有效”的差值如果输入端口的TVALID在数据前就已经拉高了很多拍这个差值就会比Latency大很多不要被迷惑。另外输出端的m_axis_dout_tvalid信号本身就是一个很好的对齐参考你在逻辑里一定是跟这个信号走而不是自己拍数。只要保证跟这个信号走、跟tdata同拍采样就基本不会错位。4.3 余数符号和定点缩放问题有符号除法比无符号除法复杂得多。IP核在有符号模式下商和余数都遵循C语言中截断除法的规则商向零取整余数的符号与被除数相同。这意味着如果你用(-7)除以3商是-2余数是-1而不是数学上更常见的商-3余2。工程计算中如果不注意这一点结果可能会比预期少1。接下来是定点缩放问题。前面提到Fractional Width会把被除数左移后再做除法但余数对应的精度怎么办举个例子你在配置里设置了被除数位宽16、Fractional Width 8除数位宽16。输入3.5表示为896除数2表示为2除法结果是448按Q8.8解释是1.75精度是够的。但如果你关心余数此时余数实际是896除以2的整数余数0按Q8.8解释还是0。如果你输入3.6表示为922保留4位小数的3.6除以2922除以2等于461余0Q8.8解释461除以256约等于1.8008其实已经引入了0.0008的量化误差。要彻底消除这个误差除非被除数的定点表示本身是精确的否则只能接受这个精度损失。很多控制算法走除法都会在这个环节纠结我的建议是能用移位加乘法替代的除法就尽量替代实在替代不了就牺牲一点低位精度换取IP核的通用性。4.4 资源暴增和时序不过的瘦身思路有人在工程里发现除法器IP核一加进来LUT资源消耗特别大甚至发生布局布线不收敛的情况。这种情况我在几年前调一个FIFO深度很大、位宽又高的数据处理模块时也遇到过。当时我把被除数位宽配成了64位除数也配成了64位想着反正用不到那么高的精度先就这么配着。结果IP核在Radix-2算法下生成了庞大的组合逻辑时序直接崩了。后来我把位宽按照实际需要裁剪成32位和16位延迟也从默认值调低了一些资源立刻降了将近三分之二时序也过了。总结下来瘦身有三板斧第一严格按数据的有效范围设置位宽。如果被除数最大就是10000你用15位就够别习惯性写32位。第二算法类型从Radix-2换成Radix-2 LUTMult也许能减小LUT占用但要注意DSP48资源是否够用。第三把IP核的Output Register选项打开或者调整寄存器级别可以改善时序但会增加延迟。综合权衡后往往需要你反复跑几次综合才能找到一个资源和性能的平衡点。还有一个容易忽视的技巧如果你只是偶尔算一个除法完全可以用时钟门控或者状态机控制TVALID让IP核在绝大多数时间处于空闲状态。虽然动态功耗降低不太明显但至少不会在那一拍产生大量翻转对减小局部动态功耗是有帮助的。5. 一个很实用的高阶技巧用除法器IP核实现定点归一化前面说了很多配置和时序问题最后分享一个我最近在项目里用的实际方案。有一个传感器采集系统每次采集到的原始数值范围是0到10000我希望把它归一化到0到255的8位灰度范围方便直接驱动显示器。最常见的做法是乘255再除以10000。这里我没有用浮点直接构造了一个常量除数10000被除数就是原始数值乘以255后的定点表示。把乘法器做完的32位结果直接接到被除数端口除数端口固定接10000然后选择无符号模式Fractional Width设为0。因为5000乘以255等于1275000除以10000等于127.5理论上要输出128但整数除法下商为127余数8750。想要四舍五入可以在被除数那边先加上除数的一半再除也就是原始数值乘以255加5000除以10000。这个技巧本质上就是整数除法的四舍五入非常实用。从硬件资源角度讲一个乘法器加一个除法器两个IP核级联在50MHz时钟下完全没压力。整个流水线如果做到极致从原始数据进来到归一化结果出来延迟总共也就二十多个时钟周期。这个方案如果你用手写状态机来做估计要写一两百行代码还未必能保证兼容后续改动。所以说FPGA工程师的武器库里除法器IP核绝对是你应该熟练掌握的一件趁手工具。用好了不仅能大幅缩短开发周期还能让你的代码更简洁、更稳定、更容易维护。直接从一个普通的定点数据通路改造过来把乘法器输出接到除法器输入端中间省掉一堆手动对齐逻辑这就是IP核给你带来的红利。下次再遇到除法需求别再头铁自己写了先把Vivado里的Divider Generator翻出来按照这篇文章的配置思路过一遍你会发现它其实比你想象中友好得多。
返回列表