资讯动态

从半加器到超前进位:加法器设计原理与Verilog实现

发布时间:2026/10/15 17:35:56 来源:尧图企业网站定制
1. 从半加器到全加器为什么需要两级结构很多人学数字电路学到加法器这一节就开始犯迷糊。教材上先讲半加器再讲全加器然后告诉你全加器可以由两个半加器加一个或门拼出来。但为什么要这么拼直接用一个电路把三个输入加起来不行吗这个问题不搞清楚后面串行进位、超前进位就更看不懂了。先说半加器。半加器处理的是两个一位二进制数的相加输出一个和位Sum和一个进位Carry。它的真值表非常直观0000010110011110。从逻辑表达式看Sum就是A和B的异或Carry就是A和B的与。两个门电路就能搞定简单利落。但问题来了。做多位加法的时候比如算一个8位加法最低位可以用半加器但从第二位开始每一位都必须考虑来自低位的进位。也就是说一个完整的加法单元需要处理三个输入加数A、被加数B、低位进位Cin。半加器只有两个输入不够用。这时候全加器就登场了。全加器的核心思路是把三个输入的加法拆成两步。第一步A和B先加得到一个中间和S1和中间进位C1第二步把S1和Cin再加得到最终和S和进位C2。两个进位C1和C2只要有一个为1最终进位Cout就是1所以用一个或门把两个进位合并输出。这个拆分看起来多了一步但它的好处在于每一步都是两个数的加法可以直接复用半加器的结构。在CMOS工艺里这意味着你只需要设计一种标准单元然后重复调用。从版图面积和设计复杂度来看这比重新设计一个三输入加法电路要划算得多。注意全加器的逻辑表达式Sum A ⊕ B ⊕ CinCout AB Cin(A ⊕ B)。这个表达式在化简时有一个技巧——把A ⊕ B看作一个整体这样Cout的形式就和半加器的进位一模一样只是多了一项Cin乘以(A ⊕ B)。实际设计中全加器的晶体管级实现有很多种。常见的有基于传输门的实现、基于互补CMOS的实现、以及基于传输管逻辑的实现。不同实现方式在速度、面积、功耗上的取舍不同。比如传输门实现的全加器只需要20个晶体管左右而互补CMOS实现可能需要28个以上。对于初学者来说先掌握逻辑层面的结构再去看晶体管级的优化会清晰很多。2. 串行进位加法器的延迟账本搞清楚了全加器的结构接下来最自然的想法就是把N个全加器串起来最低位的Cin接0最低位的Cout接次低位的Cin依次类推。这就是串行进位加法器也叫行波进位加法器。结构极其简单N位加法只需要N个全加器连线也不复杂。但它的致命问题在于延迟。每一位的进位输出必须等低位的进位算出来之后才能稳定。假设每个全加器的进位延迟是t_carry那么N位加法器的总进位延迟就是N × t_carry。对于32位加法器如果每个全加器的进位延迟是0.5ns总延迟就是16ns。这个数字在现代处理器里是不可接受的——一个时钟周期可能才0.3ns。这里需要算一笔更细的账。全加器的延迟并不是一个单一的数字从输入到Sum和从输入到Cout的延迟是不同的。通常Cout的延迟比Sum小因为进位路径经过的门更少。在串行进位结构中关键路径是最低位的进位一路传播到最高位的进位然后再影响最高位的Sum输出。所以总延迟 (N-1) × t_carry t_sum。这个公式告诉我们两个重要信息。第一加法器的位宽越大延迟线性增长这是串行进位的根本瓶颈。第二最后一位的Sum延迟也要算进去不能只算进位链。很多人在估算时只算进位链结果实际仿真出来的延迟比预期大了一截就是因为忽略了最后那一段Sum的延迟。那为什么还要用串行进位因为它的面积小、功耗低。在那些对速度要求不高的场合比如低功耗嵌入式芯片、或者时钟频率很低的控制逻辑里串行进位加法器反而是最优解。我见过一些低功耗设计明明可以用更快的结构但为了省面积和功耗故意选了串行进位然后通过降低时钟频率来满足时序。这是一种系统级的权衡不是单纯的电路问题。提示在估算串行进位加法器的延迟时不要只看进位链。最后一位的Sum输出延迟同样在关键路径上。实际仿真时把输入跳变设置在进位传播最坏的情况下才能测到真实的延迟。还有一个容易被忽略的细节进位链上的负载。每一位的Cout要驱动下一级的Cin同时还要驱动自己的Cout逻辑。如果直接用标准单元拼接进位链上的负载会随着位宽增加而累积导致延迟比理论值更大。解决办法是在进位链上插入缓冲器但这又会增加面积和功耗。所以在实际项目中串行进位加法器的位宽一般不会超过8位再大就会考虑其他结构。3. 超前进位用面积换速度的经典交易串行进位的延迟问题催生了超前进位加法器。它的核心思想很直接既然进位是一级一级传的那我能不能提前把每一位的进位都算出来不等低位传过来答案是能但代价是逻辑复杂度急剧上升。超前进位的数学基础是进位生成和进位传播的概念。对于第i位定义生成信号Gi Ai · Bi传播信号Pi Ai ⊕ Bi。Gi表示这一位自己会产生进位不管低位有没有进位Pi表示这一位会传播低位来的进位如果低位有进位这一位就会把它传到下一位。有了这两个信号第i1位的进位可以写成C(i1) Gi Pi · Ci。把这个式子展开C(i1) Gi Pi·G(i-1) Pi·P(i-1)·G(i-2) ... Pi·P(i-1)·...·P0·C0。这意味着每一位的进位都可以直接用初始进位C0和所有低位的G、P信号表示不需要等待进位逐级传播。从延迟角度看超前进位把进位链的延迟从O(N)降到了O(log N)。对于4位一组进位延迟大约是2到3级门延迟而不是4级。对于16位加法器如果用两级超前进位4位一组组间再超前进位总延迟大约是4到5级门延迟而串行进位需要16级。但这里有一个工程上的现实超前进位的逻辑表达式会随着位宽增加而急剧膨胀。4位超前进位的进位逻辑已经需要多输入与门和或门8位的话最高位的进位需要8输入的与门扇入太大实际实现时要么用多级门树要么用传输管逻辑。所以实际设计中超前进位通常是分组进行的每组4位左右组内超前进位组间再用串行或二级超前进位。结构类型进位延迟面积适用场景串行进位O(N)最小低功耗、低频、小位宽超前进位单级O(log N)中等中位宽、中高频超前进位多级O(log N)较大高位宽、高频进位选择O(√N)大特定工艺下的折中进位选择加法器是另一种思路。它把加法器分成若干块每块同时计算Cin0和Cin1两种情况的结果然后用低位的实际进位去选择正确的输出。这样每块的延迟是固定的不随位宽增加。代价是面积翻倍因为每块要算两遍。在实际项目中进位选择常用于那些对延迟有严格要求、但面积预算相对宽松的场合。注意超前进位的逻辑表达式虽然看起来复杂但它的规则性很强非常适合用脚本自动生成。如果你在做一个参数化的加法器IP写一个Python脚本生成Verilog代码会比手写高效得多而且不容易出错。4. 用Verilog实现一个参数化加法器从行为级到门级理论讲完了接下来动手写代码。我选择用Verilog来实现因为它在数字电路教学和工业界都最常用。目标是一个参数化位宽的超前进位加法器位宽可以通过参数配置方便复用到不同的项目里。先看行为级描述。行为级加法器最简单直接用“”号module adder_behavioral #(parameter WIDTH 8) ( input [WIDTH-1:0] a, input [WIDTH-1:0] b, input cin, output [WIDTH-1:0] sum, output cout ); assign {cout, sum} a b cin; endmodule这段代码综合工具会自动帮你选择合适的加法器结构。对于FPGA设计这通常是最优解因为综合工具对目标器件的底层结构非常了解。但在ASIC设计里你可能需要手动控制结构因为综合工具不一定能生成最优的进位链。接下来看门级实现。先写一个全加器模块module full_adder ( input a, input b, input cin, output sum, output cout ); wire s1, c1, c2; assign s1 a ^ b; assign sum s1 ^ cin; assign c1 a b; assign c2 s1 cin; assign cout c1 | c2; endmodule然后用generate语句拼接成N位串行进位加法器module adder_ripple #(parameter WIDTH 8) ( input [WIDTH-1:0] a, input [WIDTH-1:0] b, input cin, output [WIDTH-1:0] sum, output cout ); wire [WIDTH:0] carry; assign carry[0] cin; genvar i; generate for (i 0; i WIDTH; i i 1) begin : fa_loop full_adder u_fa ( .a(a[i]), .b(b[i]), .cin(carry[i]), .sum(sum[i]), .cout(carry[i1]) ); end endgenerate assign cout carry[WIDTH]; endmodule这段代码结构清晰但综合出来的进位链是串行的。如果你想做超前进位需要把进位逻辑单独拿出来写。下面是一个4位超前进位模块module cla_4bit ( input [3:0] a, input [3:0] b, input cin, output [3:0] sum, output cout ); wire [3:0] g, p; wire [4:0] 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 c[4] 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[3:0]; assign cout c[4]; endmodule注意c[4]的表达式有5个乘积项最高项是5输入的与。在实际综合时这个5输入与门会被拆成多级延迟并不比串行进位好多少。所以4位超前进位是一个比较平衡的选择再宽的话逻辑深度和扇入都会成为问题。提示写超前进位的时候建议用脚本生成进位表达式而不是手写。手写4位还行8位的话最高位的进位表达式有9个乘积项很容易写错。用Python写一个生成器输入位宽输出Verilog代码既快又准。5. 仿真验证怎么确认加法器真的算对了代码写完了下一步是验证。加法器的验证看起来简单但有几个容易漏掉的点。第一进位输入cin的两种取值都要测。第二边界情况要覆盖全0加全0、全1加全1、全1加0、以及进位传播最长的模式。第三要检查溢出标志是否正确。我一般用两种方式验证定向测试和随机测试。定向测试覆盖边界情况随机测试覆盖大范围的输入组合。下面是一个简单的testbench框架module tb_adder; parameter WIDTH 8; reg [WIDTH-1:0] a, b; reg cin; wire [WIDTH-1:0] sum; wire cout; adder_ripple #(.WIDTH(WIDTH)) u_dut ( .a(a), .b(b), .cin(cin), .sum(sum), .cout(cout) ); integer i, j; reg [WIDTH:0] expected; initial begin // 定向测试 a 0; b 0; cin 0; #10; a {WIDTH{1b1}}; b 0; cin 1; #10; a {WIDTH{1b1}}; b {WIDTH{1b1}}; cin 0; #10; // 随机测试 for (i 0; i 1000; i i 1) begin a $random; b $random; cin $random % 2; #10; expected a b cin; if ({cout, sum} ! expected) begin $display(FAIL: a%h b%h cin%b, got %h, expected %h, a, b, cin, {cout, sum}, expected); end end $display(Test done); $finish; end endmodule这个testbench里有一个细节值得注意expected被声明为reg [WIDTH:0]比sum宽一位这样才能容纳进位。比较的时候用{cout, sum}拼接成WIDTH1位和expected对齐。如果不注意位宽比较结果可能永远是对的因为高位被截断了。另一个容易踩的坑是延迟。在testbench里给输入赋值后要加延迟#10让组合逻辑稳定下来再比较。如果忘了加延迟仿真器可能在信号还没稳定的时候就采样导致误报。我见过有人在纯组合逻辑的testbench里不加延迟结果仿真波形看起来是对的但测试报告里全是错误排查了半天才发现是采样时机的问题。对于超前进位加法器验证的重点是进位逻辑。可以单独写一个testbench把每一位的进位输出都引出来和串行进位加法器的对应位比较。如果所有位的进位都一致那加法器的功能就是对的。这种方法比只比较最终结果更细致能定位到具体是哪一位的进位逻辑出了问题。6. 实际项目中的加法器选型与优化经验理论、代码、验证都走了一遍最后聊聊实际项目中的经验。加法器虽然是一个基础模块但在不同的应用场景下选型和优化的策略差别很大。在FPGA设计中我的建议是直接用行为级“”号让综合工具去优化。主流FPGA厂商的综合工具对加法器的优化已经非常成熟能根据目标器件的进位链结构自动映射。手动写超前进位反而可能因为映射不当而变慢。我做过对比测试在同一个FPGA上行为级加法器和手动超前进位加法器的时序差异在5%以内但行为级的代码可读性和可维护性要好得多。在ASIC设计中情况就不同了。综合工具对加法器的优化能力有限尤其是当位宽超过16位时手动控制进位链结构往往能获得更好的结果。我的经验是8位以下用串行进位8到32位用分组超前进位4位一组32位以上考虑进位选择或混合结构。当然这只是一个粗略的指导具体还要看工艺库的特点和时序目标。功耗是另一个需要关注的维度。加法器的功耗主要来自进位链的翻转。串行进位的进位链翻转率最低因为进位传播是逐级的不是所有位同时翻转。超前进位的进位逻辑翻转率更高因为每一位的进位都直接由所有低位的G、P信号决定输入变化时进位翻转更频繁。在低功耗设计中如果时序允许串行进位反而是更好的选择。提示在低功耗设计中可以在进位链上插入时钟门控或者使用操作数隔离技术。当加法器的输入在多个周期内保持不变时隔离输入可以避免进位链的无谓翻转节省动态功耗。还有一个实际项目中经常遇到的问题加法器的位宽不是固定的。比如一个处理器里可能有8位、16位、32位、64位多种加法器。如果每种位宽都单独设计维护成本很高。我的做法是写一个参数化的加法器模块位宽作为参数内部根据位宽自动选择结构。比如位宽小于等于8用串行进位大于8用分组超前进位。这样一套代码覆盖所有需求综合时根据参数生成对应的结构。最后说一个关于时序约束的经验。加法器的时序约束不能只约束输入到输出的延迟还要约束进位链的路径。在综合脚本里对进位链设置set_max_delay或者set_multicycle_path可以给综合工具更多的优化空间。如果只约束输入输出综合工具可能会为了满足输入输出延迟而牺牲进位链的优化导致整体时序变差。加法器这个模块看起来简单但真正做好做精需要理解的细节不少。从半加器到全加器从串行到超前进位从行为级到门级每一步都有它的道理和取舍。我在实际项目中最大的体会是没有绝对最优的结构只有最适合当前场景的结构。面积、速度、功耗这三个维度永远在做交易。理解每个结构背后的交易逻辑比记住某个具体的电路图更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑