Verilog无符号数乘法器的实现与优化

6 次阅读

无符号数乘法是数字电路设计中较为基础的一类运算模块,广泛用于DSP、图像处理、通信、嵌入式计算以及FPGA加速等场景。使用Verilog设计无符号数乘法器时,除了需要保证乘法结果的位宽正确,还需要关注综合后的硬件资源、时序性能以及不同实现方式之间的差异。

一、无符号乘法器的基本原理

假设有两个 N 位无符号数:

A = a[N-1:0]
B = b[N-1:0]

两者相乘后,理论上最大结果需要 2N 位保存。

例如两个 8 位无符号数相乘:

255 × 255 = 65025

65025的二进制形式需要16位表示,因此结果通常定义为:

verilog
reg [15:0] result;

从硬件角度来看,二进制乘法可以理解为多个部分积相加。乘数的每一位决定是否产生对应的被乘数移位结果,最终将所有部分积累加即可。

对于:

A × B

如果 B 的某一位为1,则生成:

A << i

如果 B 的第 i 位为0,则对应部分积为0。

因此,一个简单的无符号乘法器可以通过“移位 + 累加”实现。

二、最简单的Verilog无符号乘法器

如果主要目标是实现功能,而不是手动控制乘法器内部结构,可以直接使用Verilog的乘法运算符 *。

verilog
module 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、乘法器等专用资源进行映射。

三、使用移位累加实现乘法器

如果学习数字电路原理,或者需要实现一个不直接依赖 * 运算符的乘法器,可以采用移位累加方法。

下面是一个组合逻辑版本:

verilog
module 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位

这是设计乘法器时非常容易出现的问题。

例如:

verilog
input  wire [7:0] a;
input  wire [7:0] b;

如果直接将结果定义成8位:

verilog
output wire [7:0] result;

那么超过255的部分将无法保留。

正确的设计通常是:

verilog
output wire [15:0] result;

对于参数化设计,则使用:

verilog
output wire [2*WIDTH-1:0] result;

这样才能完整保存两个 N 位无符号数相乘的理论结果。

四、时序逻辑实现串行乘法器

组合乘法器的优点是一次计算即可得到结果,但代价可能是较大的组合逻辑和较长的关键路径。

如果芯片资源有限,可以采用多周期串行乘法器。

基本思路是每个时钟周期处理乘数的一位,通过多个周期完成整个乘法过程。这样可以减少组合逻辑规模,但计算延迟会增加。

一个简单的时序实现如下:

verilog
module 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表达式的类型和位宽。

例如:

verilog
reg [7:0] a;
reg [7:0] b;
reg [15:0] result;

always @(*) begin
    result = a * b;
end

这种写法通常可以得到预期的无符号乘法结果。

如果信号声明为:

verilog
reg signed [7:0] a;

那么表达式的符号属性就会发生变化。

因此,在设计明确的无符号乘法器时,建议保持输入、临时变量和输出的类型定义一致,避免在不同signed和unsigned信号之间进行隐式类型转换。

对于SystemVerilog,可以使用更加明确的类型声明:

systemverilog
logic [7:0] a;
logic [7:0] b;
logic [15:0] result;

assign result = a * b;

如果确实需要进行类型转换,可以显式处理,而不是依赖综合工具对复杂表达式进行推断。

七、使用流水线优化乘法器

对于高速数据处理场景,单纯增加组合逻辑可能会导致关键路径过长。

例如,一个大型组合乘法器可能需要经历:

输入
 ↓
部分积生成
 ↓
部分积压缩
 ↓
加法
 ↓
输出

如果这些逻辑全部放在一个时钟周期内,可能导致最高工作频率下降。

流水线设计可以在关键路径中插入寄存器:

输入
 ↓
部分积生成
 ↓
寄存器
 ↓
部分积压缩
 ↓
寄存器
 ↓
最终加法
 ↓
寄存器
 ↓
输出

这样可以缩短单级组合逻辑延迟,提高时钟频率。

代价是计算结果需要经过若干个流水级之后才能产生,因此需要额外处理数据有效信号。

例如:

verilog
always @(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资源,其中通常包含乘法器、加法器以及寄存器等硬件单元。

因此:

verilog
assign result = a * b;

经过综合和布局布线后,可能直接映射到DSP资源。

如果手工使用大量:

verilog
+
<<
&

来构建乘法器,反而可能让综合工具无法充分利用目标器件的专用资源。

因此优化不能简单理解为“自己写底层逻辑就是更高效”。真正有效的优化应该结合目标平台进行。

对于FPGA,建议查看综合报告中的:

  • DSP使用数量;

  • LUT使用数量;

  • Register使用数量;

  • 最大工作频率;

  • 关键路径;

  • 时序裕量。

然后再决定是否需要调整RTL结构。

九、乘法器中的常见优化思路

1. 缩小操作数位宽

位宽对乘法器资源影响明显。

例如:

verilog
wire [31:0] a;
wire [31:0] b;
wire [63:0] result;

与:

verilog
wire [15:0] a;
wire [15:0] b;
wire [31:0] result;

相比,前者需要处理更多部分积和更大的结果路径。

如果业务数据实际只需要12位,就没有必要无条件使用32位乘法器。

2. 删除无意义的扩展位

如果上层模块明确只需要乘法结果的低若干位,可以根据算法要求裁剪结果。

例如:

verilog
wire [31:0] full_result;

assign full_result = a * b;
assign result = full_result[15:0];

不过这种优化必须确认高位确实不会影响后续计算,否则可能产生精度或溢出问题。

3. 合理插入流水线

对于高速设计,与其不断压缩组合逻辑,不如根据时序报告增加流水线级数。

流水线的核心目标并不是减少运算量,而是降低单个时钟周期内的逻辑延迟。

4. 充分利用常数乘法

如果乘数是固定常量:

verilog
assign result = a * 10;

综合工具可能将其优化成移位和加法组合,例如:

10A = 8A + 2A

对应:

verilog
(a << 3) + (a << 1)

但通常不建议为了“手工优化”而盲目改写,应该先查看综合结果,因为现代工具已经能够识别大量常数乘法模式。

5. 避免不必要的重复计算

如果多个模块需要使用相同乘法结果,可以考虑共享计算结果:

verilog
wire [15:0] mult_result;

assign mult_result = a * b;

后续逻辑统一使用 mult_result,而不是在多个地方重复描述相同运算。

十、无符号乘法器测试方法

设计完成后不能只验证几个简单数据,应该覆盖边界值、随机数据以及特殊组合。

例如:

verilog
module 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,还可以通过断言进一步验证结果:

systemverilog
assert (result == a * b)
    else $error("Multiplier result error");

对于复杂流水线乘法器,则需要额外验证输入和输出之间的延迟是否符合设计预期。

十一、常见错误分析

输出位宽过小

这是最常见的问题。

8位 × 8位不能简单认为结果仍然只需要8位。完整结果需要16位。

混用signed和unsigned

例如一个操作数是:

verilog
reg [7:0] a;

另一个是:

verilog
reg signed [7:0] b;

两者参与表达式时可能产生与预期不同的符号扩展结果。

对于无符号乘法器,建议统一使用无符号数据类型。

在时序always块中错误使用阻塞赋值

设计时序逻辑时通常使用非阻塞赋值:

verilog
always @(posedge clk) begin
    result <= a * b;
end

而组合逻辑一般使用:

verilog
always @(*) begin
    result = a * b;
end

保持组合逻辑和时序逻辑的编码习惯清晰,有助于减少仿真和综合之间的不一致。

过度追求手工优化

很多设计者看到乘法器资源较高,就立即使用大量移位、加法和条件判断替代 *。

实际上,综合工具可能已经针对目标FPGA进行了专门优化。手工重写之后不仅不一定节省资源,还有可能增加LUT数量并恶化时序。

因此,更合理的流程是:

RTL设计
 ↓
功能仿真
 ↓
综合
 ↓
查看资源报告
 ↓
查看时序报告
 ↓
定位瓶颈
 ↓
针对性优化

十二、实际工程中的选择建议

如果只是需要实现一个普通的无符号乘法运算,可以优先采用:

verilog
assign result = a * b;

代码简单、可维护性高,也便于综合工具进行优化。

如果目标是学习乘法器内部工作原理,可以采用移位累加方式,通过循环观察部分积生成过程。

如果FPGA资源有限,可以考虑串行乘法器,用多个时钟周期换取更低的资源占用。

如果系统对吞吐率和工作频率要求较高,则可以考虑DSP资源和流水线结构。

如果面向ASIC实现,则需要结合工艺库、综合约束以及功耗、面积和时序目标进行门级优化,不能简单照搬FPGA上的优化策略。

总体来看,Verilog无符号数乘法器的设计重点并不只是实现 A × B,更重要的是正确处理位宽、数据类型、时序、资源和吞吐率之间的关系。对于简单场景,使用乘法运算符往往已经足够;对于资源受限或高性能场景,则可以进一步从串行计算、流水线、常数乘法、位宽裁剪以及专用DSP资源等方向进行优化。