如果你正在学计算机组成原理或者数字逻辑,大概率绕不开 Logisim 这个东西。我当年第一次打开 Logisim,是想在搭 MIPS 单周期 CPU 之前,把最底层的算术单元搞明白;结果一上手就发现,看着简单的一个加法器,真落到实处要处理的东西还挺多。这篇博文就记录我当时是怎么用一个 4 位行波进位加法器从零搭起,并改造成支持补码加减法的完整过程。文章里每一步都有接线思路、为什么这么连的解释,以及我踩过的坑;适合正在做 Logisim 新手上路实验、准备做单周期或多周期 MIPS CPU 设计,或者单纯想搞懂补码加减法的同学。看完之后,你不仅会得到一个能跑通的仿真电路,还能理解位宽、进位、溢出这些直接决定 CPU 行为的关键概念。
1. 方案设计:为什么是行波进位加法器和补码?
1.1 从需求出发:CPU 为什么需要一个加法器?
在数字电路里,加法器是 CPU 最基础也最核心的部件之一。无论你是算地址、做 ALU 运算、实现 PC 自增,都离不开加法。你要是问那为什么偏偏要先做一个 4 位加法器,理由很简单:4 位是教学里性价比最高的位宽,它足够小,让你能一颗门一颗门看清楚进位怎么流动;又足够大,能演示超过 8 的进位、负数补码、以及溢出场景。等你吃透这 4 位,扩展到 8 位、16 位,只是把同样的单元复制几遍而已,完全不会改变核心原理。
另外,你还会在 Logisim 的很多经典实验里看到加法器的身影,比如七段数码管电路、单总线 CPU 设计、理想流水线设计。很多做多周期 MIPS CPU 的同学都会先在 Logisim 里过一遍加法器,因为它是 ALU 的地基。地基稳不稳,直接影响后面指令能不能在一个周期内算对。
1.2 行波进位加法器的结构与选型理由
行波进位加法器听名字很高端,其实结构就是课堂上说的把多个全加器串起来:每一位的进位输出,接到下一位的进位输入。第 0 位在没有外部进位时,接 0;多位数乘法或后续减法控制时,再把这个入口换成控制信号。因为进位信号要像水波一样一级一级往前传,所以叫 ripple carry。
它最大的优点是结构简单、原理直观,很适合引出“时序”和“延迟”的概念。缺点也很明显:每一位的进位都必须等前一位算完才能确定,位数越多延迟越大,4 位还好,32 位如果直接用行波进位,时钟周期就得拉得很长。所以你先用行波进位入门,之后再去了解超前进位加法器,就知道后者到底优化了什么。实际项目中,4 位行波进位加法器还有一个教学上的好处:布线整齐,方便你观察每一个全加器的输入输出是否正确,排查问题时会轻松很多。
1.3 为什么补码能统一加减法
既然加法器能实现加法,那减法怎么处理?一种朴素想法是先比大小、再相减、再定符号,但这样电路会非常复杂。补码的思路更聪明:把减法 A - B 转化为 A + (~B) + 1。原因是在补码表示里,B 的相反数就是按位取反加一,也就是 ~B + 1。这样一来,一张加法电路加上若干取反逻辑,就能同时处理加法和减法。
你可以类比钟表:要拨快 3 小时,拨 +3 是对的;要拨回 3 小时,也可以等价地拨 +9,因为在 12 小时制里,-3 和 +9 是同一个动作。补码就是把往前拨和往后拨统一成同一个动作。这也是为什么现代 CPU 的 ALU 里只有加法器,没有专门的减法器,减法全靠补码转换。
1.4 4位位宽的选择与扩展
关于位宽,我建议第一次动手就用 4 位而不是 1 位,也不要一下子直接做 32 位。1 位加法器虽然简单,但你很难体会进位链对整个系统的影响;32 位又会让布线变得繁琐,一旦连线出错,排查成本很高。4 位是一个很好的折中:你有 4 组全加器,能看到进位从最低位跑到最高位的过程;同时 4 位补码能表示 -8~7,正好可以演示溢出。
如果你做的实验要求 8 位或 16 位,也没关系。等你把 4 位电路调通后,只需要在 Logisim 里复制几个全加器,把输入输出引脚位宽改成 8 或 16,进位链继续往下串,基本思路完全一样。真正要重新设计的是延迟优化,不是结构设计。
2. 搭建前的原理准备:半加器、全加器与进位链
2.1 半加器:最简单的一位加法
半加器只做一件事:把两个 1 位二进制数相加。输入是 A、B,输出是和 S 以及进位 Cout。它的真值表非常简单:
| A | B | S | Cout |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
写成布尔方程就是 S = A XOR B,Cout = A AND B。XOR 为什么能当求和用?因为二进制加法的第 0 位本质上就是看两个输入是否不同,不同为 1,相同为 0;至于同时为 1 的,进位另外输出。理解了这层,后面全加器就容易了。
2.2 全加器:考虑进位输入
实际多位加法中,第 i 位的加法不仅要管本位的 A_i 和 B_i,还要管低位可能会送过来的进位 C_{i-1}。所以全加器有三个输入:A、B、Cin,两个输出:Sum、Cout。真值表有 8 行,我建议不要死记,直接按自然想法推:三个输入加在一起最多是 3,也就是二进制 11。所以 Sum 就是三个输入 XOR 的结果,Cout 是三个输入里至少两个为 1 时输出 1。
逻辑表达式可以写为:
Sum = A XOR B XOR Cin Cout = (A AND B) OR (Cin AND (A XOR B))两套常用实现里,第一种先用 XOR 求出 A 和 B 的异或结果 xor_out,再用第二个 XOR 把 xor_out 和 Cin 相加得到 Sum;进位部分用两个 AND 门加一个 OR 门,分别判断 A、B 同为 1,或者 xor_out 和 Cin 同为 1。第二种实现更直观但门数更多,直接用三个 AND 门判断 A、B、Cin 中任意两个为 1。在 Logisim 里我建议用第一种,布线最清晰,也更接近教材上的标准全加器图。
2.3 进位链的工作方式
所谓行波进位,核心就在这一串 Cin、Cout 的连接关系。4 位加法器可以看成四个全加器 FA0、FA1、FA2、FA3 首尾相连:FA0 的 Cin 接外部进位,加法运算时接 0;FA0 的 Cout 接 FA1 的 Cin;FA1 的 Cout 接 FA2 的 Cin;FA2 的 Cout 接 FA3 的 Cin。
最低位的进位信号必须从 FA0 一路穿到 FA3,路径上每经过一个全加器,一般要经历约两个门的延迟。所以最坏情况下,一个 4 位行波进位加法器需要约 8 个门延迟才能稳定输出。这个慢在 4 位演示阶段不明显,但它解释了为什么高性能 CPU 会用超前进位、进位选择等技巧。你在 Logisim 里做实验时,其实看不到肉眼可见的延迟,因为模拟器默认按稳定状态计算,但理解这条传播链,对将来做流水线设计和时序分析非常重要。
2.4 Logisim 中的部件规划
在动手连线前,先熟悉一下 Logisim 的常用组件。你需要的核心部件有:Input/Output Pin,AND Gate、OR Gate、XOR Gate、NOT Gate、Tunnel,Probe,以及 Text 标签。有一个使用习惯特别推荐:把一个完整的全加器封装成子电路。这样主电路会变得清爽,调试时也能单独验证子电路。
做法很简单:Project 菜单里点 Add Circuit,新建一个叫 FullAdder 的电路,在里面放 3 个输入引脚 A、B、Cin,两个输出引脚 Sum、Cout,之后需要的时候在主电路里直接拖入这个自定义元件就可以。用子电路还有一个好处:你改 FullAdder 内部结构,所有引用它的地方都会同步更新,这在后面扩展位宽时非常省事。
很多同学觉得 Logisim 难,其实难的不是工具本身,而是没有从一开始养成模块化思维。把全加器封装好了,4 位加法器和 32 位加法器对你来说都只是“多拖几个模块”而已。
3. 手把手搭建 4 位行波进位加法器
3.1 Logisim 工程准备
打开 Logisim 后,先把工程保存为一个独立文件。如果用的是 Logisim 2.7.1 或者 Logisim-Evolution,菜单布局会稍有不同,但工程逻辑是一样的。进入主电路后,建议先在属性栏里统一确认位宽:输入引脚默认位宽是 1,后面如果要挂 4 位总线,可以改成 4,但第一次搭建建议所有输入输出都用 1 位引脚。这样做的好处是你能看到每一位的传输过程,排查起来最直观。
另一个技巧是打开 View 里的 Grid,让连线自动吸附网格。Logisim 的连线不支持斜线,只支持水平和垂直,网格吸附能帮你减少不少对齐烦恼。工程文件命名也建议用英文加数字,比如 RippleCarryAdder4.circ,避免某些旧版本工具对中文路径支持不好。
3.2 搭建全加器子电路
现在开始建 FullAdder 子电路。先添加三个输入引脚 A、B、Cin,两个输出引脚 Sum、Cout。然后把下面的逻辑门按顺序放好:
A ----\ XOR -> xor_out ------\ XOR ---- Sum B ----/ / / / / Cin ------------------------------/--------/ A ----| AND -> and_ab -------\ B ----| OR --- Cout xor_out -- AND -> and_cin ---------/ Cin -----/简单说,就是先做两个 XOR:第一个 XOR 接 A 和 B,输出 xor_out;第二个 XOR 接 xor_out 和 Cin,输出 Sum。进位部分用两个 AND:第一个 AND 接 A 和 B,输出 and_ab;第二个 AND 接 xor_out 和 Cin,输出 and_cin;最后用 OR 把 and_ab 和 and_cin 合并成 Cout。
连完之后可以用 Poke 工具手动置位,先验证一个简单的组合:A=0,B=0,Cin=0,Sum 和 Cout 都应为 0;A=1,B=1,Cin=1,Sum=1,Cout=1。如果子电路这个测试都不通过,后面主电路就不用谈。
3.3 级联四个全加器
回到主电路,你会发现 Library 面板里多了一个 FullAdder 元件。拖四个进来,分别当作 FA0、FA1、FA2、FA3。我习惯把 FA0 放最左边,FA3 放最右边,数据流从左往右走,看起来最符合电路图习惯。
接下来放置输入引脚:A0、A1、A2、A3 对应第 0 位到第 3 位;B0、B1、B2、B3 同理。输出引脚是 S0、S1、S2、S3,以及最高位进位 Cout。接线时把 FA0 的 Cin 接一个 Constant 0,或者暂时留空并接一个固定低电平;然后把 FA0 的 Cout 接到 FA1 的 Cin,依次类推。最后把 A0 接到 FA0 的 A,B0 接到 FA0 的 B,S0 接到 FA0 的 Sum,其他位同样处理。
这里最容易犯的错是位序混乱。在纸上标注 A3 是最高位还是最低位,A0 是 LSB,A3 是 MSB,接错了整个结果会变得莫名其妙。我在实际项目里见过有人把 A0 接到 FA3 上,结果 3 + 5 算出来完全不对,最后查了半小时才发现是位序接反。
3.4 输出与显示
为了能直接观察结果,我会在每个 Sum 输出上接一个 Probe,再在最高位进位处也接一个 Probe。Probe 在 Logisim 里会直接显示当前电平是 0 还是 1,比单纯看线的颜色直观很多。
你还可以加一个七段数码管或者 Hex Digit Display 把 4 位二进制转换成十六进制,但这里我建议谨慎:七段数码管默认按无符号数显示,如果你的实验结果里有负数补码,比如 1111,它会显示 F 而不是 -1,很容易误读。所以我更推荐先用二进制显示,等你对补码有感觉了,再把它换成十六进制也不迟。
接线完成后,先做一个加法测试:A=0101,也就是 A0=1、A1=0、A2=1、A3=0;B=0011,也就是 B0=1、B1=1、B2=0、B3=0。这时 FA0 的 Sum 应该是 0,进位 1;FA1 的 Sum 是 0,进位 1;FA2 的 Sum 是 0,进位 0;FA3 的 Sum 是 1。最终 S3S2S1S0 = 1000,也就是 8,电路就通了。
4. 改造为补码加减法器
4.1 用异或门加控制端实现取反
减法如果回到最底层,需求很简单:当用户在计算 A-B 时,我们想让 B 以补码的形式参与加法。那对 B_i 的每一位,当控制信号 M=1 时取反,M=0 时保持原样。选 XOR 门刚好满足这个需求:B_i XOR M。M=0 时输出 B_i,M=1 时输出 NOT B_i。所以对每一位 B_i,在前面串一个 XOR 门,把 B_i 和 M 一起接进去就行。
这在教材里通常叫可控取反器,或者叫 Controlled Inverter。它的门电路成本很低,一个输入一个控制位就能完成,是 ALU 减法标准做法。实际接的时候,我会把四个 XOR 门放在输入引脚和全加器之间,让数据流从左边输入开始,经过取反,再进入全加器,非常规则。
4.2 减法模式下的初始进位 Cin=1
光取反还不够,补码的负数要求 ~B + 1,所以还要把 +1 加上去。这个 +1 不需要额外电路,只需把最低位全加器的进位输入接到 M 上:加法时 M=0,进位输入=0;减法时 M=1,进位输入=1,正好完成 +1。
这一步看似简单,却特别容易被忽略。我第一次做减法电路时,明明已经给 B 加了 XOR 取反,结果 5-3 算出来是 4,怎么都对不上,后来才意识到补码少了 +1。因为 5-3 应该是 5 + (~3) + 1 = 5 + 12 + 1 = 18,截断到 4 位是 0010,也就是 2;如果不加这个 1,得到的是 0001,自然错了。
4.3 完整接线与逻辑
现在主电路的连接关系就很清晰了:
- A0..A3 直接接到四个全加器的 A 输入;
- B0..B3 先分别过 XOR 门,再接到全加器的 B 输入;
- 控制信号 M 接到所有 XOR 门,同时接到 FA0 的 Cin;
- 其他进位照常级联;
- 最终输出 S0..S3,以及最高位进位 C_out。
这个电路在硬件上等价于一个能执行加法和减法的 4 位 ALU 核心单元。M=0 时,B 原样进入加法器,A+B 完成加法;M=1 时,B 取反并加 1,相当于 A + (~B) + 1,等于 A-B。加法和减法共用同一套加法逻辑,这正是补码设计的魅力。
你可以给 M 用一个开关引脚,方便在 Logisim 里切换加法和减法,交互感受非常直观。
4.4 溢出与符号位处理
这里必须强调一个比实践更重要的概念:在补码运算里,最高位的进位 C_out 并不等于溢出。拿 4 位补码范围 -8~7 来看,如果算 7+1,结果是 1000,也就是 -8,这显然是错的,这叫溢出。可是最高位进位是 0,你只看 C_out 根本发现不了问题。
正确的溢出检测是判断最高位,也就是第 3 位运算的进位输入和进位输出是否相等。最直接的方法是再增加一个 XOR 门,一端接 FA2 的 Cout,也就是 FA3 的 Cin,另一端接最终 C_out,输出接入 Overflow 指示灯。如果溢出指示灯亮,说明结果超出了 4 位补码能表示的范围,需要扩展位宽或者改用更高精度电路。
很多同学在实验报告里把 C_out 当成溢出标志,这是不严谨的。C_out 对无符号加法确实可以表示进位,但在有符号补码运算里,溢出判断要看进位输入和进位输出是否一致,这一点一定要区分清楚。
5. 验证与调试:真值表、边界用例与常见问题
5.1 功能测试表
电路搭完之后,不能只测一组数据就以为万事大吉。我建议至少把下面的测试用例都跑一遍:
| A | B | M | 预期 S | C_out | 说明 |
|---|---|---|---|---|---|
| 0101(5) | 0011(3) | 0 | 1000(8) | 0 | 普通加法 |
| 0111(7) | 0001(1) | 0 | 1000(-8) | 0 | 有符号溢出,Overflow 应为 1 |
| 1111(-1) | 0001(1) | 0 | 0000(0) | 1 | 无符号进位,有符号结果正确 |
| 0111(7) | 0111(7) | 0 | 1110(-2) | 0 | 无符号进位,有符号溢出 |
| 0101(5) | 0011(3) | 1 | 0010(2) | 0 | 5-3 正常减法 |
| 0011(3) | 0101(5) | 1 | 1110(-2) | 0 | 3-5 结果是补码 -2 |
| 0000(0) | 0001(1) | 1 | 1111(-1) | 0 | 0-1 结果为 -1 |
测试时把每一组的输入引脚用 Poke 工具拨到对应电平,看 Probe 上的输出即可。如果某一位输出和预期不符,先用眼睛检查接线,再用 Probe 分段检查是取反部分错了、进位链断了,还是全加器内部有问题。
5.2 常见问题
第一个常见问题是位序接反。A0 是第 0 位,A3 是第 3 位,很多人连线时图快,结果把高位和低位对调,整个结果就会乱套。第二个常见问题是忘记把 M 接到 FA0 的 Cin,导致减法结果总比正确值少 1。第三个问题是只给 B 加了 XOR 取反,但没接 M 到 XOR 门的控制端,取反器根本不起作用。第四个问题是把 C_out 误当成溢出标志,在实验结果里看到 C_out=1 就说溢出,这在小数范围测试时容易判断错。第五个问题是用七段数码管显示补码结果,把负数 1110 看成 14,而不是 -2。
5.3 排查技巧
我在 Logisim 里排查电路问题时会用几个很有效的技巧。第一,让子电路单独测试:把 FullAdder 子电路留空只放信号源,直接验证它本身逻辑正确,再回主电路看连接。第二,用 Probe 挂到关键节点,比如 XOR 输出、FA0 的 Cout、FA3 的 Cin,从关键节点开始分段缩小问题范围。第三,学会看 Logisim 的线色,不同版本颜色含义略有差异,但异常颜色往往代表悬空或者错误状态,出现不对的颜色就要顺着线查。第四,改完电路后记得按 Ctrl+R 重置仿真,Logisim 有时会保留旧的仿真状态,导致你明明改了接线,输出却没更新。第五,复杂工程多用 Tunnel 给长线起名字,比如把 M 控制线用 Tunnel 标成 SUB,把 A3 用 Tunnel 标成 MSB,接线和查线都会更清晰。
6. 经验总结与扩展方向
6.1 我踩过的坑
说点个人体会。我在做这个实验时,第一次把 FullAdder 的 Cout 接到了前面一个门的准备位置,结果从第二级开始进位就没有传下去,现象是低位的加法正确,高位的加法全部少了进位。当时我还以为是全加器逻辑写错了,花了一个多小时才发现是进位链少接了一段。Logisim 这种图形化连线最容易出这种问题,因为一根线没接上,仿真器不会报错,只会给你一个看起来还算正常但逻辑错误的结果。
还有一次,我在做减法时给所有 B 都加了 XOR 取反,但忘了把 FA0 的 Cin 接到 M 上,结果 5-3 等于 4。这一点我已经在上面提过,真的要再强调一次,因为太容易被忽略了。补码加减法器如果没有这一步,等于只做了取反没做加一,整个减法模块就是残缺的。
另外我想提醒的是,在做实验报告时,不要只贴一张电路图就算完事。把设计思路、真值表、关键信号、测试结果截屏都放进去,一来你自己复盘方便,二来老师能看出你是真的理解了。毕竟 Logisim 这类教学工具的价值,不在于搭出一个能动的图,而在于通过动手把抽象的数字逻辑变成看得见摸得着的电路。
6.2 扩展到 8 位、16 位以及超前进位
等你把 4 位行波进位加法器完全跑通,可以考虑做 8 位版本。做法很简单:把输入输出引脚位宽改成 8,多拖 4 个 FullAdder,进位链继续往下串。但这个时候你会明显感觉到,位数越多,行波进位的延迟问题越突出。8 位情况下,进位从最低位到最高位大约要经过 16 层门,这在高频 CPU 设计里是不可接受的。
于是就有了超前进位加法器,它的思路是在同一级逻辑里并行计算出所有进位,而不是逐个等待。你可以在 Logisim 里把 4 个全加器的进位公式展开,用与或逻辑把 C1、C2、C3 直接算出来,这样最高位不再依赖前面的串行路径。这个练习非常锻炼逻辑化简能力,也能让你真正理解为什么现代 CPU 要用 CLA 而不是 RCA 来做地址加法器。
6.3 与 MIPS CPU 项目的关系
很多同学在 Logisim 里做单周期或多周期 MIPS CPU 设计时,最怕的部分就是 ALU。其实你手上这个 4 位补码加减法器,已经是 ALU 的核心雏形。MIPS 的 ALU 需要支持 add、sub、and、or、slt 等功能,其中 add 和 sub 用的底层电路就是补码加法器,只是位宽换成了 32 位。slt 指令也要用减法来判断大小,本质还是这道加减法逻辑。
所以我的建议是,在正式做 MIPS CPU 之前,一定先把加减法器玩熟。把 4 位扩展成 32 位,加上 ALUop 控制信号,再把 and、or、slt 等功能用多路选择器组合进来,就是一个很标准的单周期 ALU。后续做 PC 自增、地址偏移、分支判断时,你会发现处处都是加法器,处处都在用补码。这也是我觉得先从这颗 4 位加法器入手最值得的原因——它个头小,却几乎牵连着计算机组成里最重要的几个核心概念。