ARTICLE DETAIL

资讯详情

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

FPGA向量乘法器设计全解析:Booth编码、Wallace树与流水线技巧

FPGA向量乘法器设计全解析:Booth编码、Wallace树与流水线技巧 1. 需求拆解先别急着写代码想清楚“向量乘法器”到底要干什么“向量乘法器设计”这个标题看着简单但真动起手来你会发现不同场景下要做的产品完全是两码事。我接手过好几个类似的项目需求有做课程设计的有做FPGA加速的还有做ASIC里DSP单元的同样是“向量乘法器”侧重点可以差出十万八千里。先给个最基本的定义向量乘法器本质上是同时对一组两个以上输入数执行乘法运算的硬件单元。它和普通标量乘法器最大的区别在于“并行度”——不是一次乘一对数而是把多个乘法操作放到同一个时钟周期里并发完成。常见的形态有点积运算单元把两个等长向量的对应元素相乘后累加输出一个标量。这是神经网络推理里最频繁的操作全连接层、卷积层都要用到。逐元素乘法单元两个向量对应位置相乘输出仍是一个向量。例如图像处理里的加权融合、信号处理里的加窗操作。矩阵乘法子单元本质上是多个点积的组合但结构上会复用乘法器的结果做累加考验的是数据复用与调度。所以接到这个题目第一件事不是打开Verilog编辑器而是和提出需求的人把下面几个问题对齐第一位宽是多少输入是8位、16位、32位还是自定义浮点数这直接决定乘法器面积和时序。8位乘8位在FPGA上就是LUT里塞几个查表的事32位乘32位就得考虑DSP48硬核怎么分配了。如果是浮点数还要明确格式——IEEE 754单精度、半精度还是自定义定点格式格式一变设计难度完全不同。第二输入输出格式是定是浮定点数又要区分整数、小数位在哪里浮点数要处理阶码对齐和尾数规格化逻辑会多出好几倍。很多新手一上来就选浮点结果卡在对齐和舍入上不如先做定点版本跑通流程再扩展。第三时钟约束多少这是一个非常容易被忽视但决定方案选型的问题。如果目标时钟只有50MHz直接暴力组合逻辑乘法完全没问题但如果要求跑300MHz以上就必须考虑流水线切级、Booth编码、Wallace树这些优化手段。换句话说时序要求决定了你要不要“炫技”。第四是纯组合逻辑输出还是需要流水线纯组合逻辑简单直观但路径延迟高流水线能提高吞吐率但引入latency需要配合外部时序做同步控制逻辑会复杂一些。第五目标器件是什么如果是Xilinx/AMD的FPGA乘法器可以直接映射到DSP48E1/E2硬核上如果是Altera/Intel的平台对应的是DSP Block。用硬核和用LUT搭乘法器资源占用、功耗和代码风格都完全不一样。ASIC方向则可以针对标准单元库做逻辑综合优化。这些参数不敲定后面所有设计都是白做。我见过有人拿到题目就开写写完发现位宽需求变了整个数据通路推倒重来。所以项目的第一步永远是“需求反确认”哪怕标题看起来再简单也要把这个环节做扎实。2. 算法选型背后的门道Booth编码、Wallace树和移位累加怎么挑确定需求之后进入最核心的算法设计阶段。一个向量乘法器的性能上限其实在你选定算法的那一刻就基本决定了。这一节我把三种最常见的乘法实现思路拆开讲直接对比它们的优缺点和适用场景方便你做选择。2.1 朴素移位累加最适合入门但不适合向量场景移位累加法是最原始的乘法实现把乘数逐位扫描如果当前位是1就把被乘数左移对应位数后累加。例如计算 1011 × 0110需要扫描4个比特位产生最多4个部分积再逐个累加。这个方案的优点是逻辑极其简单写起来不容易出错DEBUG也容易缺点是乘法的时间复杂度是O(N)N是位宽。对单次乘法来说可能还能接受但向量乘法器一次要跑几十上百路乘法如果每路都这样串行累加延迟会变成灾难。适用场景教学演示、位宽小于8位的简单控制逻辑、以及对延迟完全不敏感的功能验证模型。在真正的向量运算单元里我基本不推荐这个方案。2.2 Booth编码不是必须的但能减一半部分积Booth编码的核心思想是通过重新编码乘数让部分积的数量从“位数数量”减少约一半从而减少累加次数和硬件加法器数量。它利用了二进制补码的数学性质——连续的“1”序列可以用一次加法和一次减法来表达。举个例子乘数011110可以看成100000 - 000010这样本来要加4次的部分积变成了一次加、一次减逻辑上少了两级。Booth最常用的变体是Radix-4 Booth编码也称为Booth-2它每两位乘数产生一个部分积部分积数量从N直接降到N/2同时还能直接处理有符号数无需额外的符号修正逻辑。Radix-4 Booth编码的规则表如下乘数位组合b[2i1], b[2i], b[2i-1]操作0000001被乘数1倍010被乘数1倍0112×被乘数100-2×被乘数101-被乘数-1倍110-被乘数-1倍1110从表格可以看到部分积只有五种可能性0、±被乘数、±2×被乘数。其中±2×被乘数在二进制里就是左移一位再加符号处理实现起来代价极低。这也是为什么Radix-4 Booth在工业界的乘法器设计里被大量使用——部分积数量减半加法树层级减少面积和时序双受益。适用场景位宽较大16位以上、对面积和频率有双重要求的乘法器设计。如果你的向量乘法器是在FPGA上用DSP硬核实现Booth编码的作用会被硬核内部的结构削弱因为DSP48本身就内置了优化的乘法器结构外部再做Booth反而多此一举。但在ASIC设计或使用通用逻辑资源实现时Booth几乎是必备优化。2.3 Wallace树与压缩器并行累加的关键结构有了部分积接下来就是把它们加在一起。朴素的做法是把部分积串行累加像连加一样延迟随部分积数量线性增长。Wallace树的思路则完全不同它利用全加器FA的半加性质把三行部分积压缩成两行——即全加器输入三个位、输出一个求和位Sum和一个进位位Carry行数和深度都大幅减少。具体来说Wallace树的操作过程可以这样理解假设你有8个部分积每个部分积是16位宽朴素累加需要7级加法每级进位链都很长Wallace树则分多轮压缩每轮都把三行变两行经过大约log_{3/2}(N)轮之后所有部分积变成两行最后再用一个超前进位加法器CLA或进位选择加法器CSA把两行合并成最终结果。Wallace树的好处是加法深度成对数级别下降关键路径大幅缩短坏处是版图布局不规则走线拥挤在后端物理设计阶段可能增加布线压力。所以工程上常常会做一个折中变形——平衡压缩树balanced compressor tree在延迟和布线规则性之间取平衡。2.4 我的选型建议用一句话总结我的经验如果你只是需要“能用”直接调用FPGA的乘法器IP核如果你要自己写RTL优先选择“Booth编码 压缩树/级联加法器”组合如果位宽小于等于8位直接查表或移位累加就足够别折腾高深算法。以16位向量乘法器为例我倾向的结构是先用Radix-4 Booth把部分积数量从16降到8再用3级Wallace压缩树把8个部分积压成2行最后用超前进位加法器收尾。这套组合在逻辑深度、面积和规则性上都比较均衡。当然具体到FPGA平台我建议先用IP核的时序和资源报告作为基准再去对比自研方案的收益。很多时候你会发现自研乘法器在FPGA上打不过硬核DSP那自研的意义就不大不如把精力花在数据通路和数据复用上。3. RTL实现与关键细节数据通路、符号处理、流水线插入算法选型定下来之后就到了写代码的阶段。这一节我会按一个完整的设计流程来拆解从顶层数据通路规划到关键模块的实现细节每一步都给出可以直接参考的做法。3.1 顶层数据通路设计向量乘法器的顶层结构一般由三个部分组成输入缓冲与格式对齐、并行乘法阵列、输出累加与后处理。输入缓冲的作用是把外部送入的向量数据锁存防止数据在组合逻辑中漂移。格式对齐则是把不同量化位宽的数据统一到一个内部计算位宽。这里我特别提醒一下内部计算位宽的设定直接影响精度和资源。以两个16位定点数相乘为例结果位宽是32位如果还要累加K个数累加器至少需要 1616log2(K) 位否则会发生溢出截断。很多刚接触数字设计的人只记得乘法结果位宽翻倍却忘了累加还需要额外位结果在仿真里出现莫名的不对齐错误。并行乘法阵列是这个设计的心脏。如果是8路并行的向量乘法数据通路上就应该例化8个乘法器每个乘法器独立输出部分积或最终积。这一层非常规整可以用generate语句批量例化genvar i; generate for (i 0; i 8; i i 1) begin : mul_chain wire [31:0] product_i; multiplier_16x16 u_mul ( .a(a_vec[i*16 : 16]), .b(b_vec[i*16 : 16]), .p(product_i) ); assign acc_in[i] product_i; end endgenerate注意这里用的是Verilog的“:”语法进行位段选择比手动拼接要安全得多尤其在处理循环变量时不容易写错位。这个细节不贵但能帮你省一小时查错时间。输出累加部分要根据运算类型决定。逐元素乘法不需要累加直接输出每个乘积点积运算则要把所有乘积相加这个加法可以用一个加法树又是Wallace或平衡树的思路也可以用级联加法器。级联加法器代码写起来简单但延迟较高如果你对频率有要求加法树是必然选择。3.2 有符号数处理最容易踩的坑在RTL里处理有符号乘法有几个常见的坑我一个个说。第一乘法运算符和符号有符号无符号。Verilog的“*”运算符在无符号数乘法时会产生正确结果但两个有符号数直接相乘时结果位宽如果声明不当会出现符号扩展错误。例如两个16位有符号数相乘正确做法是声明为signed类型并且结果位宽为32位logic signed [15:0] a, b; logic signed [31:0] p; assign p a * b;这里如果不加signed声明a和b会被当成无符号数负数变成很大的正数相乘结果完全错误。这几乎是每个新手都会遇到的问题。第二Booth编码中的符号扩展。如果用自定义Booth编码实现有符号乘法部分积的符号扩展必须格外小心。Radix-4编码在遇到负部分积-被乘数或-2×被乘数时需要对补码进行正确的符号扩展才能最后累加出正确结果。我建议在写编码逻辑之前先用Python或MATLAB建立一个定点模型把各种边界值比如-32768、-1、0、32767都跑一遍确定补码位宽和符号扩展的规则再翻译成RTL。直接写RTL再反推真值表太容易翻车。第三累加器溢出。前面提过累加器的位宽应该是乘法结果位宽 累加次数的二进制定点数。举例16位×16位32位做256个数的点积累加器至少是32840位。如果项目需求明确要做饱和截断saturation那么累加器内部可以用40位输出时再按需求截断到32位或16位。中间位宽太少会导致溢出太多则浪费面积这个位数的选择值得多算两步。3.3 流水线插入策略吞吐率与延迟的平衡向量乘法器的数据通路往往很长如果组合逻辑一次性算完路径延迟会很高频率上不去。解决方法是插入流水线寄存器把计算拆到多个时钟周期里。以本设计为例我建议分三级流水线第一级输入寄存 Booth编码。因为编码逻辑只依赖乘数的相邻位延迟不高可以和输入打拍合在一起。第二级部分积生成 第一轮压缩。这是组合逻辑最重的阶段Wallace树的压缩在这里完成一部分。第三级最终加法 累加。前一级剩下的压缩加最终超前进位加法然后进累加器。流水线深度增加会带来Latency输出相对输入的延迟周期数变长所以在划分边界时要注意两点每个寄存器级的组合逻辑深度要尽量均衡否则关键路径会出现在最重的那一级影响整体频率。可以通过时序报告来检查一般工具会直接给出最差路径在哪跟着调就行。流水线级数需要在设计文档里明确记录因为下游的数据校验、状态机设计都会依赖这个参数。后期改动流水线深度等于把时序验证和控制逻辑都推翻一半成本很高。实操中我通常先在无流水线的功能模型上验证算法正确性再插入流水线逐级对比流水线前中后的输出是否一致。这样排查问题会容易很多。3.4 Testbench设计与验证策略验证向量乘法器最怕的是“仿真能过、上板出错”。根因通常是测试向量覆盖不全。这里我给出我惯用的三个测试阶段定向测试先喂最典型的边界值。比如有符号数的最大值、最小值、0、-1、1以及一些容易触发布尔编码特殊分支的组合。这一步确保基本功能不跑偏。随机化测试用SystemVerilog的随机约束或Python脚本生成几千组随机输入与C模型/参考模型对比结果。对于16位×16位乘法穷举所有输入不现实但随机验证加边界值组合可以覆盖绝大多数路径。形式化验证或断言监控在RTL里插入SVA断言监控乘法器输出是否在指定周期数内到达以及是否存在x态传播。实际项目里x态传播是最隐蔽的bug往往因为复位时序问题冒出来断言能帮你快速定位。如果在FPGA上验证我还建议用ILA集成逻辑分析仪抓取实际运行时的中间信号看看是否存在亚稳态或复位不同步的问题。这一步不是必须的但能帮你提前感知上板后的时序问题比等板子回来再查省心得多。4. 常见问题与排查技巧实录时序不收敛、资源爆炸、仿真对但上板错这一节整理我实测下来最常见的几类问题每一条都是真实踩过坑后的经验。新手到这一步常常抓瞎希望这些记录能让你少走弯路。4.1 时序不收敛频率始终上不去这是自研乘法器最常见的痛点。明明功能仿真都对一综合频率就是达不到要求。我的排查顺序是这样的先看关键路径在哪个模块。综合工具会给出时序报告找到最差的路径看它是落在部分积生成、Wallace压缩还是最终加法器。如果落在最终加法器说明加法器结构不够优化可以换用更快的加法器结构例如从行波进位加法器换成超前进位加法器如果落在Wallace压缩网络说明压缩级的逻辑深度太大考虑在压缩过程中插入流水线寄存器。再看位宽是否分配合理。有时候内部计算位宽比需求多出很多位导致进位链逻辑变长白白拖慢路径。适当截断不需要的冗余位前提是保证精度不损失可以显著改善时序。这个工作需要做bit-true仿真来验证截断后的结果误差在可接受范围内不能拍脑袋。最后考虑寄存器复制。如果乘法器的某个输入扇出特别大比如一个向量元素被多个乘法器使用会形成严重的扇出瓶颈。解法是在RTL里手动复制这个输入寄存器让每个乘法器从就近的寄存器取数降低单个节点的负载。FPGA综合工具一般也会做寄存器复制优化但手动指定关键信号往往效果更直接。4.2 资源占用爆炸乘法器比预想的大得多很多人在FPGA上实现向量乘法器后发现LUT消耗远超预期。这里有两种常见原因原因一位宽没有合理利用DSP48硬核。Xilinx的DSP48E1原生支持25×18有符号乘法但如果你把两个22位以上的数相乘硬核一次可能放不下工具会拆成多个DSP拼接资源就上去了。解决办法是在需求允许的前提下把位宽裁剪到DSP48能单核处理的范围或者合理拆分为高低位部分积再合并。原因二综合器把乘法器搭在了LUT上而不是DSP上。有时综合策略会默认“节省DSP”优先用逻辑资源实现乘法导致LUT暴涨。在Vivado里可以通过综合属性或约束强制让乘法器映射到DSP硬核set_property USE_DSP48 {Auto} [get_cells mul_chain_gen*]如果项目中乘法器数量很多更建议直接用IP核来例化IP核内部已经做好DSP映射和时序优化省心得多。我见过不少“自研原教旨主义”的人死磕LUT乘法器在FPGA上跟硬核资源较劲最后频率和面积都没讨到便宜实在没必要。4.3 仿真正确上板后结果不对这个问题的典型元凶是复位时序不一致。仿真模型里所有寄存器都是同步复位但FPGA内部的全局复位网络如果被异步释放或者复位释放时刻不满足时序要求就会导致部分寄存器状态不确定输出全乱。排查方法是在仿真里对复位信号做“异步断言、同步释放”处理同时在板级测试时用逻辑分析仪抓复位信号的实际翻转波形确认其干净稳定。另一个常见元凶是输入数据没有和时钟沿建立/保持关系对齐。外部送入的数据如果相对于内部时钟有偏差就可能采到毛刺或中间态。解决办法是在顶层对输入数据先做两级寄存器同步打拍再进行运算。这个简单的“打两拍”操作能杀掉90%以上的上板数据不稳定问题几乎是所有外部接口的标配。4.4 精度不够输出和数学期望对不上定点乘法器的一个天然问题是量化误差。如果你在软件模型里用浮点数算出一个参考结果再和硬件定点数输出比较会发现有差异。这不一定是硬件bug而是定点位宽和舍入方式导致的误差。我建议在设计文档里提前约定误差容忍范围例如“相对误差不超过0.1%”并且在验证脚本里按这个标准比较。如果有符号数出现了反常的大误差先检查是否发生了溢出如果只是小幅偏差那就去检查舍入策略——是做截断还是四舍五入是否做了饱和处理。定点数设计就是“三位一体”的活位宽、舍入、饱和每一项都要有意识地设计和记录。5. 工具链选型与辅助脚本用Python搭建参考模型效率翻倍这一节属于加分项。很多做硬件的人习惯拿到需求就直接开写RTL但我强烈建议先花十几分钟写一个Python参考模型后面所有验证都拿它当“标准答案”。这件事带来的效率提升是巨大的。5.1 为什么需要参考模型RTL代码本身的抽象层次较低直接看代码很难判断逻辑是否正确。而Python/C这类高级语言可以用非常接近数学定义的写法来描述同一种运算。只要两边输入数据相同、位宽和舍入规则一致参考模型的输出就可以作为RTL仿真的“黄金值”。所有的随机测试、边界测试都可以用脚本自动化对比而不是靠人瞪着眼睛看波形。5.2 一个简单的定点乘法参考模型以16位有符号定点数乘法为例Python模型可以这样写import random def saturate(value, bits): max_val (1 (bits - 1)) - 1 min_val -(1 (bits - 1)) return max(min_val, min(value, max_val)) def mul_16x16_signed(a, b): # 输入为16位有符号整数输出为32位有符号整数 return saturate(a * b, 32) # 生成测试向量并输出hex格式供Verilog testbench读取 for _ in range(1000): a random.randint(-2**15, 2**15 - 1) b random.randint(-2**15, 2**15 - 1) p mul_16x16_signed(a, b) print(f{a 0xFFFF:04X} {b 0xFFFF:04X} {p 0xFFFFFFFF:08X})这段脚本把输入按16位补码寄存输出按32位补码寄存和RTL的行为完全一致。把生成好的向量文件塞给Testbench做仿真再用脚本比对输出整个过程不需要人肉看波形几千个用例几分钟跑完。5.3 定点量化的精度预估脚本如果你在决定内部位宽可以用一段Python脚本模拟不同量化方案下的误差分布import numpy as np def fixed_point_error(data, int_bits, frac_bits): scale 2 ** frac_bits quantized np.round(data * scale) / scale overflow_mask np.abs(quantized) (2 ** int_bits) quantized[overflow_mask] np.sign(quantized[overflow_mask]) * (2 ** int_bits - 1 / scale) error np.abs(data - quantized) return error.max(), error.mean() # 假设数据服从[-1, 1]均匀分布 data np.random.uniform(-1, 1, 10000) max_err, mean_err fixed_point_error(data, 2, 13) print(f最大误差: {max_err:.6f}, 平均误差: {mean_err:.6f})这种脚本的价值在于它让你在写RTL之前就知道选哪个位宽能达到精度要求而不是等硬件做完了发现精度不够重新改位宽。硬件设计的返工成本远比软件高前置验证的收益非常可观。我个人在实际项目中的经验是RTL开发本身只占项目周期的大约三到四成其余时间几乎都花在验证、调时序和排查边界情况上。一个Python参考模型 一套自动化比对脚本能把验证时间压缩到原来的三分之一这种投入非常值得。6. 后续扩展方向与个人经验总结向量乘法器做完往往只是更大系统的起点。如果你有兴趣继续深入有几个方向我认为很有价值向量乘法器扩展到矩阵乘法单元。当你的向量乘法器具备一定规模之后很自然的下一步就是矩阵乘法。矩阵乘法本质上是多个向量点积的组合但难点在于数据的组织和复用。外部数据通常是按行/列存储的存储访问模式和运算单元的映射关系需要精心设计。此时你手头的向量乘法器可以作为“计算内核”但总线带宽、缓存调度和累加逻辑都要重新设计。浮点支持。从定点点积扩展到浮点运算要处理的逻辑会多很多尾数乘法、阶码对齐、舍入模式、规格化、异常处理等等。建议先在IEEE 754格式上做功能仿真模型再分模块转RTL。我见过不少团队在这里栽跟头主要原因是把浮点运算想得太简单没有预先设计好特殊值NaN、Inf、零的处理流程。更高性能的方向脉动阵列Systolic Array。如果你想做一个真正高性能的矩阵运算单元脉动阵列是绕不开的结构。它会要求向量乘法器单元之间具备相邻通信能力而不仅仅是独立完成单次乘法。这种结构在Google TPU和大量NPU设计里都有应用也正是“向量乘法器”这个基础组件在更大系统里发挥核心作用的经典形态。最后分享一点个人的工作习惯在项目启动时我会在文档里固定一张“设计参数表”把位宽、符号类型、时钟频率、流水线级数、目标器件、面积/功耗约束全部列清楚。每做一处设计决策都回到这张表确认一下有没有偏离。向量乘法器看起来是一个很小的模块但它牵涉的决策点一点不比大系统少需求一变牵一发动全身。把这张表钉在项目最前面能避免大量回头返工的麻烦。
返回列表