做FPGA开发的人,第一次意识到Carry4的存在,多半是在看综合后的Schematic或者时序报告的时候。明明RTL里只写了一行assign sum = a + b;,软件却生成了一长串叫 CARRY4 的元件,占了不少面积,还经常出现在关键路径上。如果没搞明白这东西到底在干什么,后面做时序优化、跨平台移植、资源评估时就会很被动。这篇文章就把 Carry4 的内部结构、工作原理、以及如何利用它做高性能算术逻辑讲清楚,适合刚开始接触 FPGA 内部结构的初学者,也适合那些被进位链时序问题困扰的开发朋友。
1. 为什么会有Carry4:加法器的“最后一公里”
1.1 从全加器的进位传播说起
数字电路里最基础的算术单元是全加器:输入两个加数 bit(A、B)和低位进位 CI,输出本位和 S 以及向高位的进位 CO。逻辑表达式是 S = A XOR B XOR CI,CO = (A AND B) OR (A XOR B AND CI)。把很多个全加器按位串起来,就得到一个脉动进位加法器 Ripple Carry Adder,也就是大多数 FPGA 加法器的原始形态。
但这种结构有一个天然瓶颈:本位和可以并行计算,进位却必须从 bit0 一个接一个往高位传。bit31 的求和结果要等前面 31 位的进位全部稳定之后才能确定,延迟几乎和位宽成线性关系。在实际工程里,32 位加法器不算夸张,64 位、128 位累加器也常出现,如果每个进位都走通用逻辑和通用布线,时序根本收不住。所以 FPGA 厂商必须提供一条“专门给进位走”的快速通道,这就是 Carry4 存在的第一个理由。
1.2 LUT实现加法的局限
有人会说,FPGA 里不是有 LUT 吗,LUT 不是可以实现任意组合逻辑吗,为什么不能只用 LUT 搭加法器?确实能搭,但效率很低。一个 LUT 输出只有一个端口,要实现全加器的 S 和 CO 两个输出,至少需要两个查找表;更重要的是,CO 作为输出信号离开 LUT 之后,要经过通用互连资源,再从另一个 LUT 的输入端进去。这中间的走线延迟不可控,还占用了本可用于其他信号的布线通道。
如果只用 LUT 搭 32 位加法器,进位信号会反复进出通用布线矩阵,一段路径的延迟可能达到专用进位链的几倍到十几倍。而且综合工具不一定能把这种 LUT 网表识别成加法结构,后续优化也无从谈起。专用进位逻辑把“进位传播”从通用逻辑中剥离出来,用硬化的 MUX 和 XOR 门来实现,速度和资源利用率都远好于 LUT 方案。这就是你在网表里看到大量 CARRY4 的最根本原因。
1.3 Carry4在Xilinx FPGA中的定位
在 Xilinx 7 系列以及后续 UltraScale 系列中,进位逻辑以原语 CARRY4 的形式存在于每个 Slice 内部。Slice 本来就是 FPGA 的基本逻辑单元,里面有 LUT、触发器、多路选择器和算术进位逻辑。CARRY4 和 LUT 配合使用:LUT 负责产生“与进位相关的选择信号”,CARRY4 负责把进位从低 bit 快速送到高 bit。它不是一个独立的 IP,也不是开发者主动实例化才会出现的黑盒子,而是综合工具在编译算术表达式时自动调用的硬件结构。
理解这一点很重要。很多新手把 FPGA 想象成“万能逻辑阵列”,认为任何逻辑都能靠 LUT 堆出来,实际上 FPGA 内部藏着不少类似 CARRY4 的专用硬件:DSP 块用来做乘加,BRAM 用来做存储,进位链用来做算术逻辑。写代码时如果能主动适配这些结构,资源利用率和时序表现都会有质的提升。Carry4 正是这种“架构友好型设计”的典型代表。
2. Carry4内部结构逐层拆解
2.1 端口信号说明
CARRY4 原语的端口并不复杂,但每个端口都有明确的职责。先把一张信号表放在这里,后面所有分析都围绕这张表展开。
| 信号 | 方向 | 位宽 | 作用 |
|---|---|---|---|
| CI | 输入 | 1 | 上一级 CARRY4 的进位输入,用于级联 |
| CYINIT | 输入 | 1 | 整条进位链的初始进位值,通常加 0 减 1 |
| DI | 输入 | 4 | 进位 MUX 的数据输入,多数场景接加数或被加数 |
| S | 输入 | 4 | 进位 MUX 的选择输入,通常由 LUT 输出产生 |
| O | 输出 | 4 | 求和结果或逻辑结果输出 |
| CO | 输出 | 4 | 进位输出,CO[3] 连接到下一个 CARRY4 的 CI |
注意 CARRY4 本身是纯组合逻辑,没有时钟、没有复位引脚。我们写计数器、累加器时看到的时序行为,其实是 CARRY4 之外的触发器来实现的。不要把 CARRY4 理解成一个“带寄存器的运算单元”,它只是完成了组合逻辑中最关键的那段进位传播。
2.2 一个比特位的核心:MUX与XOR的配合
CARRY4 内部可以看成 4 个 1 位进位逻辑的串联组合。对其中任意一位来说,核心电路由两部分组成:一个专用 MUX 负责产生进位,一个 XOR 门负责产生求和结果。如果用公式表示,即:
O[i] = S[i] XOR CI[i] CO[i] = S[i] ? CI[i] : DI[i]当我们要做 A + B 的加法时,LUT 提前把 A XOR B 的结果送到 S[i],把 A 送到 DI[i]。这时 CO[i] 的行为就等价于一个全加器的进位输出:如果 A 和 B 不同,S[i]=1,说明本级不产生进位,能不能继续向上走完全取决于低位的进位 CI[i],所以选择 CI[i] 作为输出;如果 A 和 B 相同,S[i]=0,这时候 DI[i] 就是 A,要么是 0 加 0 不产生进位,要么是 1 加 1 必然产生进位,所以直接选择 DI[i] 作为输出。
这实际上就是“进位传播/进位产生”思想在硬件上的直接落地。MUX 就是进位传播的通路,XOR 就是求和结果的生成器。CARRY4 把一个加法器最慢的那条“进位链”压缩成了一串 MUX 的级联,每级延迟很小,且路径固定在 Slice 内部,不经过通用布线。
2.3 级联方式与多比特加法
CARRY4 只处理 4 个 bit,比 4 位宽的加法器就要把多个 CARRY4 串起来。级联端口是 CO[3] 接到下一个 CARRY4 的 CI。这种连接不是普通逻辑之间的长走线,而是 FPGA 布线资源中专用的短路径,专门跑进位信号。所以综合工具在生成大位宽加法器时,会把多个 CARRY4 排成一条链,整条链上的延迟主要来自 CARRY4 内部的 MUX,而不是级联走线。
举个具体例子,32 位无符号加法器在 7 系列 FPGA 上通常由 8 个 CARRY4 组成:前 4 bit 用第一个 CARRY4,第 5 到第 8 bit 用第二个,依此类推。每 4 个 bit 还配套使用 4 个 LUT,用来生成 S 信号。这样 32 位加法器大约消耗 8 个 Slice 的资源,面积并不夸张,速度却远远好于纯 LUT 方案。这个 “4 bit 一组” 的粒度是 Xilinx 架构长期演化的结果,理解之后再看网表就清楚多了。
3. Carry4在RTL综合中的“隐形角色”
3.1 一行addition代码如何变身CARRY4
几乎所有开发者都不会手写 CARRY4,因为在 Verilog 里写加法只要一行。比如下面这段代码:
module adder32 ( input wire [31:0] a, input wire [31:0] b, input wire cin, output wire [31:0] sum, output wire cout ); assign {cout, sum} = a + b + cin; endmodule综合之后,Vivado 会把它映射成 4 个 bit 一组的结构。每组的 LUT 计算 S = a[i] XOR b[i],CARRY4 通过 DI 输入拿到 a[i],再把 S 作为选择信号,逐级把进位传到高 bit。最后 sum 的每一位从 CARRY4 的 O 端口输出,cout 从最后一个 CARRY4 的 CO[3] 输出。你在 Vivado 的 Elaborated Design 或 Synthesized Design 里打开 Schematic,搜索 CARRY4 就能看到这条清晰的进位链。
这个映射过程不需要任何额外的代码,综合工具对简单的+运算符识别得很准。前提是别在 RTL 里绕太多弯,比如用 if-else 拼出一个“看似加法但实际是复杂查找表”的逻辑。越直白的算术表达式,越容易被工具映射成 LUT + CARRY4 的最佳组合。
3.2 除了加法,哪些逻辑也在消耗CARRY4
加法是最典型的场景,但 CARRY4 的用途远不止加法。减法本质上就是“加上补码”,所以减法器的进位链和加法器几乎一样,只是 CYINIT 要置 1,LUT 生成的 S 信号变成 A XOR (~B)。比较器更直接:如果综合工具把a < b转换成a - b,判断结果符号位或者借位标志,那么底层的减法器就会使用 CARRY4。计数器、累加器、BCD 码计数器、CRC 计算里的部分逻辑也会用到进位链。
更巧妙的是,CARRY4 本质上就是 4 个级联的 2 选 1 MUX。只要你能把数据流表达成“由低位条件决定高位结果”的形式,就能用 CARRY4 加速。比如优先编码器、前导零检测、宽位或逻辑,甚至某些多路选择器,都可能被综合工具映射到进位链上。所以看资源报告时不要只盯着加法器,很多你没想到的逻辑都在偷偷消耗 CARRY4。
3.3 手动实例化CARRY4的正确姿势
虽然大部分情况下不需要手动实例化 CARRY4,但在自定义高性能算术路径、或者查看原语库时,还是要认识一下它的例化模板。Vivado 提供的模板大致如下:
CARRY4 #( .CARRY_WEIGHT(1) ) c4_inst ( .CO (co), // 4-bit carry out .O (o), // 4-bit sum output .CI (ci), // 1-bit carry cascade input .CYINIT (cyinit), // 1-bit carry initialization .DI (di), // 4-bit carry-MUX data input .S (s) // 4-bit carry-MUX select input );手动例化时,LUT 的映射关系必须自己控制:S 信号来自 LUT,DI 信号来自某个输入,O 是 XOR 结果。这么写很灵活,但也很容易出错。我的建议是,除非你已经很明白工具自动生成的结构,否则优先用a + b这种 RTL 写法,让综合工具去选择 CARRY4 并优化位置。手动例化应该用在“标准 RTL 无法表达你的自定义进位关系”的场景,而不是为了炫技。
4. 时序分析与优化:把Carry4链从敌人变成朋友
4.1 进位链延迟从哪里来
加法器最坏路径一定在进位传播上。以 32 位加法器为例,信号从输入引脚进入,先经过一个 LUT 生成 S,然后进入 CARRY4,在一长串 MUX 中逐级跳跃,最后再经过一个 XOR 门生成最高位的求和结果。路径逻辑级数大约等于“1 个 LUT + 32 个进位 MUX + 1 个 XOR”。因为 CARRY4 每 4 bit 一段,30 多级 MUX 被分成 8 个 CARRY4 级联,实际延迟大约等同于 8 段进位链的积累。
在时序报告里,这类路径往往会显示很多个 CARRY4 单元。有人看到几十个 CARRY4 就慌了,以为是工具生成了复杂的逻辑。其实这正是工具高效利用硬件的表现:CARRY4 的每级 MUX 延迟极低,甚至比 LUT 内部的路径还短。只要约束合理、布局不拥挤,一条几十位加法器进位链的关键路径通常可以满足中等频率的时序要求。真到不满足时,需要优化的不是删掉 CARRY4,而是想办法缩短链长或切断链路。
4.2 三种有效的进位链优化手段
第一种是流水线切割。把一个 64 位加法器拆成 4 个 16 位加法级,每级之间插入触发器缓存部分和与进位。这样原本一条 64 位长链变成 4 条 16 位短链,频率可以明显提升。代价是加法的单次延迟变成 4 拍,但如果是连续数据流,吞吐量不受影响,流水线带来的收益非常可观。需要注意处理中间进位的时序,别把进位信号送到高级逻辑时才想起对齐。
第二种是把算术逻辑搬进 DSP 块。Xilinx 的 DSP48E1/DSP48E2 内部带有快速加法器和乘法器,进位路径经过专门优化,比通用逻辑里的 CARRY4 更快。对乘法、乘累加、FIR 滤波这类重负载计算,使用(* use_dsp = "yes" *)综合属性或直接例化 DSP IP,能把大量的 CARRY4 释放出来,时序压力也小很多。当然,DSP 块数量有限,不能把普通计数器也塞进去。
第三种是减小进位链长度。这听起来像废话,但在架构层面很有效:能不计算高位的场合就不计算,能缩小位宽就缩小位宽。比如做图像处理时,像素位宽从 12 bit 缩到 10 bit,理论上进位链缩短 2 级,面积和功耗都有改善。还有些场景可以把连续累加改成并行树形求和,让进位链长度从线性增长变成对数增长,代价是需要多级 pipeline。
4.3 在Vivado里快速定位Carry4瓶颈
遇到时序违规,我最常用的命令是report_timing_summary和report_path。在 Tcl Console 里输入:
report_timing -from [get_pins ...] -to [get_pins ...] -sort_by group然后看路径细节里的 Cell 列表,如果发现一排 CARRY4,并且它们占用了大部分 Delay,就说明瓶颈在进位链。此时不要急着调约束,先确认这个 CARRY4 链对应的是 RTL 里的哪个运算。Vivado 的 Schematic 里选中 CARRY4,可以高亮源文件中的对应表达式。
另一个小技巧是通过report_utilization检查 CARRY4 用量。如果本来应该用 CARRY4 的逻辑没有用,多半是代码风格问题,比如用乘法器 IP 替代了加法、或者手工展开了位运算。检查资源报告并对照 RTL 位置,往往能很快定位问题。
5. 常见问题与排查技巧实录
5.1 综合结果里居然没有CARRY4?
新手经常碰到的怪事是:明明写了加法,可综合报告里的 CARRY4 数量却是 0。先别急着怀疑工具,先看看操作数是不是常量。如果两个加数有一个是固定常数,工具很可能直接用 LUT 优化掉进位链,只保留简化的逻辑。另一个常见原因是把加法写进了if分支,有些分支条件下结果被赋成常量,工具优化后并不会生成完整进位链。
还有一种情况是使用了较复杂的算术表达式,比如(a + b) >> 1。综合工具会先优化成与门和 MUX,进位链可能被吸收掉了。这本身不一定是坏事,但如果你明确需要进位链来保证时序,可以尝试给信号加综合属性、或者调整代码让加法单独计算。检查综合日志里的Number of CARRY4一项,就能确认预期和实际是否一致。
5.2 进位链太长导致时序不过怎么办
如果一个设计中出现了很多条高位宽进位链,布局布线后很容易因为局部拥塞导致时序不过。常见表现是:单个路径的 CARRY4 数量并不多,但很多 CARRY4 挤在同一个时钟区域内,布线资源被占满。这时候再强的约束也难救。
我的处理顺序是先看资源分布,如果某个 Slice 附近塞了大量算术逻辑,考虑用SET_PROPERTY或者pblock做区域约束,把它们分散开。然后是评估是否可以切片成流水线,或者直接把整块计算搬进 DSP 块。最后才考虑调整综合策略,比如把-retiming打开。不要上来就疯狂加延时约束,表面是过了时序,实际是把问题推给了后端。
5.3 复位、时钟使能与累加器的坑
CARRY4 虽然是组合逻辑,但累加器、计数器这类时序逻辑大多会用 CARRY4 同时配合触发器。很多人以为只要复位了寄存器结果就归零,但实际上复位信号如果同时影响了产生 S 和 DI 的 LUT 路径,可能会导致额外的组合延迟。比如用if (rst) acc <= 'd0; else acc <= acc + din;这种写法,工具通常会把复位逻辑优化到触发器本身,不会增加进位链长度,但如果代码写了不必要的全局复位网络,就可能让逻辑层级变多。
累加器的时钟使能也要注意。有些代码里用en直接门控加法结果,在无时钟门控的 FPGA 设计里,这会被转成寄存器的使能引脚,通常不会破坏进位链。但如果你习惯把en和加法结果做逻辑与,就可能影响 S 信号的生成,让进位链变得复杂。尽量把使能交给触发器 CE 引脚,而不是塞进组合逻辑,这样 CARRY4 才是最优形态。
6. 从Carry4看FPGA内部结构设计思路
6.1 专用逻辑和通用逻辑的边界
Carry4 的价值不在它本身有多复杂,而在于它揭示了 FPGA 设计的一条核心原则:高频重复且延迟敏感的功能,应该用专用硬件实现。LUT 是通用逻辑,能覆盖任意函数,但速度和效率都不如专用结构。进位链就是“加法器太常用了,干脆用硬 MUX 做一条专用通道”的典型思路。类似的还有 DSP 块里的乘法器、块 RAM 里的地址译码器,都是把通用逻辑中“慢且浪费”的部分固化下来。理解这个边界,才能在写 RTL 时主动选择与硬件匹配的表达方式。
6.2 其他FPGA厂商的类似实现
Carry4 是 Xilinx 的原语名称,但专用进位链并不是 Xilinx 独家发明。Intel/Altera 的 FPGA 在逻辑单元内部也有进位级联结构,把加法器的进位输出直接连到相邻逻辑单元,避免通用布线;Lattice、Microsemi 等厂商同样有类似的快速进位路径。只是原语名称、每级处理的 bit 数、内部 MUX 细节有所不同。跨厂商移植代码时,如果使用了大量算术逻辑,资源评估不能只比较 LUT 数量,还要关注进位链结构是否兼容。
6.3 CARRY4之后:CARRY8与未来
到了 UltraScale+ 系列,Xilinx 进一步引入了 CARRY8 原语,一次处理 8 bit 进位,比 CARRY4 少了一半的级联次数。对于更高位宽的加法器,CARRY8 能减少进位链中间经过的“元件”数量,对时序有一定帮助。但设计思路完全一致:LUT 生成 S,进位 MUX 传播,XOR 输出结果。所以不管当前项目用的是 7 系列还是 UltraScale,把 CARRY4 的原理吃透,后面看 CARRY8 甚至未来新的进位结构都会很轻松。
做大型项目时我总习惯先看一眼综合后的资源报告,确认 CARRY4 数量是否符合预期。有一次优化一个视频缩放模块,发现进位链成了瓶颈,我把一个 48 位累加器改成分段流水累加,整体帧率立刻提上去了。实际上 CARRY4 既不高深也不神秘,它只是 FPGA 在“如何快一点完成算术运算”这件事上交出的答案。你越早看懂它,写出的代码就越贴近硬件,时序收敛也就越省心。