从移位相加到硬件实现:FPGA二进制乘法器的设计精髓
1. 从纸笔计算到硬件逻辑:二进制乘法的本质
记得第一次学二进制乘法时,我拿着铅笔在纸上画了半天移位相加的步骤。比如计算1101×1011,就像小学生列竖式一样,先写下1101×1=1101,然后1101×1左移一位变成11010,接着1101×0左移两位变成000000,最后1101×1左移三位变成1101000,把这些结果相加得到10001111(143)。这种移位相加的方法,本质上就是把十进制竖式乘法搬到了二进制世界。
但真正让我感到震撼的是,当我把这个算法用Verilog代码实现后,综合出来的电路竟然和纸上的计算步骤一一对应!每个与门对应着乘数某一位的判断,每级移位器对应着纸上的左移操作,而加法器链就是最后那一步求和。这种算法到硬件的直接映射,正是FPGA设计最迷人的地方——你的代码就是在"画电路"。
二进制乘法的硬件优势在于,它把数学运算分解成了与、或、非这些最基础的逻辑操作。比如两个4位数相乘,软件可能需要几十条指令,而硬件只需要:
- 4个与门阵列生成部分积
- 3个加法器完成累加
- 若干移位寄存器对齐数据
这种并行处理的能力,让FPGA在做定点乘法时,速度可以比CPU快上几十倍。我在一个图像处理项目里实测过,用Xilinx Artix-7实现的8位乘法器,延迟只有3.2ns,而同样算法在树莓派上跑需要120ns。
2. 硬件乘法器的核心架构
2.1 与门阵列:乘法的基础细胞
设计乘法器时,我习惯先从与门阵列开始搭建。这部分对应着人工计算中"乘数的每一位与被乘数相与"的步骤。以4位乘法器为例,需要用4组4位与门生成部分积:
// 每位乘数控制一个与门阵列 wire [3:0] partial_products [0:3]; generate for (genvar i=0; i<4; i++) begin assign partial_products[i] = a & {4{b[i]}}; end endgenerate这里有个设计细节:与门阵列的输出宽度要和被乘数一致。我曾在一次项目里犯过错,把部分积直接赋给8位寄存器,结果综合出来一堆无用逻辑。正确的做法是后续再通过位拼接实现移位。
2.2 移位操作:硬件中的对齐魔法
在纸上计算时我们需要手动左移,硬件中则通过位拼接实现。Verilog的{}运算符就像个智能移位器:
// 给部分积添加对应的前导零和尾零 wire [7:0] shifted_products [0:3]; assign shifted_products[0] = {4'd0, partial_products[0]}; // 不移位 assign shifted_products[1] = {3'd0, partial_products[1], 1'd0}; // 左移1位 assign shifted_products[2] = {2'd0, partial_products[2], 2'd0}; // 左移2位 assign shifted_products[3] = {1'd0, partial_products[3], 3'd0}; // 左移3位这里有个坑要注意:移位后的位宽必须是最终结果位宽(乘数位宽之和)。我有次少算了一位,导致乘法结果高位被截断,图像处理时出现了奇怪的条纹。
2.3 加法器树:速度与面积的权衡
最后一步求和,最简单的做法是三级级联加法器:
wire [7:0] sum_stage1 = shifted_products[0] + shifted_products[1]; wire [7:0] sum_stage2 = sum_stage1 + shifted_products[2]; assign m = sum_stage2 + shifted_products[3];但在实际项目中,当乘数位宽较大时(比如16位),我会改用Wallace树结构来优化。曾经用Carry-Save加法器重构过一个8位乘法器,延迟从15ns降到了9ns,代价是多用了200个LUT。这种时空权衡是FPGA设计的永恒主题。
3. Verilog实现的艺术
3.1 可综合编码风格
刚开始写乘法器时,我直接用了行为级的*运算符:
output reg [7:0] m; always @(*) begin m = a * b; // 简单但不可控 end直到有次需要优化时序才发现,综合器生成的电路用了DSP48单元,虽然速度快但灵活性差。现在我会明确写出结构化的移位相加逻辑,这样:
- 可以精确控制用LUT还是DSP
- 方便插入流水线寄存器
- 利于做面积优化
3.2 参数化设计技巧
固定位宽的乘法器复用性差,我后来改用参数化设计:
module param_mult #(parameter WIDTH=4) ( input [WIDTH-1:0] a, b, output [2*WIDTH-1:0] m ); // 生成可变数量的部分积 wire [2*WIDTH-1:0] shifted_products [0:WIDTH-1]; generate for (genvar i=0; i<WIDTH; i++) begin assign shifted_products[i] = {{(WIDTH-i){1'b0}}, (a & {WIDTH{b[i]}}), {i{1'b0}}}; end endgenerate // 可配置的加法器树 assign m = shifted_products.sum(); // SystemVerilog的数组缩减操作 endmodule这种写法在最近的一个通信项目中帮了大忙,同一套代码既能实现8位粗调又能做16位精算。
3.3 时序约束要点
硬件乘法器最常见的时序问题是加法器链过长。我的解决方案是:
- 设置合理的时钟约束
create_clock -period 5 [get_ports clk]- 对关键路径插入寄存器
always @(posedge clk) begin stage1_reg <= shifted_products[0] + shifted_products[1]; stage2_reg <= stage1_reg + shifted_products[2]; m_reg <= stage2_reg + shifted_products[3]; end- 必要时改用流水线结构
在Zynq项目里,通过三级流水线把200MHz的设计提升到了300MHz,代价是增加了2个周期的延迟。
4. 硬件优化的实战策略
4.1 资源利用对比
用Xilinx Vivado综合4位乘法器时,不同实现方式的资源消耗:
| 实现方式 | LUT | 寄存器 | 最大频率 |
|---|---|---|---|
| 行为级(*) | 3 | 0 | 650MHz |
| 移位相加 | 28 | 0 | 320MHz |
| 流水线移位相加 | 32 | 24 | 550MHz |
可以看到,虽然行为级写法最省资源,但当我们需精确控制电路结构时,还是应该采用显式的移位相加实现。
4.2 进位保留技巧
在高速设计中,我常用进位保留加法器(CSA)来优化关键路径:
// 3:2压缩器示例 module compressor_3to2( input [7:0] a, b, c, output [7:0] sum, carry ); assign sum = a ^ b ^ c; assign carry = {a[6:0]&b[6:0] | a[6:0]&c[6:0] | b[6:0]&c[6:0], 1'b0}; endmodule这种结构可以减少加法器级数,在一个雷达信号处理项目中,用CSA重构的8位乘法器频率提升了40%。
4.3 混合架构设计
对于大位宽乘法(如32位),纯组合逻辑会占用大量资源。我的经验是:
- 低位部分用组合逻辑实现
- 高位部分改用时序逻辑分步计算
- 最后用DSP单元做结果整合
这种混合架构在保证吞吐量的同时,能显著减少LUT消耗。去年做的那个神经网络加速器,用这种方法把乘法器面积减少了35%。
