加法器设计(一):从半加器到超前进位加法器

各位同学,咱们今天开始聊加法器。说实话,加法器这东西,是数字电路里最基础、也最核心的运算单元。你在RISC-V处理器里看到的浮点运算,底层拆开来,全是加法器在干活。我当年刚入行时,总觉得加法器太简单,不就是个二进制加法嘛。直到我在一个高性能计算项目里,被时序问题卡了整整两周,才真正明白——加法器设计得好不好,直接决定了你的芯片能跑多快。

好,咱们一步步来。先从最简单的开始。

1. 半加器:最朴素的加法单元

半加器,说白了就是两个二进制位相加。它不考虑来自低位的进位,只输出一个和(Sum)和一个进位(Carry)。

真值表很简单:

ABSumCarry
0000
0110
1010
1101

你看,Sum = A XOR B,Carry = A AND B。就这么简单。

Verilog实现:

module half_adder (
    input  wire a,
    input  wire b,
    output wire sum,
    output wire carry
);
    assign sum   = a ^ b;
    assign carry = a & b;
endmodule

嗯,这里要注意:半加器只能处理最低位的加法。为什么?因为它没有进位输入。你想想看,多位数相加时,每一位都可能收到来自低位的进位。半加器搞不定这个。

2. 全加器:把进位考虑进来

全加器比半加器多了一个进位输入(Cin)。三个输入:A、B、Cin,两个输出:Sum、Cout。

真值表:

ABCinSumCout
00000
00110
01010
01101
10010
10101
11001
11111

逻辑表达式:

  • Sum = A XOR B XOR Cin
  • Cout = (A & B) | (A & Cin) | (B & Cin)

Verilog实现:

module full_adder (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
我的小经验: 全加器可以用两个半加器拼起来。第一个半加器算A+B,第二个半加器把结果和Cin相加。这样做的好处是,在FPGA里LUT资源紧张时,可以复用已有的半加器模块。

3. 行波进位加法器:简单但慢

行波进位加法器(Ripple Carry Adder,RCA),就是把N个全加器串起来。每个全加器的进位输出,接到下一个全加器的进位输入。

我画个图帮你理解:

FA0 (bit 0) FA1 (bit 1) FA2 (bit 2) FA3 (bit 3) C0 C1 C2 Cout (C3) Cin (C-1) A0 B0 A1 B1 A2 B2 A3 B3 S0 S1 S2 S3

4位行波进位加法器的Verilog实现:

module ripple_carry_adder_4bit (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       cin,
    output wire [3:0] sum,
    output wire       cout
);
    wire c0, c1, c2;
    
    full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin),   .sum(sum[0]), .cout(c0));
    full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c0),    .sum(sum[1]), .cout(c1));
    full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c1),    .sum(sum[2]), .cout(c2));
    full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c2),    .sum(sum[3]), .cout(cout));
endmodule
避坑指南: 行波进位加法器的关键路径太长。每个全加器都有门延迟,N位加法器的总延迟就是N倍的单级延迟。我曾经在一个32位加法器项目里用了RCA结构,结果时序分析报告一片红。后来改成超前进位,才把频率从50MHz拉到200MHz。

说白了,RCA适合位宽小(比如4位、8位)或者对速度要求不高的场景。位宽一大,你就得换思路了。

4. 超前进位加法器:快,但更复杂

超前进位加法器(Carry Lookahead Adder,CLA)的核心思想是:提前算出进位,而不是等上一级算完再传过来。

怎么提前算?我们定义两个中间信号:

  • 生成信号(Generate):G = A & B。只要A和B都是1,这一位一定会产生进位。
  • 传播信号(Propagate):P = A XOR B。只要A和B中有一个是1,进位就能从这一位传过去。

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

C0 = Cin
C1 = G0 | (P0 & C0)
C2 = G1 | (P1 & G0) | (P1 & P0 & C0)
C3 = G2 | (P2 & G1) | (P2 & P1 & G0) | (P2 & P1 & P0 & C0)
...

你看,每个进位只依赖输入和Cin,不依赖中间进位。这就是快的秘密。

4位超前进位加法器Verilog实现:

module carry_lookahead_adder_4bit (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       cin,
    output wire [3:0] sum,
    output wire       cout
);
    wire [3:0] g, p, c;
    
    // 生成和传播信号
    assign g = a & b;
    assign p = a ^ b;
    
    // 超前进位逻辑
    assign c[0] = cin;
    assign c[1] = g[0] | (p[0] & c[0]);
    assign c[2] = g[1] | (p[1] & g[0]) | (p[1] & p[0] & c[0]);
    assign c[3] = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & c[0]);
    assign cout = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1]) | (p[3] & p[2] & p[1] & g[0]) | (p[3] & p[2] & p[1] & p[0] & c[0]);
    
    // 求和
    assign sum = p ^ c;
endmodule
关键对比:
  • 行波进位:延迟 = N × T_FA(N位宽,T_FA为单级全加器延迟)
  • 超前进位:延迟 ≈ 3 × T_gate(与位宽基本无关,但受扇入限制)

我个人的经验是:16位以内用CLA很舒服,超过16位建议用分层结构,比如4位CLA拼成16位,再拼成64位。否则门扇入太大,反而拖慢速度。

5. 仿真验证:别偷懒

写完了代码,一定要仿真。我见过太多人写完加法器直接上板,结果跑出来全是错的。

一个简单的testbench:

module tb_adder;
    reg  [3:0] a, b;
    reg        cin;
    wire [3:0] sum_rca, sum_cla;
    wire       cout_rca, cout_cla;
    
    ripple_carry_adder_4bit u_rca (.a(a), .b(b), .cin(cin), .sum(sum_rca), .cout(cout_rca));
    carry_lookahead_adder_4bit u_cla (.a(a), .b(b), .cin(cin), .sum(sum_cla), .cout(cout_cla));
    
    initial begin
        // 遍历所有输入组合
        for (int i = 0; i < 16; i++) begin
            for (int j = 0; j < 16; j++) begin
                for (int k = 0; k < 2; k++) begin
                    a = i; b = j; cin = k;
                    #10;
                    // 检查RCA和CLA结果是否一致
                    if ({cout_rca, sum_rca} != {cout_cla, sum_cla})
                        $display("Mismatch: a=%d b=%d cin=%d", a, b, cin);
                end
            end
        end
        $display("Test completed.");
        $finish;
    end
endmodule

嗯,这里有个小技巧:同时例化RCA和CLA,对比它们的输出。如果结果不一致,说明至少有一个写错了。我在项目中经常用这种「背靠背验证」的方法,省了不少调试时间。

6. 总结一下

今天咱们聊了:

  • 半加器:两个输入,没有进位输入。适合最低位。
  • 全加器:三个输入,考虑进位。是加法器的基本单元。
  • 行波进位加法器:简单直观,但速度慢。位宽大了别用。
  • 超前进位加法器:提前算进位,速度快。但逻辑复杂,注意扇入限制。

说实话,加法器设计是数字IC工程师的基本功。你在RISC-V的浮点运算单元里,会看到各种加法器的变种——比如用CLA做指数对齐、用RCA做尾数加法。把这些基础打牢了,后面学浮点运算会轻松很多。

好,今天就到这儿。代码自己跑一遍,有问题随时交流。


专注资料整理