news 2026/9/16 7:46:59

4bit超前进位加法器设计:从逻辑推导到16bit两级CLA与仿真

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
4bit超前进位加法器设计:从逻辑推导到16bit两级CLA与仿真

简介:这是一份面向FPGA开发的Verilog学习资源,重点围绕4位超前进位加法器以及如何用4位加法器级联成16位加法器,系统讲解数字逻辑设计方法。资源基于Vivado工具完成工程搭建、逻辑综合与仿真验证,适合初学者熟悉硬件描述语言开发流程,也适合进阶者复习进位链优化思路。压缩包内共432个文件,总大小约1.68MB,内容涵盖Verilog源码、Xilinx工程文件、批处理与Tcl运行脚本、综合报告和仿真日志等,各类文件分工明确,目录结构清晰,便于按模块查阅。工程源码分别实现了4位CLA和16位级联加法器,测试工程包含仿真激励,可帮助读者核对超前进位生成函数、传播函数以及最终进位结果是否正确。同时,通过波形仿真可观察进位级联过程。已有2775人学习或下载,打开即可复用或修改,适合作为课程设计、实验报告或FPGA入门实践的完整基础模板。

1. 为什么4bit超前进位加法器值得手写一遍

行波进位加法器每产生一位进位都要等前一位算完,16bit行波的最坏路径从bit0一路串到bit15,这在数字IC笔试和FPGA面试里几乎是必问的题:怎么把这条进位链砍短。超前进位加法器(Carry Look-Ahead Adder)把进位从“逐级传递”改成“按公式并行展开”,4bit块内用3级左右的门同时算出C1到C4,再把4bit块拼成16bit后,关键路径与“等16次进位”无关。这个题目正好覆盖组合逻辑推导、时序估算和模块例化三项基本功,无论目标是设计岗还是验证岗,手写一遍都比背公式收获更大。

2. 4bit超前进位加法器的逻辑推导与Verilog实现

2.1 用G和P把全加器拆成“生成”与“传播”

对于一个全加器,输入是A、B、Cin,输出和S与进位Cout。把真值表化简后得到两个等价式:

S = A ^ B ^ Cin
Cout = (A & B) | (A & Cin) | (B & Cin)

如果只盯着Cout看,可以把它拆成一条与Cin无关的路径和一条依赖Cin的路径。令G = A & B,P = A ^ B,Cout变成:

Cout = G | (P & Cin)

当A和B同时为1时,G=1,本bit无论Cin是0还是1都会向高位产生进位,所以G叫生成信号(generate)。当A和B不同时,G=0,若P=1,高位的进位等于Cin原样透传,所以P叫传播信号(propagate)。S也可以借助P写得更短:S = P ^ Cin。

把加法和进位拆成两组中间信号后,原来“后一位要等前一位Cout”的递归链条,变成了可以逐层展开的布尔表达式。这是超前进位加法器和行波进位加法器在结构上最大的区别。

2.2 展开C1到C4:把串行进位换成并行表达式

4bit CLA 的接口我用下列信号:输入A[3:0]、B[3:0]、Cin,输出S[3:0]、Cout。对每一位,定义G[i] = A[i] & B[i],P[i] = A[i] ^ B[i]。从最低位开始展开进位:

C[0] = Cin
C[1] = G[0] | (P[0] & C[0])
C[2] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & C[0])
C[3] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & C[0])
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])

C[1]到C[4]没有任何一项依赖C[1]、C[2]、C[3]自身的计算结果,等式右边全部来自输入和C[0]。也就是说,这4个进位可以同时算出来,等待时间只等于与或门深度,而不是4次串行加法。对应关系如下表:

进位信号最长项需要等待的进位
C[1]P0 & C0C0
C[2]P1 & P0 & C0C0
C[3]P2 & P1 & P0 & C0C0
C[4]P3 & P2 & P1 & P0 & C0C0

每个进位都只依赖C0,这就是“超前进位”中“超前”二字的来源。位宽继续增大时,C[n]最长的与项会变成n+1个输入,门扇入迅速变大,所以工程上很少做8bit以上的单级CLA,4bit是常见粒度。

2.3 可直接落地的4bit超前进位加法器Verilog代码

下面是我在项目里常用的写法,模块同时输出Gg和Pg,为后面16bit扩展留好接口:

module cla4 ( input [3:0] a, input [3:0] b, input cin, output [3:0] s, output cout, output Gg, // 组生成:本4bit块独立产生进位 output Pg // 组传播:本4bit块把进位透传下去 ); wire [3:0] g = a & b; wire [3:0] p = a ^ b; wire [3:0] c; assign c[0] = cin; assign c[1] = g[0] | (p[0] & cin); assign c[2] = g[1] | (p[1] & g[0]) | (p[1] & p[0] & cin); assign c[3] = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & cin); assign Gg = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1]) | (p[3] & p[2] & p[1] & g[0]); assign Pg = p[3] & p[2] & p[1] & p[0]; assign cout = Gg | (Pg & cin); assign s = p ^ c; endmodule

这段代码的核心是三个部分。第一部分用两条wire位向量算出每一位的G和P,g和p只依赖a、b,与进位无关。第二部分用assign并行展开C[1]到C[3],每个表达式都直接写完整,不引入中间进位变量。第三部分把C[4]拆成Gg与Pg两个输出,Gg代表“本组自己产生进位”的所有条件,Pg代表“组内的4位全部允许进位透传”的条件,因此cout = Gg | (Pg & cin)。

如果只做4bit加法器,Gg和Pg不接任何信号即可,综合器会优化掉;如果做16bit,这两个输出会在下一级CLA里参与组间进位计算。S用p ^ c而不是直接用加法运算符,目的是保持CLA的结构:和位本来就等于P[i] ^ C[i],写成p ^ c能让综合器识别出异或树,避免把整个模块重新推断成行波进位加法器。

提示:只要输入组合没变化,这组assign的综合结果就是一张纯组合逻辑表。不要在always块里用case把所有2^32输入组合枚举出来,那样综合器反而可能因为case分支太多而重新生成串行进位链。

3. 用4bit加法器搭建16bit加法器:行波级联与两级超前进位

3.1 为什么16bit不直接写16位展开式

理论上可以把C[0]到C[16]全部展开,C[16]的最长与项会包含17个输入,综合到FPGA的6输入LUT上需要拆成多级LUT,时钟频率不升反降。更常见的做法是把位宽切成4bit小块,块内复用cla4,块与块之间再单独设计进位网络。基于上一节的cla4,16bit加法器自然有两种拼法:组间行波级联和组间超前进位。

3.2 方案一:4个4bit加法器行波级联

第一种接法最简单,把上一级的cout接到下一级的cin,依次连接四个cla4:

module cla16_ripple ( input [15:0] a, b, input cin, output [15:0] s, output cout ); wire [2:0] c; // C4, C8, C12 cla4 u0 (.a(a[3:0]), .b(b[3:0]), .cin(cin), .s(s[3:0]), .cout(c[0]), .Gg(), .Pg()); cla4 u1 (.a(a[7:4]), .b(b[7:4]), .cin(c[0]), .s(s[7:4]), .cout(c[1]), .Gg(), .Pg()); cla4 u2 (.a(a[11:8]), .b(b[11:8]), .cin(c[1]), .s(s[11:8]), .cout(c[2]), .Gg(), .Pg()); cla4 u3 (.a(a[15:12]), .b(b[15:12]), .cin(c[2]), .s(s[15:12]), .cout(cout), .Gg(), .Pg()); endmodule

四个例化语句把16bit切成四段,a[3:0]、b[3:0]属于第一组,以此类推。块内部仍然是超前进位,所以真正串行的只有c[0](C4)到c[2](C12)三段组间进位。这种方式功能完全正确,代码量最小,但组间进位没有并行化,关键路径约等于4个cla4的延迟串联,16bit相对4bit的优势只体现在每段内部。

例化时把Gg和Pg显式留空,是为了避免输出悬空的编译告警;在实际工程里如果时序约束允许,也可以把Gg/Pg引出到顶层做观测。

3.3 方案二:组间超前进位,两级CLA架构

第二种方案把四个cla4的Gg、Pg收集起来,再用一组与C[1]~C[4]结构相同的表达式同时算组间进位。这样整个16bit只有两级CLA:第一级在四个块内并行算各自的G/P和低位进位,第二级同时生成C4、C8、C12、C16。

module cla16 ( input [15:0] a, b, input cin, output [15:0] s, output cout ); wire [2:0] C; // C4, C8, C12 wire [3:0] G, P; // 四个块的组生成/组传播 cla4 u0 (.a(a[3:0]), .b(b[3:0]), .cin(cin), .s(s[3:0]), .Gg(G[0]), .Pg(P[0])); cla4 u1 (.a(a[7:4]), .b(b[7:4]), .cin(C[0]), .s(s[7:4]), .Gg(G[1]), .Pg(P[1])); cla4 u2 (.a(a[11:8]), .b(b[11:8]), .cin(C[1]), .s(s[11:8]), .Gg(G[2]), .Pg(P[2])); cla4 u3 (.a(a[15:12]), .b(b[15:12]), .cin(C[2]), .s(s[15:12]), .Gg(G[3]), .Pg(P[3])); assign C[0] = G[0] | (P[0] & cin); assign C[1] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & cin); assign C[2] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & cin); 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] & cin); endmodule

u0的cin直接接顶层cin,u1到u3的cin分别接C[0]、C[1]、C[2],这四个C由第二级CLA同时产生。u0到u3的cout端口在这里不需要连接,因为每组的最终进位已经由assign的直接表达式算出来了。cla4内部计算自己的cout所用的逻辑与C[0]等表达式重复,综合时会被等价合并,不会让面积变成两倍。

从时序上看,16bit CLA的关键路径不再横跨16位,而是先经过第一级cla4算出G/P,再经过第二级CLA算出C[2],最后进入u3内部从cin到S[15]的末级路径。与组间行波相比,进位等待从“三段串行”变成“一段并行加末段组内”,在综合报告里能明显看到路径长度差异。两种方案的取舍如下表:

对比项组间行波级联两级超前进位
组间进位方式C4→C8→C12串行第二级CLA并行
关键路径约4个cla4串行约2个cla4等效深度
额外逻辑第二级CLA的与或门
代码量4条例化加约10行assign
适用场景面积优先、教学演示时序优先、ALU/DSP数据通路

如果继续扩展到64bit,两级还不够:需要把四个16bit块再次收集G/P,组成第三级CLA。位宽每乘4就多一级,关键路径按级数增长而不是按位宽增长,这正是超前进位结构在高位宽加法器里的价值。

4. 16bit超前进位加法器的testbench设计与仿真验证

4.1 自校验testbench:边界用例加5000组随机激励

验证16bit加法器不能只靠看波形。下面这个testbench同时做三件事:对全0、全1等边界输入做定点检查;产生5000组随机输入;用无符号加法表达式算出期望值并自动比对。

`timescale 1ns / 1ps module tb_cla16; reg [15:0] a, b; reg cin; wire [15:0] s; wire cout; cla16 dut ( .a(a), .b(b), .cin(cin), .s(s), .cout(cout) ); reg [16:0] expect; integer i; initial begin $dumpfile("cla16_tb.vcd"); $dumpvars(0, tb_cla16); a = 16'h0000; b = 16'h0000; cin = 1'b0; #10; if ({cout, s} !== 17'h00000) begin $display("FAIL case0: 0+0+0"); $finish; end a = 16'hFFFF; b = 16'h0000; cin = 1'b1; #10; if ({cout, s} !== 17'h10000) begin $display("FAIL case1: FFFF+0+1"); $finish; end a = 16'hFFFF; b = 16'hFFFF; cin = 1'b1; #10; if ({cout, s} !== 17'h1FFFF) begin $display("FAIL case2: FFFF+FFFF+1"); $finish; end for (i = 0; i < 5000; i = i + 1) begin a = $random; b = $random; cin = $random & 1; #5; expect = {1'b0, a} + {1'b0, b} + cin; if ({cout, s} !== expect) begin $display("MISMATCH: t=%0t a=%h b=%h cin=%b s=%h cout=%b", $time, a, b, cin, s, cout); $finish; end end $display("ALL TESTS PASSED"); $finish; end endmodule

三个边界用例分别覆盖“全0进位0”、“最大数加1”和“溢出到第17位”三种情况。{cout, s}拼接成17bit与expect比较,避免只比较s漏掉cout。随机循环里$random & 1取随机数的最低位作为cin,保证cin只有0和1两个取值。expect = {1'b0, a} + {1'b0, b} + cin把两个16bit输入显式扩展成17bit,防止出现位宽截断。

把所有用例写成自动比较而不是手动观察,是组合逻辑验证的底线;伪随机序列在同一仿真器下可重复,出现MISMATCH时第一次报错的位置就是最小复现用例。

4.2 使用iverilog和GTKWave跑仿真的具体命令

在VSCode终端或任意shell里直接执行:

iverilog -s tb_cla16 -o cla16_tb.vvp cla16.v tb_cla16.v vvp cla16_tb.vvp gtkwave cla16_tb.vcd

iverilog后面的-s tb_cla16指定仿真顶层,避免cla16和cla16_ripple同时编译时顶层不明确;-o指定生成的vvp仿真文件。vvp运行仿真,因为testbench里写了$dumpfile$dumpvars,运行目录下会生成cla16_tb.vcd。gtkwave打开波形文件查看内部信号。没有波形需求时可以省略最后一条命令,直接看ALL TESTS PASSED输出。

如果想连组间行波版一起回归,把cla16_ripple也加入编译列表,例化两个DUT后对{cout, s}做一致性比较,代码量增加不到十行,能顺带确认两种架构结果完全等价。

4.3 波形里检查两级CLA的关键信号

打开GTKWave后,把tb_cla16顶层下的C[0]、C[1]、C[2]、G[0..3]、P[0..3]加到波形窗口。重点观察一组随机输入下C[1]是否和手工计算的G[1] | (P[1]&G[0]) | ... 一致。检查点见下表:

信号输入条件期望值
C[0](即C4)a[3:0]=4'hF, b[3:0]=4'h0, cin=11
C[1](即C8)a[7:4]=4'hF, b[7:4]=4'h0, 前级进位=11
cout(即C16)a[15:12]=4'hF, b[15:12]=4'h0, C[2]=11

这三行分别覆盖组间进位的三级传递。如果C[0]正确而C[1]错误,问题出在第二级CLA的表达式,与组内cla4无关;如果C[0]就错,先回头查第一级的G[0]、P[0]。在Xilinx工程里也可以用ILA把C/G/P抓到片上调试,原理和看波形一样,只是采样点换成了真实时钟边沿。

5. 超前进位加法器验证技巧与三个常踩的坑

5.1 三个常踩的坑

手撕超前进位加法器时,功能无非就是那几行展开式,但下面三个坑几乎每次都有人踩到。

现象正确做法
和位用 a+b+cin 直接算功能对,综合后回到串行进位结构S = P ^ C,保持CLA结构
Gg/Pg展开式漏项16bit扩展时只在特定组合出错逐个核对与项,或用SVA断言
组合逻辑用<=赋值仿真出现无关毛刺,综合前后不一致assign或always @*内用=

第一个坑最常见,因为功能仿真看不出差别,综合报告里才暴露。第二个坑隐蔽在16bit场景:第一级的Gg/Pg表达式与cout共用了大部分项,如果只图省事把cout赋值给Gg而不写完整展开式,级联到高位时C12、C16会在中间某组不产生进位的情况下算错。验证时把三个手工边界用例扩成第4章的随机回归,基本能兜住。

5.2 把内部进位一起打印到回归日志里

我自己习惯在随机循环里多加一条$display,把每组输入对应的C[0]~C[2]打出来:

$display("a=%h b=%h cin=%b -> s=%h cout=%b | G=%b P=%b C=%b", a, b, cin, s, cout, dut.G, dut.P, dut.C);

配合grep就能在日志里快速筛选组间进位为1的样本:

vvp cla16_tb.vvp | grep "C=1"

这一行会把所有组间进位为1的样本筛出来,配合a、b的输入值验证进位预测是否符合手工展开式。回归时执行iverilog -s tb_cla16 -o cla16_tb.vvp cla16.v tb_cla16.v && vvp cla16_tb.vvp | grep -c "ALL TESTS PASSED",计数为1就是当天改动没有破坏进位链的最直接证据。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 7:46:52

从零搭建上帝视角视觉系统:全景拼接与实时渲染实战

1. 项目缘起&#xff1a;为什么我们需要“上帝视角”先说个场景。去年我接了一个旅游景区智能化改造的需求&#xff0c;客户反复提一个词&#xff1a;能不能让我在办公室就能看到整个园区的实时状况&#xff1f;不要那种单路摄像头的画面切来切去&#xff0c;要像打游戏一样&am…

作者头像 李华
网站建设 2026/9/16 7:45:47

agent-skills:TypeScript+NX构建可验证智能体技能协议

1. 项目概述&#xff1a;一个被严重低估的“技能容器”设计范式“agent-skills”这四个字乍看像某个开源库的包名&#xff0c;或是某篇技术文档里的二级标题&#xff0c;但如果你在Nx monorepo里翻过十几个微前端项目、在TypeScript类型系统里调试过三天泛型推导、又亲手用Node…

作者头像 李华
网站建设 2026/9/16 7:45:26

矿企业S/4HANA选择性转型(上篇):复杂ERP环境如何破局

矿企业的不断发展之际&#xff0c;最初的简单的ERP架构也随之逐步的扩张、日趋庞杂&#xff0c;最终形成了一个由多个系统、多个子系统、多个模块的庞大的ERP综合应用系统。本文将介绍如何通过SNP中国公司的S/4HANA选择性转型方案&#xff0c;降低迁移风险、严控数据范围&#…

作者头像 李华
网站建设 2026/9/16 7:44:16

12013 页只 OCR 2 页:Agent 文档处理的两遍式设计

12013 页只 OCR 2 页&#xff1a;Agent 文档处理的两遍式设计原文&#xff1a;LlamaIndex Blog - 《Just-in-Time Agentic OCR》&#xff08;https://www.llamaindex.ai/blog/just-in-time-agentic-ocr&#xff09;引子&#xff1a;一个很常见的两难 用户上传几十份 PDF&#x…

作者头像 李华
网站建设 2026/9/16 7:41:50

10.24早鸟优惠倒计时|第八届HCC2026人本计算与数据智能|Springer‑CCIS出版,多届稳定EI检索|广州会议

&#x1f4cc;平台简介&#xff1a;RDLINK研发家-一站式学术会议数字化科研服务平台 RDLINK研发家(www.yanfajia.com)是一家创新型C2M数字化学术会议科研服务平台,专注于国际学术交流、国际论文出版、学术传播、科研数字化领域提供全过程解决方案,构建开放高效的学术交流生态圈…

作者头像 李华
网站建设 2026/9/16 7:41:47

Power BI切片器设计实战:从筛选组件到业务语义翻译器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华