加法器:从半加器到流水线加法器

加法器这东西,说简单也简单,说复杂也复杂。我刚开始学Verilog那会儿,觉得加法不就是个"+"号吗?后来做卷积加速器才发现,加法器的设计直接决定了整个加速器的吞吐量。今天咱们就把它彻底讲透。

半加器与全加器:最基础的计算单元

先看最简单的半加器。两个1位二进制数相加,产生和(Sum)与进位(Carry)。

// 半加器
module half_adder (
    input  a,      // 加数
    input  b,      // 加数
    output sum,    // 和
    output carry   // 进位
);

    assign sum   = a ^ b;   // 异或:相同为0,不同为1
    assign carry = a & b;   // 与:两个都是1才进位

endmodule

嗯,这里要注意:半加器不考虑来自低位的进位。实际项目中几乎用不到它,因为多位加法必须考虑进位传递。全加器才是真正有用的。

// 全加器
module full_adder (
    input  a,      // 加数
    input  b,      // 加数
    input  cin,    // 来自低位的进位
    output sum,    // 和
    output cout    // 向高位的进位
);

    assign sum   = a ^ b ^ cin;          // 三个输入异或
    assign cout  = (a & b) | (a & cin) | (b & cin);  // 至少两个为1

endmodule

全加器的真值表其实很简单,我建议你手推一遍:

abcinsumcout
00000
00110
01010
01101
10010
10101
11001
11111

个人经验:我在做卷积加速器时,乘法器里的部分积累加就是靠全加器阵列完成的。当时为了省面积,把全加器用LUT实现了,结果时序跑不过。后来改成专用加法器单元,频率直接提升了30%。

行波进位加法器:最直观的多位加法

把多个全加器串联起来,就构成了行波进位加法器(Ripple Carry Adder)。每个全加器的进位输出接到下一个全加器的进位输入。

// 4位行波进位加法器
module ripple_carry_adder_4bit (
    input  [3:0] a,
    input  [3:0] b,
    input        cin,
    output [3:0] sum,
    output       cout
);

    wire c1, c2, c3;  // 内部进位

    full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin),  .sum(sum[0]), .cout(c1));
    full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c1),   .sum(sum[1]), .cout(c2));
    full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c2),   .sum(sum[2]), .cout(c3));
    full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c3),   .sum(sum[3]), .cout(cout));

endmodule

这种加法器有个致命问题:进位像多米诺骨牌一样逐级传递。4位加法器需要4级门延迟,32位就需要32级。在卷积加速器里,如果数据位宽是32位,那延迟就太长了。

避坑指南:我曾经在一个图像处理项目里用了行波进位加法器做像素累加,结果时序收敛不了。后来换成超前进位加法器,问题才解决。记住:位宽超过8位就别用行波进位了。

超前进位加法器:速度与面积的权衡

超前进位加法器(Carry Look-ahead Adder)的核心思想是:提前计算出进位,而不是等前一级算完。它用两个关键信号:

  • 生成信号 G = a & b:只要a和b都是1,就一定产生进位
  • 传播信号 P = a ^ b:如果a和b不同,进位取决于cin

有了G和P,进位公式就变成了:

c1 = G0 | (P0 & cin)
c2 = G1 | (P1 & G0) | (P1 & P0 & cin)
c3 = G2 | (P2 & G1) | (P2 & P1 & G0) | (P2 & P1 & P0 & cin)
// 以此类推...

你看,每个进位都可以用组合逻辑直接算出来,不需要等待。代价是逻辑门数量呈指数增长。4位超前进位加法器还行,32位的话,那个公式会写到你想哭。

实际工程做法:通常采用"分层超前进位"——先做4位一组,组内超前进位,组间再超前进位。这样速度和面积都比较均衡。我在卷积加速器里就是这么干的。

流水线加法器:为高速而生

如果加法器要跑在500MHz以上,组合逻辑延迟肯定扛不住。这时候就需要流水线了。

// 4级流水线加法器
module pipelined_adder (
    input             clk,
    input             rst_n,
    input  [31:0]     a,
    input  [31:0]     b,
    input             cin,
    output reg [31:0] sum,
    output reg        cout
);

    // 第1级:低8位加法
    reg [7:0]  sum_low;
    reg        carry_mid1;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum_low    <= 0;
            carry_mid1 <= 0;
        end else begin
            {carry_mid1, sum_low} <= a[7:0] + b[7:0] + cin;
        end
    end

    // 第2级:次低8位加法
    reg [7:0]  sum_mid_low;
    reg        carry_mid2;
    reg [7:0]  a_mid_low, b_mid_low;  // 输入打拍
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sum_mid_low <= 0;
            carry_mid2  <= 0;
            a_mid_low   <= 0;
            b_mid_low   <= 0;
        end else begin
            a_mid_low   <= a[15:8];
            b_mid_low   <= b[15:8];
            {carry_mid2, sum_mid_low} <= a_mid_low + b_mid_low + carry_mid1;
        end
    end

    // 第3级、第4级类似,省略...
    // 最后一级输出最终结果

endmodule

流水线加法器的好处是:虽然单个加法需要4个时钟周期,但每周期都能启动一个新的加法。吞吐量是原来的4倍。在卷积加速器里,我们通常用这种设计来做乘累加运算。

我的习惯:做流水线设计时,我会在每级之间加valid信号做握手。这样即使某级延迟变化,也不会丢数据。另外,复位逻辑一定要写全,否则仿真时会出现X态,排查起来很痛苦。

加法器在卷积加速器中的典型应用

卷积运算的核心就是乘累加(MAC)。一个3x3的卷积核,需要做9次乘法和8次加法。如果直接用"+"号,综合工具会帮你选最优结构。但如果你想手动优化,可以这样:

// 卷积核的加法树(3x3)
module adder_tree (
    input  [7:0] p0, p1, p2, p3, p4, p5, p6, p7, p8,  // 9个乘法结果
    output [11:0] result
);

    wire [8:0] sum1, sum2, sum3, sum4;
    wire [9:0] sum5, sum6;
    wire [10:0] sum7;

    // 第1级:两两相加
    assign sum1 = p0 + p1;
    assign sum2 = p2 + p3;
    assign sum3 = p4 + p5;
    assign sum4 = p6 + p7;

    // 第2级:继续合并
    assign sum5 = sum1 + sum2;
    assign sum6 = sum3 + sum4;

    // 第3级:加上最后一个数
    assign sum7 = sum5 + sum6;

    // 第4级:加上p8
    assign result = sum7 + p8;

endmodule

这种加法树结构,延迟是log2(N)级,比串行加法快得多。我在做YOLO的卷积层加速时,就是用这种树形结构,配合流水线,实现了每时钟周期输出一个卷积结果。

关键总结:

  • 小位宽(≤8位)用行波进位,简单省面积
  • 中位宽(8-32位)用超前进位,平衡速度和面积
  • 高位宽(>32位)或高频场景,必须上流水线
  • 卷积加速器里,加法树+流水线是标配

好了,加法器这块就聊到这儿。记住:没有最好的加法器,只有最适合你项目的加法器。下次遇到时序问题,先别急着调流水线,看看是不是加法器选型出了问题。

加法器设计决策树 加法器设计 位宽 ≤ 8位 位宽 > 8位 行波进位加法器 优点:结构简单,面积小 缺点:延迟随位宽线性增长 超前进位加法器 优点:延迟低,适合中位宽 缺点:面积随位宽指数增长 高频场景 → 流水线加法器 决策节点 方案节点 说明节点

可可精品资料库