记得我第一次接触数字电路时,看着那些密密麻麻的门电路图就头疼。直到有一天导师拿着两个开关和一个灯泡对我说:"这就是最基础的数字逻辑"。确实,计算机世界里最复杂的运算,本质上都是由与、或、非这些基本门电路组合而成的。今天我们就从最基础的加法器开始,用Verilog这把"钥匙"打开数字电路设计的大门。
加法器是CPU算术逻辑单元(ALU)的核心组件,从8位单片机到64位服务器CPU都离不开它。理解加法器的工作原理,不仅能帮我们掌握组合逻辑设计方法,更是学习FPGA开发的绝佳起点。我会带着大家用"真值表→逻辑表达式→门电路→Verilog代码"的标准设计流程,完整实现半加器和全加器。过程中你还会看到,如何用两个半加器"拼"出一个全加器——这种模块化思想在复杂IC设计中至关重要。
半加器(Half Adder)之所以叫"半",是因为它只能处理单比特加法而不考虑进位输入。就像小学生刚学加法时还不会处理"进位到十位"的情况。我们先列出它的真值表:
| A (输入) | B (输入) | Sum (和) | Cout (进位) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
观察Sum列,是不是很像"相同为0,不同为1"的异或(XOR)逻辑?而Cout列则是标准的与(AND)操作。这引出了半加器的核心表达式:
根据上述表达式,我们可以用基本门电路搭建硬件实现:
code复制A ────┐
XOR ─── Sum
B ────┘
A ────┐
AND ─── Cout
B ────┘
在面包板上实际搭建这个电路时,建议使用74系列芯片:74LS86(四路异或)和74LS08(四路与门)。连接电源和地线后,用拨码开关作为输入A/B,LED灯显示Sum/Cout,你会直观看到1+1=10(二进制)的运算过程。
用Verilog描述这个电路简直像写伪代码一样简单:
verilog复制module add_half(
input A,
input B,
output S,
output C
);
assign S = A ^ B; // 异或运算
assign C = A & B; // 与运算
endmodule
测试时可以用以下testbench代码:
verilog复制initial begin
A=0; B=0; #10;
A=0; B=1; #10;
A=1; B=0; #10;
A=1; B=1; #10;
$finish;
end
在ModelSim中运行后,你会看到波形图完美复现真值表。这就是硬件描述语言的魅力——用代码"画"出电路。
半加器的局限在于无法处理进位输入,就像不会做竖式加法。全加器(Full Adder)通过增加进位输入Cin解决了这个问题,其真值表如下:
| 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列,发现当三个输入中1的个数为奇数时Sum=1,这提示我们可以用三级异或实现:
Sum = A ⊕ B ⊕ Cin
而Cout的逻辑是:当至少两个输入为1时产生进位。用与或表达式表示为:
Cout = (A&B) | (A&Cin) | (B&Cin)
全加器的门级实现需要更多元件:
code复制A ────┐
XOR ───┬── XOR ─── Sum
B ────┘ │
Cin
A ────┐
AND ───┐
B ────┘ │
OR ─── Cout
A ────┐ │
AND ───┘
Cin ──┘
B ────┐
AND ───┘
Cin ──┘
实际布线时会发现需要3个与门和1个或门。在FPGA中,这些逻辑会被映射到查找表(LUT)资源上。
verilog复制module add_full(
input A,
input B,
input Cin,
output S,
output Cout
);
assign S = A ^ B ^ Cin;
assign Cout = (A&B) | (A&Cin) | (B&Cin);
endmodule
进阶技巧:可以用位拼接运算符优化Cout逻辑:
verilog复制assign Cout = (A+B+Cin) > 1; // 当和大于1时产生进位
全加器可以分解为两个半加器和一个或门:
这种设计展示了数字电路的重要思想——层次化设计。就像用基础积木搭建复杂结构,在芯片设计中这种思想被广泛应用。
verilog复制module add_full_using_half(
input A,
input B,
input Cin,
output S,
output Cout
);
wire S1, C1, C2;
add_half HA1(.A(A), .B(B), .S(S1), .C(C1));
add_half HA2(.A(S1), .B(Cin), .S(S), .C(C2));
assign Cout = C1 | C2;
endmodule
这里我们实例化了两个半加器模块,通过内部连线(wire)将它们连接起来。注意实例化时的端口映射方式:.A(A)表示将模块的A端口连接到当前模块的A信号。
将多个全加器串联,前一级的Cout连接下一级的Cin,就构成了n位行波进位加法器(Ripple Carry Adder)。虽然结构简单,但进位信号需要逐级传递,导致延迟随位数线性增加。
verilog复制module adder_4bit(
input [3:0] A,
input [3:0] B,
output [3:0] Sum,
output Cout
);
wire [2:0] carry;
add_full FA0(.A(A[0]), .B(B[0]), .Cin(1'b0), .S(Sum[0]), .Cout(carry[0]));
add_full FA1(.A(A[1]), .B(B[1]), .Cin(carry[0]), .S(Sum[1]), .Cout(carry[1]));
add_full FA2(.A(A[2]), .B(B[2]), .Cin(carry[1]), .S(Sum[2]), .Cout(carry[2]));
add_full FA3(.A(A[3]), .B(B[3]), .Cin(carry[2]), .S(Sum[3]), .Cout(Cout));
endmodule
工业级CPU中使用的是超前进位加法器(Carry Lookahead Adder),通过并行计算进位信号显著提升速度。其核心思想是提前计算所有位的进位,而不是等待前一级的进位结果。
verilog复制// 简化版的4位CLA实现
module cla_4bit(
input [3:0] A,
input [3:0] B,
output [3:0] Sum,
output Cout
);
wire [3:0] G = A & B; // 生成信号
wire [3:0] P = A | B; // 传播信号
wire [3:0] C;
assign C[0] = G[0] | (P[0] & 1'b0);
assign C[1] = G[1] | (P[1] & G[0]);
assign C[2] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]);
assign C[3] = G[3] | (P[3] & G[2]) | (P[3] & P[2] & G[1]) | (P[3] & P[2] & P[1] & G[0]);
assign Cout = C[3];
assign Sum = A ^ B ^ {C[2:0], 1'b0};
endmodule
在FPGA实现时,需要用时序约束指导工具优化。例如在Vivado中:
tcl复制create_clock -period 10 [get_ports clk]
set_input_delay 2 -clock clk [all_inputs]
set_output_delay 1 -clock clk [all_outputs]
对于行波进位加法器,关键路径是进位链。可以通过寄存器打拍或流水线设计提高时钟频率。
完善的验证流程包括:
推荐使用SystemVerilog编写更强大的测试用例:
systemverilog复制initial begin
for (int i=0; i<8; i++) begin
{A,B,Cin} = i;
#10;
assert ({Cout,Sum} === A+B+Cin) else $error("Test failed");
end
end
在资源受限的FPGA设计中,需要根据需求选择加法器实现方式:
在Xilinx FPGA中,一个有趣的现象是:综合工具可能把加法运算符(+)自动优化为DSP48模块,这有时比LUT实现更高效。可以通过以下方式控制:
verilog复制(* use_dsp48 = "no" *) wire [7:0] sum = a + b;