无符号数乘法是数字电路设计中较为基础的一类运算模块,广泛用于DSP、图像处理、通信、嵌入式计算以及FPGA加速等场景。使用Verilog设计无符号数乘法器时,除了需要保证乘法结果的位宽正确,还需要关注综合后的硬件资源、时序性能以及不同实现方式之间的差异。
一、无符号乘法器的基本原理
假设有两个 N 位无符号数:
A = a[N-1:0] B = b[N-1:0]
两者相乘后,理论上最大结果需要 2N 位保存。
例如两个 8 位无符号数相乘:
255 × 255 = 65025
65025的二进制形式需要16位表示,因此结果通常定义为:
verilogreg [15:0] result;
从硬件角度来看,二进制乘法可以理解为多个部分积相加。乘数的每一位决定是否产生对应的被乘数移位结果,最终将所有部分积累加即可。
对于:
A × B
如果 B 的某一位为1,则生成:
A << i
如果 B 的第 i 位为0,则对应部分积为0。
因此,一个简单的无符号乘法器可以通过“移位 + 累加”实现。
二、最简单的Verilog无符号乘法器
如果主要目标是实现功能,而不是手动控制乘法器内部结构,可以直接使用Verilog的乘法运算符 *。
verilogmodule unsigned_multiplier #( parameter WIDTH = 8 )( input wire [WIDTH-1:0] a, input wire [WIDTH-1:0] b, output wire [2*WIDTH-1:0] result ); assign result = a * b; endmodule
这个实现非常简洁,而且综合工具通常能够根据目标FPGA或ASIC工艺自动选择合适的硬件结构。
例如:
a = 8'd12 b = 8'd10
输出:
result = 16'd120
需要注意的是,输入信号没有声明为 signed,因此这里表达的是无符号乘法。
对于大多数工程项目,如果没有明确要求自己设计乘法器结构,直接使用 * 往往是最合适的选择。现代综合工具可以对乘法表达式进行优化,并根据目标器件的DSP、乘法器等专用资源进行映射。
三、使用移位累加实现乘法器
如果学习数字电路原理,或者需要实现一个不直接依赖 * 运算符的乘法器,可以采用移位累加方法。
下面是一个组合逻辑版本:
verilogmodule shift_add_multiplier #( parameter WIDTH = 8 )( input wire [WIDTH-1:0] a, input wire [WIDTH-1:0] b, output reg [2*WIDTH-1:0] result ); integer i; always @(*) begin result = {(2*WIDTH){1'b0}}; for (i = 0; i < WIDTH; i = i + 1) begin if (b[i]) result = result + (a << i); end end endmodule
其核心逻辑可以概括为:
result = 0 如果 b[0] = 1,则 result += a 如果 b[1] = 1,则 result += a << 1 如果 b[2] = 1,则 result += a << 2 ...
最终得到:
result = A × B
为什么结果必须扩展到2N位
这是设计乘法器时非常容易出现的问题。
例如:
veriloginput wire [7:0] a; input wire [7:0] b;
如果直接将结果定义成8位:
verilogoutput wire [7:0] result;
那么超过255的部分将无法保留。
正确的设计通常是:
verilogoutput wire [15:0] result;
对于参数化设计,则使用:
verilogoutput wire [2*WIDTH-1:0] result;
这样才能完整保存两个 N 位无符号数相乘的理论结果。
四、时序逻辑实现串行乘法器
组合乘法器的优点是一次计算即可得到结果,但代价可能是较大的组合逻辑和较长的关键路径。
如果芯片资源有限,可以采用多周期串行乘法器。
基本思路是每个时钟周期处理乘数的一位,通过多个周期完成整个乘法过程。这样可以减少组合逻辑规模,但计算延迟会增加。
一个简单的时序实现如下:
verilogmodule serial_multiplier #( parameter WIDTH = 8 )( input wire clk, input wire rst_n, input wire start, input wire [WIDTH-1:0] a, input wire [WIDTH-1:0] b, output reg [2*WIDTH-1:0] result, output reg busy, output reg done ); reg [WIDTH-1:0] multiplicand; reg [WIDTH-1:0] multiplier; reg [2*WIDTH-1:0] accumulator; integer count; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin multiplicand <= 0; multiplier <= 0; accumulator <= 0; result <= 0; busy <= 0; done <= 0; count <= 0; end else begin done <= 1'b0; if (start && !busy) begin multiplicand <= a; multiplier <= b; accumulator <= 0; count <= 0; busy <= 1'b1; end else if (busy) begin if (multiplier[0]) accumulator <= accumulator + multiplicand; multiplicand <= multiplicand << 1; multiplier <= multiplier >> 1; if (count == WIDTH-1) begin result <= multiplier[0] ? accumulator + multiplicand : accumulator; busy <= 1'b0; done <= 1'b1; end else begin count <= count + 1; end end end end endmodule
这种结构的主要特点是:
-
硬件资源较少;
-
一个乘法运算需要多个时钟周期;
-
可以通过
busy表示计算状态; -
使用
done表示计算完成; -
适合对吞吐率要求不高、但资源比较紧张的场景。
需要注意的是,实际项目中通常还会根据接口协议、复位方式以及时序要求进一步完善该结构。
五、组合乘法器与时序乘法器的选择
两种实现方式没有绝对的优劣,关键取决于系统需求。
| 实现方式 | 资源消耗 | 单次延迟 | 吞吐率 | 设计复杂度 |
|---|---|---|---|---|
* 运算符 | 取决于综合结果 | 较低 | 高 | 低 |
| 移位累加组合逻辑 | 较高 | 低 | 高 | 中 |
| 串行乘法器 | 较低 | 较高 | 低 | 中 |
| 流水线乘法器 | 较高 | 多周期 | 很高 | 高 |
如果使用FPGA,通常优先考虑直接使用 *,让综合工具尝试映射到芯片内部的DSP Block或专用乘法资源。
如果需要自己控制资源占用,则可以使用串行移位累加结构。
如果系统要求连续输入大量数据,并且对吞吐率要求很高,则应该考虑流水线设计。
六、Verilog中的无符号类型问题
无符号乘法设计中,一个经常被忽略的问题是Verilog表达式的类型和位宽。
例如:
verilogreg [7:0] a; reg [7:0] b; reg [15:0] result; always @(*) begin result = a * b; end
这种写法通常可以得到预期的无符号乘法结果。
如果信号声明为:
verilogreg signed [7:0] a;
那么表达式的符号属性就会发生变化。
因此,在设计明确的无符号乘法器时,建议保持输入、临时变量和输出的类型定义一致,避免在不同signed和unsigned信号之间进行隐式类型转换。
对于SystemVerilog,可以使用更加明确的类型声明:
systemveriloglogic [7:0] a; logic [7:0] b; logic [15:0] result; assign result = a * b;
如果确实需要进行类型转换,可以显式处理,而不是依赖综合工具对复杂表达式进行推断。
七、使用流水线优化乘法器
对于高速数据处理场景,单纯增加组合逻辑可能会导致关键路径过长。
例如,一个大型组合乘法器可能需要经历:
输入 ↓ 部分积生成 ↓ 部分积压缩 ↓ 加法 ↓ 输出
如果这些逻辑全部放在一个时钟周期内,可能导致最高工作频率下降。
流水线设计可以在关键路径中插入寄存器:
输入 ↓ 部分积生成 ↓ 寄存器 ↓ 部分积压缩 ↓ 寄存器 ↓ 最终加法 ↓ 寄存器 ↓ 输出
这样可以缩短单级组合逻辑延迟,提高时钟频率。
代价是计算结果需要经过若干个流水级之后才能产生,因此需要额外处理数据有效信号。
例如:
verilogalways @(posedge clk) begin a_reg <= a; b_reg <= b; end always @(posedge clk) begin result_reg <= a_reg * b_reg; end
这是最简单的流水线思想。实际设计中还需要根据目标器件的DSP结构、时序报告以及数据吞吐需求决定流水线级数。
八、利用FPGA DSP资源优化
对于FPGA设计而言,使用 * 并不意味着一定会生成庞大的通用逻辑。
许多FPGA内部都集成了专用DSP资源,其中通常包含乘法器、加法器以及寄存器等硬件单元。
因此:
verilogassign result = a * b;
经过综合和布局布线后,可能直接映射到DSP资源。
如果手工使用大量:
verilog+ << &
来构建乘法器,反而可能让综合工具无法充分利用目标器件的专用资源。
因此优化不能简单理解为“自己写底层逻辑就是更高效”。真正有效的优化应该结合目标平台进行。
对于FPGA,建议查看综合报告中的:
-
DSP使用数量;
-
LUT使用数量;
-
Register使用数量;
-
最大工作频率;
-
关键路径;
-
时序裕量。
然后再决定是否需要调整RTL结构。
九、乘法器中的常见优化思路
1. 缩小操作数位宽
位宽对乘法器资源影响明显。
例如:
verilogwire [31:0] a; wire [31:0] b; wire [63:0] result;
与:
verilogwire [15:0] a; wire [15:0] b; wire [31:0] result;
相比,前者需要处理更多部分积和更大的结果路径。
如果业务数据实际只需要12位,就没有必要无条件使用32位乘法器。
2. 删除无意义的扩展位
如果上层模块明确只需要乘法结果的低若干位,可以根据算法要求裁剪结果。
例如:
verilogwire [31:0] full_result; assign full_result = a * b; assign result = full_result[15:0];
不过这种优化必须确认高位确实不会影响后续计算,否则可能产生精度或溢出问题。
3. 合理插入流水线
对于高速设计,与其不断压缩组合逻辑,不如根据时序报告增加流水线级数。
流水线的核心目标并不是减少运算量,而是降低单个时钟周期内的逻辑延迟。
4. 充分利用常数乘法
如果乘数是固定常量:
verilogassign result = a * 10;
综合工具可能将其优化成移位和加法组合,例如:
10A = 8A + 2A
对应:
verilog(a << 3) + (a << 1)
但通常不建议为了“手工优化”而盲目改写,应该先查看综合结果,因为现代工具已经能够识别大量常数乘法模式。
5. 避免不必要的重复计算
如果多个模块需要使用相同乘法结果,可以考虑共享计算结果:
verilogwire [15:0] mult_result; assign mult_result = a * b;
后续逻辑统一使用 mult_result,而不是在多个地方重复描述相同运算。
十、无符号乘法器测试方法
设计完成后不能只验证几个简单数据,应该覆盖边界值、随机数据以及特殊组合。
例如:
verilogmodule tb_unsigned_multiplier; reg [7:0] a; reg [7:0] b; wire [15:0] result; unsigned_multiplier dut ( .a(a), .b(b), .result(result) ); initial begin a = 8'd0; b = 8'd0; #10; a = 8'd1; b = 8'd255; #10; a = 8'd255; b = 8'd255; #10; a = 8'd12; b = 8'd25; #10; $finish; end endmodule
重点测试以下情况:
0 × 0 0 × 最大值 1 × 最大值 最大值 × 最大值 随机数 × 随机数
对于参数化乘法器,还应该测试不同的 WIDTH。
例如:
WIDTH = 4 WIDTH = 8 WIDTH = 16 WIDTH = 32
如果使用SystemVerilog,还可以通过断言进一步验证结果:
systemverilogassert (result == a * b) else $error("Multiplier result error");
对于复杂流水线乘法器,则需要额外验证输入和输出之间的延迟是否符合设计预期。
十一、常见错误分析
输出位宽过小
这是最常见的问题。
8位 × 8位不能简单认为结果仍然只需要8位。完整结果需要16位。
混用signed和unsigned
例如一个操作数是:
verilogreg [7:0] a;
另一个是:
verilogreg signed [7:0] b;
两者参与表达式时可能产生与预期不同的符号扩展结果。
对于无符号乘法器,建议统一使用无符号数据类型。
在时序always块中错误使用阻塞赋值
设计时序逻辑时通常使用非阻塞赋值:
verilogalways @(posedge clk) begin result <= a * b; end
而组合逻辑一般使用:
verilogalways @(*) begin result = a * b; end
保持组合逻辑和时序逻辑的编码习惯清晰,有助于减少仿真和综合之间的不一致。
过度追求手工优化
很多设计者看到乘法器资源较高,就立即使用大量移位、加法和条件判断替代 *。
实际上,综合工具可能已经针对目标FPGA进行了专门优化。手工重写之后不仅不一定节省资源,还有可能增加LUT数量并恶化时序。
因此,更合理的流程是:
RTL设计 ↓ 功能仿真 ↓ 综合 ↓ 查看资源报告 ↓ 查看时序报告 ↓ 定位瓶颈 ↓ 针对性优化
十二、实际工程中的选择建议
如果只是需要实现一个普通的无符号乘法运算,可以优先采用:
verilogassign result = a * b;
代码简单、可维护性高,也便于综合工具进行优化。
如果目标是学习乘法器内部工作原理,可以采用移位累加方式,通过循环观察部分积生成过程。
如果FPGA资源有限,可以考虑串行乘法器,用多个时钟周期换取更低的资源占用。
如果系统对吞吐率和工作频率要求较高,则可以考虑DSP资源和流水线结构。
如果面向ASIC实现,则需要结合工艺库、综合约束以及功耗、面积和时序目标进行门级优化,不能简单照搬FPGA上的优化策略。
总体来看,Verilog无符号数乘法器的设计重点并不只是实现 A × B,更重要的是正确处理位宽、数据类型、时序、资源和吞吐率之间的关系。对于简单场景,使用乘法运算符往往已经足够;对于资源受限或高性能场景,则可以进一步从串行计算、流水线、常数乘法、位宽裁剪以及专用DSP资源等方向进行优化。