8位可控加减法器与CLA74182:补码减法、进位链优化及16位先行进位

发布时间:2026/10/29 12:18:42
8位可控加减法器与CLA74182:补码减法、进位链优化及16位先行进位

简介:这份《运算器实验》课程实验报告面向计算机组成原理学习者,聚焦8位可控加减法器、4位快速加法器与16位快速加法器的设计与实现,帮助读者理解二进制运算、进位机制及硬件电路调试流程。资源包共1个docx文件,压缩包约1.26MB,内容以实验报告文档形式组织,便于按实验项目查阅。报告涵盖设计要求、方案设计、实验步骤、故障与调试、测试与分析等环节,并给出先行进位、补码减法、溢出检测等关键知识点的实现思路,可作为课程实验复盘与电路搭建参考。已有2564人学习下载,适合需要巩固运算器原理、对照实验过程或查漏补缺的学生使用。

1. 加法器的天花板不在加法,而在等进位

把八个全加器首尾串起来,运算器实验里的 8 位加法器当场就能算对任何一组输入,可它慢得毫无道理——慢的地方根本不在“加”,而在“等”。最高位的和要等第 7 位的进位爬过前面七级门才能定下来,这条进位链的长度才是真正的天花板。同样的约束在别处也一样成立:网络设备里转发芯片的算术通路、处理器数据通路的时钟周期,卡的都是这一条链。所以教科书非要在 8 位可控加减法器之后塞进一片 CLA74182,不是为了多做一个器件,而是要把“排队等进位”改成“同时算进位”。搞懂这一步,运算器实验才算真的做完。

2. 补码把减法变成加法:Sub 一根线怎么同时控制取反和进位

2.1 全加器只认加法,减法得靠补码“骗”过去

一个一位全加器 FA 的输入是 Xi、Yi、Ci,输出是 Si 和向高位的 Ci+1,布尔式写出来就两行:Si = Xi ⊕ Yi ⊕ Ci,Ci+1 = Xi·Yi + (Xi ⊕ Yi)·Ci。它天生只会做加法,没有任何“减法模式”。8 位加减法器之所以能减,是因为在 8 位字长下,减法 X−Y 恒等于 X + (−Y),而 −Y 的补码表示就是按位取反再加 1,也就是 X + (~Y) + 1,整个运算被折叠回模 2⁸ 的加法。

这个等价关系带来一个非常划算的电路结构:不需要单独的减法器,不需要先算补码再送加法器,取反和加 1 都可以在加法器内部顺手完成。取反交给异或门,加 1 交给最低位的进位输入。于是整个 8 位可控加减法器只需要 8 个 FA、8 个异或门,外加一根叫 Sub 的控制线。

2.2 异或门是可控反相器:Sub 一分为九

异或门有个非常好用的性质:a ⊕ 0 = a,a ⊕ 1 = ~a。把 Sub 接到每个异或门的一个输入端,异或门的另一个输入端接 Yi,输出就是受控的操作数。Sub=0 时 Yi 原样通过,Sub=1 时按位取反。同一根 Sub 还要接到最低位 FA 的进位输入端,充当补码里的那个“+1”。一根信号扇出到九个地方,加法与减法就被同一套硬件覆盖了。

这里有个容易被忽略但很关键的设计动机:如果把“取反”和“加 1”拆成两步做,就得先得到一个完整的 ~Y,再送进第二级加法器加 1,多出一整级进位延迟;把 +1 塞进 C0 之后,加 1 与加法在同一个进位链里一次完成,代价为零。这不是省器件,是省时间。

def add_sub_8bit(x: int, y: int, sub: int): """按门级串行结构模拟 8 位可控加减法器。 x, y: 8 位无符号整数;sub: 0 加法 / 1 减法 返回 (S, Cout, OF) """ cin = sub # Sub 同时充当最低位进位:补码里的 +1 carry = [] # carry[i] = 第 i 位 FA 向高位的进位 s = 0 for i in range(8): xi = (x >> i) & 1 yi = (y >> i) & 1 yi_eff = yi ^ sub # 异或门:sub=1 时按位取反 total = xi + yi_eff + cin s |= (total & 1) << i # Si = Xi ⊕ Yi' ⊕ Ci cin = (total >> 1) & 1 carry.append(cin) cout = carry[7] # 最高位进位:无符号视角的溢出 of = carry[7] ^ carry[6] # 符号位进位 ⊕ 次高位进位:有符号溢出 return s, cout, of print(add_sub_8bit(8, 4, 0)) # (12, 0, 0) Sub=0,加法 print(add_sub_8bit(8, 4, 1)) # (4, 0, 0) Sub=1,减法 print(add_sub_8bit(0x80, 0x01, 1)) # (127, 1, 1) -128-1 越界,OF=1

这段代码把每一位的进位单独存进carry数组,就是为了能取到carry[6]和carry[7]的差别。cin = sub这一行是整个设计的精髓:Sub 既控制取反,又是最低位的进位输入,一行代码对应电路里那根分叉的连线。最后一行0x80 - 0x01是有符号溢出用例,结果是 127 而不是 −129,OF=1正确报出越界,Cout=1只是说明无符号视角下也有进位,两个标志说的不是一回事。

2.3 Cout 和 OF 是两回事:一个看无符号,一个看符号位

实验报告里最容易写错的一句话,是把溢出直接等同于最高位进位。8 位无符号加法的溢出看 Cout,而有符号加法的溢出要看“进入符号位的进位”和“从符号位出去的进位”是否一致,也就是 OF = C7 ⊕ C6。这两个标志在多数情况下并不相同。

举两组输入:0x7F + 0x01 = 0x80,无符号 127+1=128 没有超出 0~255,Cout=0;但有符号视角下 127+1 已经越过 +127 的上界变成 −128,OF=1。反过来 0xFF + 0x01 = 0x00,无符号 255+1 溢出,Cout=1;有符号视角下 −1+1=0 完全正常,OF=0。同一个加法器,两个标志指向两个不同的世界,接错一个就会出现“加法用例全过、减法用例全错”的玄学现场。

还有一处语义细节:在 Sub=1 的减法模式下,Cout 表示的是“无借位”。X−Y 时若 X≥Y,Cout=1;若 X<Y,Cout=0 表示向更高位借了 1。测试时如果只测 8−4 不测 4−8,Cout=0 的那条路径永远没被走到。

2.4 延迟账:8 位串行的关键路径有多长

把每级全加器拆成门看,进位从 Ci 到 Ci+1 走的是一条“与门 + 或门”的路径,按两级门延迟算;8 位串起来,最高位进位要等 16 级门延迟。更糟的是最高位的和 S7 = X7 ⊕ Y7 ⊕ C7,还得在这个进位之后再串一级异或。于是 8 位串行加法器的关键路径大致是“16 级门 + 1 级异或”。

这个数字是理解后面所有结构的基准。想让加法变快,只有两条路:让每一级的进位来得更早,或者让进位不要一级一级地传。前者优化门电路,后者才是结构层面的革命,也就是先行进位要做的事。

3. 先行进位:CLA74182 凭什么把进位链压平

3.1 Gi 和 Pi:把“产生进位”和“传播进位”拆开

重看进位式 Ci+1 = Xi·Yi + (Xi ⊕ Yi)·Ci,它其实在说两件事。Xi·Yi 这一项与 Ci 无关,只要两个输入位都是 1,本位一定向高位送出一个进位,这叫进位生成,记作 Gi = Xi·Yi。另一项 (Xi ⊕ Yi)·Ci 表示本位不生成进位,但只要两个输入位有一个为 1,低位的进位就能原样透传到高位,这叫进位传递,记作 Pi = Xi ⊕ Yi。于是 Ci+1 = Gi + Pi·Ci。

这个改写看起来只是换了个记号,价值在于 Gi 完全不等任何人:4 位输入的 G0~G3 可以在输入稳定的那一刻同时算出来,不用等 C0。被等待的只剩下 Pi·Ci 这一项,而 Pi 也是同时可得的。等的东西从“前面所有位的进位”缩成了“一个已知量乘以一个已知量”。

这里有个必须点破的坑:实验报告 2.2.3 里 Pi 被写成 Xi + Yi(或),而前面又写 Pi = Xi ⊕ Yi(异或),两处不一致。做进位链时 Pi 取或确实也能让进位传对,因为 Xi=Yi=1 时 Gi 已经把进位钉成 1 了;但算和数时 Si = Pi ⊕ Ci 这个式子只有 Pi 取异或才成立,因为异或的输出恰好就是本位的半加和。搭电路时按或门接 Pi、又按 Si = Pi ⊕ Ci 求和,输入 1010 + 1010 这种全 1 位就会出错,而且错得很有规律——只在 Xi=Yi=1 的位上翻车。

3.2 展开 C1~C4:为什么进位可以不等前面算完

把递推式一层层代进去,每个进位都被展开成只含 G、P、C0 的表达式:

C1 = G0 + P0·C0 C2 = G1 + P1·G0 + P1·P0·C0 C3 = G2 + P2·G1 + P2·P1·G0 + P2·P1·P0·C0 C4 = G3 + P3·G2 + P3·P2·G1 + P3·P2·P1·G0 + P3·P2·P1·P0·C0

式子里再没有 C1、C2、C3 这些中间量,四个进位彼此独立,可以同时求值。硬件上每一级都是“一层与门 + 一层或门”,所以 4 位先行进位的延迟是固定的两级门,与位宽无关。串行结构里“等第 1 位进位 → 算第 2 位进位 → 算第 3 位进位”的链条,被换成了“所有项一起算,最后或起来”。

代价立刻体现在扇入上:C4 的最后一项要 5 个输入,C8 要 9 个,C16 要 17 个。静态 CMOS 门做不出 17 输入与门,就算做出来延迟也会被扇入拖垮。这就是先行进位必须分组的原因,也是 74182 只做 4 位的原因。

def cla4(x, y, c0): """4 位先行进位加法器:组内并行,输出和、C4、成组 G*/P*""" g = [(x >> i & 1) & (y >> i & 1) for i in range(4)] p = [(x >> i & 1) ^ (y >> i & 1) for i in range(4)] # 注意是异或 c1 = g[0] | (p[0] & c0) c2 = g[1] | (p[1] & g[0]) | (p[1] & p[0] & c0) c3 = g[2] | (p[2] & g[1]) | (p[2] & p[1] & g[0]) | (p[2] & p[1] & p[0] & c0) c4 = (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] & c0)) s = 0 for i, ci in enumerate([c0, c1, c2, c3]): s |= (p[i] ^ ci) << i # Si = Pi ⊕ Ci-1 gstar = g[3] | (p[3] & g[2]) | (p[3] & p[2] & g[1]) | (p[3] & p[2] & p[1] & g[0]) pstar = p[3] & p[2] & p[1] & p[0] return s, c4, gstar, pstar print(cla4(0b1000, 0b0100, 0)) # (12, 0, 0, 0) 对应报告 2.2.5 的测试

四个进位c1~c3全部由g、p、c0直接算出,没有一环引用前一环,这正是“并行”在代码里的样子。gstar和pstar是这一组 4 位整体对外的两个接口信号,它们的表达式和c4高度相似,区别在于c4多了一项p3·p2·p1·p0·c0——c4 = gstar + pstar·c0,这个恒等式是后面理解 C0 那段排查的关键。最后一行的测试对应报告里 X=1000、Y=0100、Cin=0 得到 S=1100 的用例,pstar=0是因为最低位 p0=0,整组传不上去。

3.3 成组 G*/P*:74182 上端接口是给下一层准备的

74182 除了吐出 C1、C2、C3、C4 之外,还要吐出 G* 和 P* 这两个成组信号。它们的物理含义很直白:G*=1 表示这一组 4 位自己就能向更高一组送出一个进位,跟 C0 没关系;P*=1 表示这一组 4 位完全导通,低位来的进位会原封不动地穿到高一组去。这两个信号把 4 位的进位信息压缩成两个比特,往上一送,上一层就可以当作“一个超宽的位”来处理。

没有这两个接口,16 位加法器就只能把第二片 74182 的输入接成 4 个成组进位,那样又退化成串行。正因为有了 G*/P*,进位才能分层:组内先并行一次,组间再并行一次。

3.4 C0 是函数自变量:74182 不接 C0 会怎样

实验报告 2.2.4 里那句“74182 是 C0 的函数,一定要连上 C0”值得单独拎出来讲。从恒等式 c4 = gstar + pstar·c0 可以看出,C4 不是 G* 的函数,而是 G*、P*、C0 三者的函数。如果 C0 悬空被当成 0,那么 C4 就退化为 G*,凡是需要pstar·c0这一项的场合全错——也就是当这一组内部传得通、但自身不生成进位、而低位又确实送来了进位时,C4 会被算成 0。

这类故障的症状非常典型:Cin=0 的测试用例全过,Cin=1 的用例全挂,而报告里 2.2.5 和 2.3.5 的测试向量恰好都是 Cin=0。测试向量选得“太友好”,是这类问题被漏掉的直接原因。

延迟对比也在这里收个尾:8 位串行关键路径约 16 级门,4 位 CLA 的关键路径是“进位 2 级门 + 和数 1 级异或”,约 3 级门。用几十个额外的门换掉十几级延迟,这笔账在运算器这种高频路径上永远是划算的。

4. 16 位扩展:两级先行进位怎么把 16 位压到两级

4.1 组内并行、组间也并行:第二片 74182 干什么

16 位加法器不是把 4 个 4 位加法器串起来,那样只是把串行链从 16 级缩短到 4 段。真正的做法是:4 个 4 位 CLA 各自算自己那 4 位的 G*、P*,把这 8 个信号全送进第二片 74182,由它并行算出 C4、C8、C12、C16。层与层之间的表达式和 3.2 节完全同构:

C4 = G*0 + P*0·C0 C8 = G*1 + P*1·G*0 + P*1·P*0·C0 C12 = G*2 + P*2·G*1 + P*2·P*1·G*0 + P*2·P*1·P*0·C0 C16 = 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·C0

看清一件事:第二片 74182 只负责“每组的入口进位”,即 C4、C8、C12;组内的 C1、C2、C3 仍然由第一层 74182 用本组的 C0 算出来,不能从上一层拿。报告原文说“每 4 位的进位 C1、C2、C3、C4 是由 CLA74182 的上端接口连到 4 位快速加法器的进位输入端”,指的就是这个连接关系——上端接口给出的是本组入口 C4,组内三个进位由组内那一片负责。

这样整个 16 位加法的进位延迟是:第一层 74182 算 G*/P* 用 1 级门,第二层算 C4~C16 用 2 级门,组内再用 2 级门算 C1~C3,最后一级异或出和数。总计约 5~6 级门,而 16 位串行是 32 级门。位宽翻了四倍,延迟只涨了一倍多,这就是分层的收益。

4.2 为什么是 4 位一组:扇入、封装与延迟的三方妥协

分组宽度的选择不是拍脑袋。第一约束是扇入:n 位一组的最高进位表达式有 n+1 项,最后一项是 n+1 个输入相与,4 位对应 5 输入门,这在 CMOS 里还算能做;8 位就是 9 输入,延迟和面积都开始难看;16 位一组直接不现实。第二约束是封装:74182 本身是 4 位 CLA,用它做积木,分组宽度天然被钉在 4。第三约束是延迟收益递减:从串行到两级先行,延迟砍掉了六七成;从两级到三级,只能再砍掉一点,而面积和布线复杂度继续上涨。

所以 4 位一组是三个约束的交点,不是理论最优。真要往 64 位、128 位走,工程上会换另一条路——并行前缀结构,把 Gi、Pi 按树形归并,本质上还是这套生成/传递思想,只是把“分组”换成了“递归二分”,扇入始终控制在 2~4 之间。

4.3 用基准模型对拍:16 位加法器怎么验证

手工挑几个测试向量能证明电路“能跑”,但证明不了“都对”。16 位输入空间有 2³² 种组合,穷举不现实,随机对拍是最实际的办法:写一个逐位串行的参考实现当基准,再拿 CLA 结构的结果去比。

import random def cla16(x, y, c0): """16 位两级先行进位加法器:4 个 4 位 CLA + 一片组间 CLA""" s = 0 groups = [] # 每组的 (G*, P*) for k in range(4): xk = (x >> (4 * k)) & 0xF yk = (y >> (4 * k)) & 0xF sk, _, gk, pk = cla4(xk, yk, 0) # 先用 C0=0 拿到组内和与 G*/P* groups.append((gk, pk)) s |= sk << (4 * k) # 组间先行:只算每组的入口进位,组内进位由各组自己补 carry_in = [c0, 0, 0, 0] for k in range(1, 4): term = c0 for j in range(k): # 前面每一组的 P* 连乘 term &= groups[j][1] acc = 0 for j in range(k): # 前面某一组生成、其后各组全导通 t = groups[j][0] for m in range(j + 1, k): t &= groups[m][1] acc |= t carry_in[k] = acc | term # C4k = G* + P*·C0 的链式展开 # 用真实入口进位重算各组和数,修正 C0=0 时的偏差 s = 0 for k in range(4): xk = (x >> (4 * k)) & 0xF yk = (y >> (4 * k)) & 0xF sk, _, _, _ = cla4(xk, yk, carry_in[k]) s |= sk << (4 * k) cout = 1 if (x + y + c0) >> 16 else 0 return s & 0xFFFF, cout for _ in range(20000): x = random.randrange(1 << 16) y = random.randrange(1 << 16) for c0 in (0, 1): got, cout = cla16(x, y, c0) assert got == (x + y + c0) & 0xFFFF, (x, y, c0, got) assert cout == ((x + y + c0) >> 16), (x, y, c0, cout) print("20000 组随机对拍通过")

groups里存的是每一组的 G*、P*,组间那一段carry_in的计算就是 4.1 节那四条展开式的循环写法,注意每个term都要把 C0 参与进来——这就是“74182 是 C0 的函数”在代码里的形状。最后一轮“用真实入口进位重算各组和数”不能省:第一遍用 C0=0 算出的组内和数,在入口进位为 1 时是错的,必须补一次。漏掉这一步,对拍会在第一个 C4=1 的用例上直接崩。

4.4 报告测试向量的复现

报告的 16 位用例:X = 0000 1100 0000 1100,Y = 1000 0100 1000 0100,Cin = 0,得到 S = 1001 0000 1001 0000,Cout = 0,P* = 0,G* = 0。按 4 位一组拆开验证:组 0 是 1100+0100=10000,和 0000、向组 1 进位 1;组 1 是 0000+1000+1=1001,不产生进位;组 2 是 1100+0100=10000,和 0000、向组 3 进位;组 3 是 0000+1000+1=1001。拼起来正是 1001 0000 1001 0000,最高位没有进位,与报告一致。这组用例的价值在于它同时覆盖了“组内产生进位”和“进位跨组透传”两种情形,比单纯的全 0 输入有用得多。

5. 排查:进位电路最容易翻车的五个地方

5.1 五个典型现场:现象、原因、解决

现场一:Cin=0 的用例全过,Cin=1 的用例全错。现象是输入一组随机的带进位数据,和数整体偏低 1,P* 有时也跟着错。原因基本只有一个——74182 的 C0 端没接,或者接了但接成了常量 0,C4 = G* + P*·C0 里的P*·C0项直接丢掉了。解决:把外加的 Cin 同时引到本组 74182 的 C0 和最低位 FA 的进位输入上,用示波器或波形窗口确认这两处电平同步翻转。这条血泪经验是分组电路最常见的坑,因为它只在进位链跨组时暴露。

现场二:全 1 输入的用例出错,其余全对。现象是 1010 + 1010 这类操作数出来的和数某几位恒为反。原因是 Pi 用了或门(Xi+Yi)去接,而求和又按 Si = Pi ⊕ Ci 算。解决:求和一律用 Xi ⊕ Yi ⊕ Ci 单独算,或者干脆把 Pi 统一成异或,进位链那边它同样成立。别在一张图里混用两种 Pi 定义,混用之后波形上看不出错,只有特定输入才翻车。

现场三:有符号溢出判不出来。现象是 0x7F+0x01 被认定“没溢出”,因为 Cout=0。原因是用 Cout 顶替了 OF。解决:把第 7 位 FA 的进位输出和第 6 位 FA 的进位输出接一个异或门,输出才是 OF;Cout 单独留作无符号进位标志。两个标志各有各的用途,别合并。

现场四:某一位的和恒为 0 或恒为不定态。现象是波形上 S3 一直不动,改输入也没反应。原因通常是隧道标签重名——两个本该独立的网络起了同一个名字,被仿真器合并成一个节点。解决:给每一根线做唯一命名,比如c4_g0、c4_g1,不要图省事全叫C4。这种错误在原理图软件里没有任何报错,只能靠波形逐个网络点过去。

现场五:减法只测了大数减小数。现象是 8−4 通过,4−8 结果看着“对但标志错”。原因是没意识到减法模式下 Cout 表示无借位,4−8 时 Cout=0 才是正确结果,测的人却拿进位为 1 当通过条件。解决:固定一组边界用例,包含 0−1、1−0、−128−1、127+1、0xFF+1,每条都同时检查 S、Cout、OF 三个输出。

5.2 定位故障位:用“单 1 行走”测试向量

随机对拍能告诉你“错了”,但不能告诉你“错在哪一位”。定位阶段要换成构造性向量:让一个 1 从最低位开始逐位左移,其余全 0,每次只让进位链在一个位置上被激活。比如 0x01、0x02、0x04……配上 0xFF 这种“全导通”操作数,可以精确测出进位链在第几级断掉。

最狠的一条向量是全 1 加 1:0xFF + 0x01。它让最低位产生进位,并且每一级的 Pi 都为 1,进位必须一路穿到最高位。这条链只要有一级接错,结果立刻不对,而且波形上能直接看到进位在第几位停住。16 位版本同理,只是要注意跨组的那几个入口进位 C4、C8、C12 是否在正确时刻翻转。

5.3 波形排查顺序:从 C0 往上,不要从 S 往下

S 错可能是上游任何一处错造成的,从和数反推效率极低。正确的顺序是:先确认 C0 到位,再看每组的 G*、P* 是否符合预期(拿手算的表达式对一遍),然后看组间 74182 输出的 C4、C8、C12,最后才看组内 C1~C3 和和数 S。这条路径上每一层都是“输入已知、表达式已知”的,对不上就能立刻锁定是哪一片器件、哪一根线的问题。

顺带提一句器件型号:报告里出现过 CLA74128 和 CLA74182 两种写法,实际做电路时以封装库里真实存在的型号为准,抄错一位在元件库里根本搜不到,容易让人以为是软件问题。

6. 取舍:教科书停在 16 位,真实设计在别处拐弯

把三种结构放在一起算账,取舍就很清楚了。8 位串行加法器每级约 5 个门、关键路径 16 级;4 位先行进位加法器的进位只要 2 级门,代价是 C4 那一项的门数按组合增长;16 位两级先行,延迟约 5~6 级,面积已经比串行多出好几倍。

结构关键路径(门级)相对面积适合位宽
8 位串行进位约 16 级18 位以内
4 位先行进位约 3 级约 3~4 倍4 位一组
16 位两级先行约 5~6 级约 10 倍以上16 位
并行前缀树约 log₂n 级随位宽线性32/64 位

表格最后一行的存在本身就是答案:先行进位没有“更宽就更快”的线性红利,扇入一爆就得换家族。32 位以上普遍改用并行前缀结构,但那套东西的数学内核仍然是这一章反复出现的 Gi 和 Pi——把“这一位会不会生成进位、会不会传递进位”这两个比特,沿着树一层层归并上去。理解了 74182 的 G*、P*,再去看前缀加法器,会发现只是把“分组”换成了“二分”,思维模型没变。

验证方法也值得固化下来。这个实验最有价值的产出不是那张电路图,而是一套可复用的检查流程:先用逐位串行模型当基准,写随机对拍跑两万组;再补边界向量,覆盖全 1 进位传播、最高位溢出、减法借位;最后在波形上按 C0 → G*/P* → C4/C8/C12 → S 的顺序逐层确认。这套流程搬到任何时序或组合逻辑的调试上都成立。

我自己的习惯是:画进位链之前,先在纸上把 C0、G*、P* 这四根关键线单独列一遍,确认每一根都有明确的驱动源和明确的去向,再动手连线。吃过一次 C0 悬空的亏之后,这个动作就再没省过——电路图上看不出问题,波形上却整整齐齐地错一片,那种感觉实在难受。希望帮到你。

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