加法器设计(一):从半加器到超前进位加法器
各位同学,咱们今天开始聊加法器。说实话,加法器这东西,是数字电路里最基础、也最核心的运算单元。你在RISC-V处理器里看到的浮点运算,底层拆开来,全是加法器在干活。我当年刚入行时,总觉得加法器太简单,不就是个二进制加法嘛。直到我在一个高性能计算项目里,被时序问题卡了整整两周,才真正明白——加法器设计得好不好,直接决定了你的芯片能跑多快。
好,咱们一步步来。先从最简单的开始。
1. 半加器:最朴素的加法单元
半加器,说白了就是两个二进制位相加。它不考虑来自低位的进位,只输出一个和(Sum)和一个进位(Carry)。
真值表很简单:
| A | B | Sum | Carry |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
你看,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。
真值表:
| A | B | Cin | Sum | Cout |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
逻辑表达式:
- 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
3. 行波进位加法器:简单但慢
行波进位加法器(Ripple Carry Adder,RCA),就是把N个全加器串起来。每个全加器的进位输出,接到下一个全加器的进位输入。
我画个图帮你理解:
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
说白了,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做尾数加法。把这些基础打牢了,后面学浮点运算会轻松很多。
好,今天就到这儿。代码自己跑一遍,有问题随时交流。