简介:这份面向计算机组成原理课程学习者的运算器实验报告,围绕8位可控加减法器、4位快速加法器与16位快速加法器的设计与实现展开,适合正在完成硬件实验、复习进位机制或准备课程设计的学生对照使用。压缩包内共1个docx文件,大小约1.26MB,文档以实验报告形式组织,包含设计要求、方案设计、实验步骤、故障与调试、测试与分析等模块,并配有电路图与结果讨论。资源重点讲清串行进位、补码减法、溢出检测,以及利用CLA74182完成先行进位和16位组间扩展的思路,能帮助读者理解从4位到16位加法器的搭建与验证过程。目前已有2564人学习下载,可作为实验预习、报告撰写和排错复盘的参考材料。
1. 补码减法不是“另做一套”:8位可控加减法器为什么只改一个Sub
把运算器实验里的8位可控加减法器拆开看,减法并没有独立的减法器。Sub=1时,电路只是把Y的每一位取反,再让最低位进位Cin=1,把X-Y改写成X+~Y+1,也就是补码加法。反直觉的地方在于:控制信号不是切换两条数据通路,而是改写加数本身。计算机组成原理里运算器的核心机制就落在这里,用同一套全加器阵列同时承担加法和减法,代价只是几个异或门和一根进位控制线。搞懂这条改写规则,后面4位快速加法器、16位快速加法器的G/P信号才有落脚点;否则74182只是黑匣子,连线一错就只能靠猜。真正值得深挖的是进位链怎样从串行变成先行,而不是把电路图抄一遍。
2. 串行进位链:8位可控加减法器怎样把Sub翻译成补码控制
2.1 全加器的进位依赖:串行链的延迟到底花在哪
一位全加器的输入是Xi、Yi和低位来的进位Ci,输出和Si以及向高位的进位Ci+1。逻辑关系可以写成:
Si = Xi ⊕ Yi ⊕ Ci Ci+1 = Xi·Yi + (Xi ⊕ Yi)·Ci把8个这样的全加器按位串联,就得到8位串行进位加法器。它的结构简单,但延迟是逐位累积的:C1要等C0,C2要等C1,一直到C8要等C7。若每个全加器产生进位的门延迟记为一个单位,8位加法的关键路径上大致要穿过8级进位逻辑。做实验时看波形,最直观的现象是高位和的稳定时间明显晚于低位和。
串行链的代价不在门数,而在“后一位必须等前一位”。8位还能忍,16位、32位继续串下去,主频会被进位链拖住。快速加法器的所有设计动机,都是想办法把这条依赖链缩短,而不是把全加器换得更快。
2.2 Sub作为可编程反相器:异或门与最低位进位
8位可控加减法器的妙处在于,它把Sub同时送到两个地方:Y的每一位先与Sub异或,最低位全加器的进位输入直接接Sub。写成位级表达式:
Yi' = Yi ⊕ Sub C0 = Sub S = X + Y' + C0 (按8位取模)当Sub=0时,Yi'=Yi,C0=0,执行X+Y;当Sub=1时,Yi'=~Yi,C0=1,执行X+~Y+1,即X-Y的补码形式。异或门在这里充当可编程反相器:一根控制线同时控制8个数据位,不需要额外的减法器,也不需要多路选择器去切换两条通路。
这里有一个容易忽略的细节:减法结果的无符号进位Cout含义与加法不同。X-Y时,若X≥Y,最高位进位C8通常为1,表示没有向更高位借位;若X<Y,C8为0。做无符号比较时可以用Cout判断大小,但它不是有符号溢出标志。两者不能混用。
2.3 溢出OF的判据:为什么看C7与C6的异或
有符号溢出发生在“正加正得负”或“负加负得正”的时候。对8位补码,最高数据位是第7位,符号位也是第7位;进入符号位的进位是C7,符号位向外的进位是C8。溢出标志OF的判据是:
OF = C8 ⊕ C7直觉解释是:符号位本身也是一个二进制位,它参与加法时会收到低位来的进位C7,也会向更高位产生进位C8。如果这两个进位不一致,说明符号位的值被“挤”错了,结果符号与真实数学结果相反。比如0x7F+0x01,低7位全1加1会一路进位到符号位,C7=1,而符号位0+0+1=1,不再向外出进位,C8=0,OF=1,结果0x80表示-128,确实溢出。
注意不要把Cout直接当OF。Cout是C8,只说明最高位有没有向外界进位;OF是C8与C7的异或,关心的是符号位有没有被进位破坏。实验报告里写“最高数据位的进位与符号位的进位位是否一致”,说的就是这件事。
2.4 用行为模型验证8位加减器
下面这段Python不是综合电路,而是按位模拟全加器行为,用来对拍手算结果和波形。它把每一位的进位都记录下来,方便检查C7、C8和OF。
def add8_controlled(x, y, sub): mask = 0xFF # Sub=1时,Y的每一位取反;最低位进位也置1 y_eff = (y ^ (mask if sub else 0)) & mask cin = 1 if sub else 0 s = 0 c = cin carries = [] for i in range(8): xi = (x >> i) & 1 yi = (y_eff >> i) & 1 si = xi ^ yi ^ c cout = (xi & yi) | ((xi ^ yi) & c) carries.append(cout) # carries[i] 是 C(i+1) s |= si << i c = cout c8 = carries[7] # 最高位向外的进位 c7 = carries[6] # 进入符号位的进位 of = c8 ^ c7 return s & mask, c8, of, carries # 正+正得负:0x7F + 0x01 = 0x80,OF应为1 print(add8_controlled(0x7F, 0x01, 0)) # 减法:0x80 - 0x01 = 0x7F,C8与OF可对照波形检查 print(add8_controlled(0x80, 0x01, 1))参数x和y取0~255,sub为0或1。返回值中第一个是8位和,第二个是C8,第三个是OF,第四个是每一位向高位的进位列表。carries[6]对应C7,carries[7]对应C8,这个下标关系在排查溢出时最常写错。用0x7F+0x01和0x80-0x01两组向量,可以同时覆盖C7、C8、OF和Sub控制路径。
3. 先行进位:4位快速加法器凭什么把进位延迟压成常数级
3.1 G与P的物理含义:把进位链压成两个信号
串行进位之所以慢,是因为每一位都在等上一位的进位。先行进位的思路是:不直接等进位,而是先算出每一位“能不能自己产生进位”和“能不能把低位进位传过去”。定义:
生成函数 Gi = Xi · Yi 传递函数 Pi = Xi ⊕ YiGi=1表示这一位两个输入都是1,无论低位有没有进位,它都会向高位产生进位;Pi=1表示这一位两个输入不同,低位来1就传1,来0就传0。于是进位可以写成统一形式:
Ci+1 = Gi + Pi·Ci这个式子看起来只是换了个写法,但它把“进位”拆成了“本地生成”和“低位传递”两部分。Gi只依赖本位的X、Y,可以并行算;Pi也只依赖本位的X、Y,可以并行算。剩下要解决的是怎样用这些并行信号快速拼出C1、C2、C3、C4。
这里要特别说明Pi的定义。若Pi要同时参与求和,必须用Xi⊕Yi;若只用于进位传播,Xi+Yi在Gi覆盖下也能得到同样的进位结果,但求和时Si=Xi⊕Yi⊕Ci,不能用或门的结果直接异或。很多实验接线错误就出在把“进位传递”和“求和传递”混成一个门。
3.2 4位CLA展开:C1到C4如何消掉逐位等待
把Ci+1=Gi+PiCi逐级展开,得到4位先行进位的核心方程:
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以C4为例,它不再需要等C3稳定后再算,而是直接把G0~G3、P0~P3和C0代入一个两级与或网络。G3项表示第3位自己生成进位;P3G2表示第3位传递第2位生成的进位;P3P2G1、P3P2P1G0依次往下。最后一项P3P2P1P0C0表示四位全部传递,把C0一路送到C4。所有输入都是并行可得的,所以C1~C4的延迟不再随位数线性增长。
同时定义4位成组信号:
G* = G3 + P3·G2 + P3·P2·G1 + P3·P2·P1·G0 P* = P3·P2·P1·P0 C4 = G* + P*·C0G表示这一组4位整体会不会产生进位,P表示这一组整体会不会把C0传到C4。它们和C0无关,只由本组X、Y决定。这一点是16位扩展的关键:组信号可以先算好,再送给上一级先行电路。
3.3 74182的接口约束:C0、G*、P*分别接到哪
4位快速加法器通常用一个4位先行进位器件(实验资料里写作CLA74182,有的地方写成CLA74128,接线前要以器件引脚图为准)加4个全加器构成。74182负责根据G0~G3、P0~P3和C0并行产生C1~C4;4个全加器只负责算Si=Pi⊕Ci-1。这样求和路径变成“G/P生成 → 先行进位 → 异或求和”,进位不再逐位串。
最容易翻车的是C0。74182的输出C1~C4是C0的函数,如果C0悬空或忘记接,芯片内部会把C0当成不定值,低位和的进位全错。做4位加法器时C0就是外部进位输入Cin;做16位扩展时,低组的C0接总Cin,高组的C0接上一级算出的C4、C8、C12。C0不是“可选引脚”,它是先行进位方程里的自变量。
另一个接口点是G*、P*。4位加法器输出G*、P不是给本组求和用的,而是给更高一级先行电路用的。本组求和只需要C1~C4;G、P要引到组间CLA。若把G、P接反,或者把低组的G接到高组的位置,组间进位会整体错位。
3.4 测试向量复核:0x8+0x4为什么得到0xC
用实验报告里的向量:X=1000,Y=0100,Cin=0。按位从高到低是X3=1、X2=0、X1=0、X0=0;Y3=0、Y2=1、Y1=0、Y0=0。
逐位算G、P:
位3:G3=1·0=0,P3=1⊕0=1 位2:G2=0·1=0,P2=0⊕1=1 位1:G1=0·0=0,P1=0⊕0=0 位0:G0=0·0=0,P0=0⊕0=0代入组信号:G*=G3+P3G2+P3P2G1+P3P2P1G0=0;P*=P3P2P1P0=1·1·0·0=0。C4=G*+PC0=0。和数S=1000+0100=1100,即0xC;Cout=0;G=0,P*=0。这个向量能验证G/P生成和组信号输出,但因为它没有产生进位,不能单独证明C1~C4的进位传播路径。做实验时还要补一组会产生进位的向量,例如0xF+0x1,观察C4和G*。
下面用Python按4位CLA方程复算,便于和电路波形对照:
def cla4(x, y, cin): G = [(x >> i) & 1 & ((y >> i) & 1) for i in range(4)] P = [((x >> i) & 1) ^ ((y >> i) & 1) for i in range(4)] C = [0] * 5 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])) 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] s = 0 for i in range(4): s |= (P[i] ^ C[i]) << i return s, C[4], gstar, pstar print(cla4(0b1000, 0b0100, 0)) # 期望 s=1100, Cout=0, G*=0, P*=0 print(cla4(0b1111, 0b0001, 0)) # 观察 C4 与 G*参数x、y取0~15,cin为0或1。返回和、C4、G*、P*。代码里C[0]到C[4]对应C0到C4,G[0]是最低位。用0b1111+0b0001可以看到低位连续进位如何把C4抬高,这组向量比全0全1更能暴露先行进位网络的接线问题。
4. 两级先行:16位快速加法器如何用组G*/P*跨过4位边界
4.1 分组:四个4位块怎样各自产出局部G与P
16位快速加法器不是把16个全加器直接串起来,也不是把4个4位加法器简单串行进位。它的做法是把16位切成4个4位组,每组内部先用4位先行进位算出本组的C1~C4、和数S0~S3,同时产出本组的G和P。组内进位是局部的,组间进位交给第二级先行电路。
设四组从低到高为组0、组1、组2、组3。每组i的局部信号记作Gi*、Pi*。它们只由该组的4对X、Y决定,和该组的进位输入Cin无关:
Gi* = G3 + P3G2 + P3P2G1 + P3P2P1G0 Pi* = P3P2P1P0这里的G3、P3是该组最高位,G0、P0是最低位。组内4位全加器的求和仍然用本组的C1~C4,但高组的C1~C4要等第二级把C4、C8、C12分发下来后才能最终确定。所以16位先行加法器的延迟不是16级,而是“组内G/P生成 + 组间先行 + 组内求和”几级。
4.2 第二级先行:C4、C8、C12如何从组信号算出
第二级74182吃的是四个组的G0*~G3和P0~P3*,外加总进位C0。它产生的C4、C8、C12、C16分别是组间进位:
C4 = G0* + P0*·C0 C8 = G1* + P1*·G0* + P1*·P0*·C0 C12 = G2* + P2*·G1* + P2*·P1*·G0* + P2*·P1*·P0*·C0 C16 = G3* + P3*·G2* + P3*·P2*·G1* + P3*·P2*·P1*·G0* + P3*·P2*·P1*·P0*·C0总成组信号为:
G_total* = G3* + P3*G2* + P3*P2*G1* + P3*P2*P1*G0* P_total* = P3*·P2*·P1*·P0* C16 = G_total* + P_total*·C0注意这里的下标方向:G0是低4位组,G3是高4位组;展开时高组在前。若把组号接反,C4、C8、C12会整体错位,现象是低4位结果可能对,高4位全错。
第二级算出的C4送给组1作为进位输入,C8送给组2,C12送给组3;组0的进位输入就是总C0。每个4位组再把自己的C4作为组内最高进位,继续驱动组内全加器。这样跨组进位只经过一级组间CLA,而不是等低组4位全部加完再传给高组。
4.3 16位求和与最终G*/P*:输入0x0C0C+0x8484逐组拆解
用实验报告里的向量:X=0000 1100 0000 1100,即0x0C0C;Y=1000 0100 1000 0100,即0x8484;Cin=0。从低到高分成4组:
| 组 | X组 | Y组 | 组内和 | 组进位输出 | 局部G* | 局部P* |
|---|---|---|---|---|---|---|
| 组0(低4位) | 0xC | 0x4 | 0x0 | C4=1 | 1 | 0 |
| 组1 | 0x0 | 0x8 | 0x9 | C8=0 | 0 | 0 |
| 组2 | 0xC | 0x4 | 0x0 | C12=1 | 1 | 0 |
| 组3(高4位) | 0x0 | 0x8 | 0x9 | C16=0 | 0 | 0 |
组0:0xC+0x4=0x10,低4位和为0x0,向组1进位C4=1。组0的G*=1,因为最高位X3=1、Y3=0,P3=1,次高位X2=1、Y2=1,G2=1,所以G*=G3+P3G2=1。组1:0x0+0x8+1=0x9,不产生C8,G*=0,P*=0。组2同理产生C12=1。组3:0x0+0x8+1=0x9,C16=0。
最终S=0x9090,C16=0。总G*=G3*+P3G2+P3P2G1*+P3P2P1G0。因为P3*=0,后面传递项全被切断,总G*=G3*=0;总P*=P3P2P1P0=0。结果与实验报告的S=1001 0000 1001 0000、Cout=0、p*=0、G*=0一致。
这组向量的价值在于:它同时让组0和组2产生局部进位,又让组1和组3接收进位,能验证组间C4、C8、C12的分发;同时总G*、P*为0,能验证高组没有把低组信号错误传出去。如果只测0x0000+0x0000,所有进位都是0,接线错了也看不出来。
4.4 用Python按组模型复算C16
def cla4_group(x, y): # 只算本组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)] 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 gstar, pstar def add16_two_level(x, y, cin): xs = [(x >> (4 * i)) & 0xF for i in range(4)] ys = [(y >> (4 * i)) & 0xF for i in range(4)] gp = [cla4_group(xs[i], ys[i]) for i in range(4)] Gs = [gp[i][0] for i in range(4)] Ps = [gp[i][1] for i in range(4)] C = [0] * 5 C[0] = cin C[1] = Gs[0] | (Ps[0] & C[0]) C[2] = Gs[1] | (Ps[1] & Gs[0]) | (Ps[1] & Ps[0] & C[0]) C[3] = Gs[2] | (Ps[2] & Gs[1]) | (Ps[2] & Ps[1] & Gs[0]) | (Ps[2] & Ps[1] & Ps[0] & C[0]) C[4] = (Gs[3] | (Ps[3] & Gs[2]) | (Ps[3] & Ps[2] & Gs[1]) | (Ps[3] & Ps[2] & Ps[1] & Gs[0]) | (Ps[3] & Ps[2] & Ps[1] & Ps[0] & C[0])) g_total = Gs[3] | (Ps[3] & Gs[2]) | (Ps[3] & Ps[2] & Gs[1]) | (Ps[3] & Ps[2] & Ps[1] & Gs[0]) p_total = Ps[3] & Ps[2] & Ps[1] & Ps[0] # 各组用第二级分发的C[i]作为本组Cin,再算组内4位和 s = 0 for i in range(4): si, _, _, _ = cla4(xs[i], ys[i], C[i]) s |= si << (4 * i) return s & 0xFFFF, C[4], g_total, p_total print(hex(add16_two_level(0x0C0C, 0x8484, 0)[0])) # 期望 0x9090 print(add16_two_level(0x0C0C, 0x8484, 0)[1:]) # 期望 C16=0, G*=0, P*=0参数x、y取0~0xFFFF,cin为0或1。代码先按组算G*、P*,再用第二级方程算C4、C8、C12、C16,最后把每组C[i]送回组内CLA求和。用0x0C0C+0x8484可以看到组0和组2各自产生进位,而总G*、P*仍为0;再把X改成0xFFFF、Y改成0x0001,可以看到连续进位如何一路穿过四个组,C16被抬高。这两类向量配合,才能同时验证“组内先行”和“组间先行”。
5. 避坑与排查:运算器实验里最容易翻车的五类进位链问题
5.1 接线类故障:C0悬空、Sub没进最低位、P用错门
现象一:减法结果总是差1,加法正常。原因:Sub只接到了Y的异或门,没有接到最低位全加器的进位输入C0。补码减法要求X+~Y+1,那个“1”就是C0=Sub。解决:把Sub同时引到8个Y异或门和最低位C0;用0x05-0x03(应得0x02)和0x03-0x05(应得0xFE)两组向量对拍。
现象二:4位加法器低位和正确,高位和随机或全错。原因:74182的C0没有接,或者C0接到了固定电平而不是外部Cin。74182产生的C1~C4是C0的函数,C0悬空会让内部节点进入不定态。解决:先量74182的C0引脚,确认它等于外部Cin;做16位扩展时,低组C0接总Cin,高组C0接第二级CLA的C4、C8、C12。
现象三:进位传播时对时错,换一组数据就翻车。原因:把Pi用成了Xi+Yi,又把这个或门输出直接送去做求和异或。进位传播在某些输入下看起来对,但Si=Xi⊕Yi⊕Ci要求Pi必须是异或结果。解决:G/P生成级用Xi·Yi和Xi⊕Yi;若器件内部已经把Pi做成或门,求和必须另取异或信号,不能一根线两用。
5.2 判读类故障:把Cout当OF、把组G当总G
现象四:有符号溢出判断错误,加法结果明明是负数却报没溢出。原因:直接把Cout当OF。Cout是最高位向外的进位C8,OF是C8⊕C7。比如0x7F+0x01,C8=0但C7=1,OF=1;只看Cout会漏报。解决:从波形里同时抓C7和C8,做异或;或者用“两正得负、两负得正”的符号位规则交叉验证。
现象五:16位加法器总G*、P输出与预期不符,但和数是对的。原因:把某个4位组的局部G当成了总G*,或者组间展开时把组号方向接反。总G要由第二级CLA按G3、P3*、G2*、P2*、G1*、P1*、G0*、P0的顺序算,不是四个局部G直接或。解决:先单独量每组G*、P*,再量第二级C4、C8、C12,最后量总G*;用0x0C0C+0x8484这种“局部有进位、总G*为0”的向量,最容易看出组间传递项有没有接反。
5.3 测试向量设计:全0全1测不出进位链
全0加全0所有进位都是0,全1加全1所有进位都是1,这两组只能验证电源和基本连通,不能验证进位链的选择性。有效的向量要满足三个条件:至少一组让低位产生进位、至少一组让进位跨过4位边界、至少一组让符号位进位与最高位进位不同。比如8位用0x7F+0x01看OF,用0xFF+0x01看C8;4位用0xF+0x1看C4,用0x8+0x4看G*/P*为0;16位用0x0C0C+0x8484看组间分发,用0xFFFF+0x0001看C16。
排查时按“先电源、再C0、再G/P、再组间C4/C8/C12、最后OF”的顺序量。不要一上来就盯着和数看,和数是进位链的最后结果,进位节点稳定了,和数自然对。实验报告里的故障与调试部分如果只写“经检验正常运行”,复查时就少了一张定位地图;把每组测试的C0、C4、C8、C12、C16记下来,比拍一张整体电路图更有用。
6. 设计取舍:位数继续变大时,先行进位该停在哪一级
把8位串行、4位先行、16位两级先行放在一起看,能看出一个清晰的取舍:先行进位不是越多越好,而是用“分组”换“扇入”。4位一组的CLA展开式里,C4项最多要5个输入与门再或起来,扇入还在TTL器件能接受的范围内;如果直接做16位全先行,C16的展开项会膨胀到十几个乘积项,门电路扇入和布线都会变得不现实。16位快速加法器的做法是组内4位先行、组间再4位先行,用两级CLA把16位拆成4×4,延迟从16级压到“组内生成 + 组间进位 + 组内求和”几级,面积又没有失控。
| 结构 | 关键路径大致级数 | 主要代价 | 适用场景 |
|---|---|---|---|
| 8位串行进位 | 约8级全加器进位 | 门数少,延迟随位数线性增长 | 验证补码、溢出、Sub控制 |
| 4位先行进位 | 约2~3级(G/P、CLA、求和) | 需要74182和展开与或网络 | 作为16位的基本积木 |
| 16位两级先行 | 约3~4级(组内G/P、组间CLA、组内求和) | 需要两组CLA和组间布线 | 兼顾速度与扇入 |
再往上做32位、64位,常见思路不是继续把一级CLA的方程写长,而是增加分级:4位一组、4组一级、再4组一级,形成树状先行。每增加一级,跨组进位的延迟只增加一级CLA,但布线复杂度和组信号数量上升。真正的工程判断点在于:你的关键路径是进位链还是求和异或;如果工艺门延迟已经很低,继续堆先行级的收益会被布线和扇出吃掉。
验证时我只看三个量:C0到Cout的依赖路径有没有被组间CLA截断,OF是不是C8⊕C7而不是Cout,总G*、P*是不是由第二级按高组到低组展开。把这三个量在波形里标出来,再配合0x7F+0x01、0xF+0x1、0x0C0C+0x8484、0xFFFF+0x0001四组向量,基本能覆盖串行链、组内先行、组间分发和溢出判据。那份运算器实验报告把8位、4位、16位的设计要求、方案设计、故障调试和测试分析按顺序记录下来,电路图和测试向量可以直接对照;需要拿它做连线复核时,下载后先翻到每节的测试与分析,把输入输出抄到自己的波形旁边,比从头读一遍更快。
从那以后我每次搭进位链,都强制先画一张从C0到Cout的依赖图,标出哪些进位是组内产生、哪些是组间传递,再动烙铁或连仿真。图里只要有一根线跨过了不该跨的组边界,测试向量一跑就会露馅。希望帮到你。
本文还有配套的精品资源,点击获取