1. 从“*”号到“芯”路为什么有符号数乘法是个技术活刚接触Verilog那会儿我觉得乘法器设计挺简单的不就是个“*”号的事儿吗后来真刀真枪做项目尤其是在做音频处理芯片和运动控制单元时才发现自己太天真了。当你的输入数据既有正数又有负数比如温度传感器的读数-20°C到50°C或者电机控制的速度指令正向转动和反向制动直接用assign product a * b;这种写法综合出来的电路要么面积大得吓人要么时序根本跑不上去甚至可能得到完全错误的结果。有符号数乘法在硬件世界里远不止是数学运算那么简单。它关乎**面积Area、时序Timing和功耗Power**的平衡。一个8位有符号数乘以另一个8位有符号数结果应该是多少位直接想可能是16位但考虑到符号位其实有效位宽是15位MN-1。这个细节就决定了你最终电路里寄存器、加法器和布线资源的用量。我见过不少新手工程师因为没处理好符号扩展和位宽导致在FPGA上仿真明明是对的但下载到板子上数据就溢出或者符号错乱调试起来非常头疼。所以这篇文章我想和你聊聊在Verilog里实现有符号数乘法到底有哪些“门派”和“招式”。我们不只停留在原理我会把我这些年踩过的坑、实测有效的优化技巧以及不同场景下的选型建议都掰开揉碎了讲给你听。无论你是正在学习数字逻辑的学生还是需要优化现有设计的工程师相信都能找到可以直接上手的干货。2. 基础构建原码与补码乘法器的实现与抉择在深入各种优化算法之前我们得先把地基打牢。处理有符号数硬件层面主要有两种表示方法原码和补码。这两种思路也直接衍生出两种最直观的乘法器实现方案。2.1 原码乘法器化繁为简的“分治”策略原码的思路非常符合人的直觉符号和数值分开处理。一个数的最高位表示符号0为正1为负剩下的位表示绝对值。那么乘法就可以分解成三步1取两个操作数的绝对值2用无符号乘法器计算绝对值乘积3根据两个操作数的符号位异或决定最终结果的符号。module mults_4A ( input wire [7:0] multiplicand, // 有符号数最高位为符号位 input wire [7:0] multiplier, output wire [14:0] product // 15位有符号结果 ); wire signOfMulA, signOfMulB; wire [6:0] absMulA, absMulB; // 7位绝对值 wire signOfPro; wire [13:0] absPro; // 14位无符号乘积 assign signOfMulA multiplicand[7]; assign signOfMulB multiplier[7]; assign absMulA multiplicand[6:0]; assign absMulB multiplier[6:0]; // 核心调用一个现成的无符号乘法器可以是IP核或你自己的设计 assign absPro absMulA * absMulB; assign signOfPro signOfMulA ^ signOfMulB; // 符号位异或 assign product {signOfPro, absPro}; // 拼接符号和数值 endmodule这种方法的优点是思路清晰可以直接复用成熟的无符号乘法器模块无论是用组合逻辑“*”实现还是用我们后面会讲的流水线加法器阵列都非常方便。缺点也很明显它需要一次额外的取绝对值和符号判断操作。更重要的是对于补码表示的系统这是计算机和绝大多数数字系统的标准原码乘法器需要前端进行码制转换会引入额外的延迟和逻辑。2.2 补码乘法器直面“负数”的优雅方案既然现代数字系统普遍使用补码那能不能直接对补码数进行运算呢答案是肯定的而且公式还挺优美。对于两个补码数A和B位宽为N其乘积P可以表示为P A * B (作为无符号数相乘) - A * (B的最高位 * 2^N) - B * (A的最高位 * 2^N)。 这个公式看起来复杂但用Verilog实现起来却相当直接。module mults_4B ( input wire [7:0] multiplicand, // 补码输入 input wire [7:0] multiplier, output wire [14:0] product ); wire [1:0] signs; wire [15:0] modifyA, modifyB; wire [15:0] unsignedProduct; reg [15:0] finalProduct; assign signs {multiplicand[7], multiplier[7]}; // 构造修正项如果该数为负则相当于其值减去 2^8 * 符号位扩展 assign modifyA {multiplicand, 8‘b0}; // A 8 assign modifyB {multiplier, 8’b0}; // B 8 // 关键先将输入都视为无符号数相乘 assign unsignedProduct multiplicand * multiplier; always (*) begin case(signs) 2b00: finalProduct unsignedProduct; // 正数×正数 2b01: finalProduct unsignedProduct - modifyA; // 负数×正数 2b10: finalProduct unsignedProduct - modifyB; // 正数×负数 2b11: finalProduct unsignedProduct - modifyA - modifyB; // 负数×负数 default: finalProduct 16b0; endcase end assign product finalProduct[14:0]; endmodule我实测过这种基于修正的补码乘法器在FPGA上用LUT实现时面积会比原码方案稍大一点因为它多了几个选择器和减法器。但是它避免了码制转换的环节数据通路更统一。在数据本身就是补码格式的流水线中这种方案的整体延迟可能反而更有优势。选择哪一种取决于你的数据源格式和系统整体架构。3. 性能突围从组合逻辑到流水线加法器阵列直接使用“*”操作符或者上面的补码修正法综合工具通常会生成一个巨大的组合逻辑乘法器。当位宽增加到16位甚至32位时关键路径会变得很长严重限制系统最高时钟频率。这时候我们就需要更精细地控制乘法过程加法器阵列是其中最经典的结构。3.1 加性分解最直观的“移位相加”回想一下我们小学学过的竖式乘法其实就是加性分解。对于一个8位乘数我们可以把它展开成8个部分积每个部分积是被乘数左移一定位数后根据乘数对应位是0还是1来决定是否累加。原始文章里给出了一个很好的组合逻辑实现例子我这里把它重构成一个单周期、纯组合逻辑的版本并加上注释module mult_array_combinational ( input wire [7:0] a, // 被乘数 input wire [7:0] b, // 乘数 output wire [15:0] p // 乘积 ); // 声明8个16位的部分积 wire [15:0] pp [7:0]; // 生成部分积如果乘数b的第i位为1则部分积为a左移i位否则为0。 // 注意位宽扩展防止溢出。 assign pp[0] b[0] ? {8‘b0, a} : 16’b0; assign pp[1] b[1] ? {7‘b0, a, 1’b0} : 16‘b0; assign pp[2] b[2] ? {6’b0, a, 2‘b0} : 16’b0; assign pp[3] b[3] ? {5‘b0, a, 3’b0} : 16‘b0; assign pp[4] b[4] ? {4’b0, a, 4‘b0} : 16’b0; assign pp[5] b[5] ? {3‘b0, a, 5’b0} : 16‘b0; assign pp[6] b[6] ? {2’b0, a, 6‘b0} : 16’b0; assign pp[7] b[7] ? {1’b0, a, 7‘b0} : 16’b0; // 将所有部分积相加。综合工具会将其映射为一个多操作数加法树。 // 对于高位宽这会产生很长的组合路径。 assign p pp[0] pp[1] pp[2] pp[3] pp[4] pp[5] pp[6] pp[7]; endmodule这个电路结构清晰但问题在于最后的那个8输入加法器。综合后它会形成一条很深的组合逻辑链从pp[0]到pp[7]的进位需要层层传递导致传播延迟Propagation Delay很大。在低速电路里勉强能用一旦时钟频率超过百兆赫兹时序就很难收敛。3.2 流水线化用时间换空间和速度为了解决延迟问题流水线Pipeline是我们的王牌。流水线的核心思想是把一个大的组合逻辑块切割成多个较小的阶段并在阶段之间插入寄存器暂存中间结果。这样虽然完成一次计算需要多个时钟周期延迟 Latency增加了但每个周期都能开始处理一组新数据吞吐率Throughput可以做到每个时钟周期输出一个结果极大地提升了数据处理的整体带宽。下面是一个经典的三级流水线加法树实现它将8个部分积的相加过程分成了三个阶段每一级的结果都打拍寄存module mult_array_pipelined ( input wire clk, input wire [7:0] a, input wire [7:0] b, output reg [15:0] p ); // 第一级寄存器存储部分积 reg [15:0] pp0_reg, pp1_reg, pp2_reg, pp3_reg, pp4_reg, pp5_reg, pp6_reg, pp7_reg; // 第二级寄存器存储两两相加的中间和 reg [15:0] sum01_reg, sum23_reg, sum45_reg, sum67_reg; // 第三级寄存器存储第二级相加的结果 reg [15:0] sum0123_reg, sum4567_reg; // 第一阶段计算部分积并寄存 always (posedge clk) begin pp0_reg b[0] ? {8‘b0, a} : 16’b0; pp1_reg b[1] ? {7‘b0, a, 1’b0} : 16‘b0; pp2_reg b[2] ? {6’b0, a, 2‘b0} : 16’b0; pp3_reg b[3] ? {5‘b0, a, 3’b0} : 16‘b0; pp4_reg b[4] ? {4’b0, a, 4‘b0} : 16’b0; pp5_reg b[5] ? {3‘b0, a, 5’b0} : 16‘b0; pp6_reg b[6] ? {2’b0, a, 6‘b0} : 16’b0; pp7_reg b[7] ? {1’b0, a, 7‘b0} : 16’b0; end // 第二阶段将8个部分积两两相加并寄存 always (posedge clk) begin sum01_reg pp0_reg pp1_reg; sum23_reg pp2_reg pp3_reg; sum45_reg pp4_reg pp5_reg; sum67_reg pp6_reg pp7_reg; end // 第三阶段将第二级的和再次两两相加并寄存 always (posedge clk) begin sum0123_reg sum01_reg sum23_reg; sum4567_reg sum45_reg sum67_reg; end // 第四阶段输出完成最终相加 always (posedge clk) begin p sum0123_reg sum4567_reg; end endmodule这个设计有4个时钟周期的延迟但每个时钟周期都能吃进一组新的a和b。在图像处理、数字滤波这种需要连续处理大量数据的场景里吞吐率的提升是决定性的。你需要根据系统允许的延迟和需要的吞吐率来权衡流水线的级数。级数越多最高运行频率越高但延迟也越大面积寄存器数量也会增加。4. 算法精进BOOTH算法与华莱士树的深度优化当我们需要在更小的面积或更低的功耗下实现乘法或者处理更高位宽如32x32时就需要请出更高级的算法了。BOOTH算法和华莱士树Wallace Tree是硬件乘法器优化的两大“利器”。4.1 BOOTH算法减少部分积的“聪明”策略加性分解的一个问题是部分积的数量等于乘数的位宽。BOOTH算法的核心洞察是通过检查乘数中连续的1可以将多个加法操作合并为一次减法和一次加法从而显著减少部分积的数量。它基于补码每次查看乘数的两位当前位和右侧低位。BOOTH编码规则如下表所示当前位 (b_i)右侧低位 (b_{i-1})操作00001被乘数10-被乘数110一个经典的、带控制流的BOOTH乘法器实现如下。它采用状态机在每个时钟周期内检查乘数的最低两位执行加、减或不变操作然后将乘数右移一位。module booth_multiplier ( input wire clk, input wire rst_n, input wire start, // 启动信号 input wire signed [7:0] multiplicand, input wire signed [7:0] multiplier, output reg done, // 计算完成信号 output reg signed [15:0] product ); localparam IDLE 2‘b00; localparam CALC 2’b01; localparam DONE 2‘b10; reg [1:0] state, next_state; reg [7:0] count; // 迭代计数器 reg signed [15:0] A; // 被乘数扩展寄存器 reg signed [15:0] S; // 被乘数补码负值扩展寄存器 reg signed [15:0] P; // 乘积寄存器 reg [7:0] Q; // 乘数寄存器 reg Q_minus1; // 乘数右侧附加位 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; A 0; S 0; P 0; Q 0; Q_minus1 0; count 0; done 0; end else begin state next_state; case (state) IDLE: begin if (start) begin // 初始化A存储被乘数S存储被乘数的负值补码P高8位清零低8位存乘数 A {multiplicand, 8‘b0}; S {-multiplicand, 8’b0}; // 注意负数的补码计算 P {8‘b0, multiplier}; Q multiplier; Q_minus1 0; count 8; // 8位乘数需要8次迭代 next_state CALC; end end CALC: begin // 根据P的最低两位和Q_minus1决定操作即Q[0]和Q_minus1 case ({P[0], Q_minus1}) 2‘b01: P P A; // 被乘数 2’b10: P P S; // -被乘数 default: ; // 0 保持不变 endcase // 算术右移P和Q {P, Q, Q_minus1} {P[15], P[15:1], Q, P[0]}; // 注意符号扩展 count count - 1; next_state (count 1) ? DONE : CALC; end DONE: begin product {P[15:8], Q}; // 最终乘积在P和Q的组合中 done 1‘b1; next_state IDLE; end endcase end end endmoduleBOOTH算法的优势在于对于包含连续1的乘数它能减少加法/减法操作的次数。例如乘数00111100普通方法需要4次加法而BOOTH算法可能只需要2次。这在随机数据下平均能减少约一半的部分积对于降低功耗和面积非常有效。它的缺点是控制逻辑稍复杂且对于8位乘法其时序性能可能不如深度优化的流水线加法树但在16位及以上位宽时优势会显现。4.2 华莱士树压缩部分积的“高速公路”如果说加法器阵列是让部分积排着队一个一个加那么华莱士树就是一场精心组织的“并行压缩”狂欢。它使用全加器FA和半加器HA作为基本单元以对数时间复杂度将众多的部分积压缩到只剩两个最后用一个快速的进位传递加法器如超前进位加法器完成最终相加。它的设计过程更像是在画一张优化布线图这里很难用简短的代码展示全貌但其思想可以概括为将同一权重的所有部分积比特作为输入用全加器3输入2输出和半加器2输入2输出网络进行多层压缩每一层都减少比特的数量直到最后每列权重下最多只剩两个比特一个和位一个进位位。华莱士树能实现理论上最小的乘法延迟之一特别适合对速度要求极高的ASIC设计。在FPGA中由于底层结构是固定的LUT和专用进位链纯华莱士树的优势可能不如在标准单元库中那么明显但它的设计思想——并行压缩——仍然深刻影响着高性能乘法器IP核的设计。5. 实战策略资源利用与场景化选型指南理论讲完了最后落到实战上我们到底该怎么选是追求极致的速度还是极致的面积或者是平衡这里我结合FPGA和ASIC的不同特点给你一些我的经验之谈。5.1 利用专用硬件资源DSP Slice与Block RAM现代FPGA如Xilinx的UltraScale、Intel的Agilex内部都集成了大量DSP Slice。这是一个高度优化的硬核内部包含预加法器、乘法器和累加器专门为乘加运算设计。它的速度极快功耗远低于用LUT搭建的等效逻辑而且不占用宝贵的可编程逻辑资源。在Vivado或Quartus中你可以直接实例化一个乘法器IP核工具会自动将其映射到DSP48E2等单元上。对于有符号乘法你只需要在IP核配置界面选择数据格式为Signed即可。// 这是一个调用Xilinx DSP48E2原语进行有符号乘加的示例概念代码 // 实际工程中强烈推荐使用IP核向导生成 module dsp_mult_acc ( input wire clk, input wire signed [17:0] a, input wire signed [17:0] b, output reg signed [47:0] p ); // 实际会使用类似 DSP48E2 的原语 // DSP48E2_inst (.CLK(clk), .A(a), .B(b), .P(p), ...); // 这里用行为级描述示意 always (posedge clk) begin p a * b; // 综合工具识别此模式在有时钟寄存器的情况下很可能推断出DSP end endmodule提示对于小于等于18x19位的乘法一个DSP Slice就能搞定。对于更大的乘法IP核会自动级联多个DSP。在FPGA设计中我的第一条建议永远是优先考虑使用DSP Slice除非你的设计里DSP资源已经用光了。另一种特殊资源是Block RAM。对于位宽不大比如两个8位输入、但需要极低功耗或确定延迟的场景可以把所有可能的乘积结果预先计算好存入ROM中乘法就变成了查表。一个8位有符号乘法的完整结果表需要2^8 * 2^8 * 16 bit ≈ 1 Mbit这对于Block RAM来说太大了。但如果是系数固定的乘法例如FIR滤波器的固定抽头系数那么只需要存储针对一个变量的结果查表法就非常高效。5.2 场景化选型决策表为了帮你快速做决定我总结了一个简单的决策表你可以根据设计约束来匹配设计约束 / 场景推荐方案关键考量对速度要求极高时钟频率 300MHz1.专用DSP IP核2. 深度流水线的加法器阵列如4级或更多DSP是性能王者。流水线阵列需要精细设计平衡级数。面积敏感逻辑资源紧张1.BOOTH算法串行或轻度并行2. 复用现有小型乘法器进行多周期计算BOOTH减少部分积节省加法器。多周期计算用时间换面积。低功耗设计1.门控时钟的串行BOOTH乘法器2. 查表法适用于小位宽或固定系数减少开关活动因子。串行方案只在必要时工作。查表访问功耗低。中等性能兼顾面积和速度2-3级流水线的加法器阵列在LUT逻辑中实现的最佳平衡点控制逻辑简单。数据位宽非常大32位混合方案高位拆分为多个小块分别用DSP或优化乘法器计算最后合并。避免单一巨型组合逻辑。利用数据流并行性。教学或原理验证原码乘法器或基础的加性分解组合逻辑乘法器结构清晰易于理解原理和进行波形调试。最后我想说乘法器设计没有银弹。在我做的一个电机驱动项目里因为要同时处理多个通道的电流环PID运算DSP资源告急我不得不把一部分24位乘法用修改过的二级流水线BOOTH算法在逻辑里实现。调试的时候为了那一点时序余量反复调整流水线寄存器的位置折腾了好几天。但正是这种折腾让我对数据路径和时序的关系有了肌肉记忆般的理解。所以多动手实现几种方案用你的目标器件和综合工具实际跑一跑看看面积和时序报告比读十篇文章都有用。从最简单的“*”开始逐步深入到流水线和BOOTH你会发现自己对数字电路的理解也跟着这些0和1的流动一起变得深刻起来。