计算机组成原理这门课,对软件学院的学生来说,一直有种"最熟悉的陌生人"的感觉。理论课上讲的流水线、Cache、中断,每个字都认识,但连在一起总觉得隔着一层纱。直到做到实验二,我才真正有一种"哦,原来CPU是这么回事"的通透感。
这篇博文,我把山东大学软件学院计算机组成原理课程设计实验二的完整思路、实操步骤、踩坑记录都整理出来。不管你是刚开始接触Logisim的新手,还是已经搭过电路想优化性能的老手,这篇文章都能给你一些参考。我会从实验目标反推设计思路,讲清楚ALU、进位链、控制信号这些核心模块到底怎么搭,再把我调试时遇到的那些奇葩问题原原本本摆出来——这些问题在实验指导书上可不会写。
1. 先别急着手画电路:实验二到底在考什么
1.1 从课程设计的全局图看实验二的位置
软件学院开设计算机组成原理课程设计,目的从来不是让你成为一个硬件工程师,而是让你通过亲手搭建关键部件,理解"软件指令最终是如何被硬件执行的"这条核心链路。实验一通常围绕数据表示、译码器、多路选择器等基础组合逻辑器件展开,让你熟悉工具和基本电路思维。到了实验二,就开始进入CPU的核心地带——运算器(ALU)与相关时序逻辑了。
在很多高校的教学安排里,实验二往往对应着"运算器设计"或者"ALU与寄存器堆设计"。它考察的知识点包括但不限于:进制转换与补码运算、组合逻辑电路设计、加法器的结构与进位优化(这里就涉及热搜词里反复出现的"组间串行进位")、功能选择信号(S信号)的编码规则,以及如何通过时序信号(时钟、复位)把运算器嵌到一个更大的状态机或计数器场景里。
值得特别注意的是,软件学院的学生往往会问:"我们以后写Java/Python,学这个有什么用?" 我的回答是:实验二的核心不是让你记住74LS181芯片的引脚定义,而是强迫你站在硬件工程师的角度,理解一条ADD R1, R2指令在硬件层面到底经历了什么。当你亲手把控制信号拉到指定电平、看到运算结果从输出引脚稳定出现的那一刻,你对"程序是如何跑起来的"这个问题的理解,会比背一百遍王道笔记都深刻。
1.2 我拿到题目后第一时间做的事
很多人拿到实验指导书,第一反应是打开Logisim就开始拖元件。千万别这样。我在做完实验一、准备实验二时,总结了一套流程,可以帮你省下至少一半的调试时间:
第一步,通读实验指导书,提取硬性要求:比如支持的运算类型(是只做加减法,还是包含AND、OR、XOR、SLT?)、数据位宽(8位还是16位,还是32位?)、是否有进位输出(Carry Out)、是否有溢出标志(Overflow)等。用荧光笔把这些硬指标圈出来,这就是验收的最低标准。
第二步,画出顶层结构草图:不需要画电路图细节,只需要画出数据流。比如输入A、输入B进入ALU内部,控制信号S决定执行哪种运算,结果从Output输出,同时还有Zero(零标志)、Carry(进位标志)等状态输出。有了这个数据流图,后续模块化设计就有了骨架。
第三步,确认工具版本和元件库:山大软院的实验环境一般使用Logisim,但Logisim有两个常见版本——原始的Logisim 2.7.x和活跃维护的Logisim-Evolution。两个版本在元件库名称、引脚命名上有细微差别。我个人建议使用Logisim-Evolution,因为它支持子电路封装时自定义引脚标签,调试体验好很多。而且它自带的"位分线器"(Bit Extender / Splitter)和探针(Probe)在调试时非常直观。
第四步,预设测试向量:在设计之前,就把测试用例想好。比如
A=0x0F, B=0xF0, ADD结果应该是什么,A=0x7F, B=0x01, ADD是否产生溢出。好的测试向量是调试的"导航仪",没有它,电路出现问题后你会像无头苍蝇一样乱找。
1.3 实验环境准备:几个容易忽视的细节
环境准备阶段看起来简单,但有几个细节值得单独提出来说。第一个是Logisim中"项目属性"的设置。在Project > Options里,建议把电路的仿真精度调高,同时开启"Complete Gates with invalid inputs"的警告提示,这样当引脚悬空或连接到无效电平时,Logisim会给出明确的警告线(蓝色线表示悬空,红色线表示冲突),这对后续排查问题至关重要。
第二个是子电路的封装命名。实验二功能复杂,强烈建议使用子电路(Subcircuit)把ALU、进位链、计数器等功能模块拆开。命名时不要用1-ALU这种带横杠的名字,Logisim对以数字开头或带特殊符号的名称有时会导致引脚映射混乱。我习惯用ALU_8bit、CLA_4bit、PC_Count这种命名方式,清晰且不容易出问题。
第三个是保存路径和自动备份。Logisim偶尔会崩溃,特别是在拖拽大量连线时。我因为没保存吃过一次大亏,重新搭了大半个电路。建议开启系统的自动保存,或者每隔半小时手动存一次档,并保留.circ文件的版本副本,这样即使改坏了也能回退。
2. 四个必懂原理:ALU、加法器、进位链、控制信号
2.1 ALU能干哪些事:算术、逻辑、移位
实验二的核心模块是ALU(Arithmetic Logic Unit,算术逻辑单元)。它就像一个小型计算中心,输入两个操作数,根据控制信号决定执行哪种运算,然后把结果送出去。经典的ALU操作可以分成三类:
算术运算:加法(ADD)、减法(SUB)、加一(INC)、减一(DEC)、比较(SLT,Set Less Than)。 逻辑运算:与(AND)、或(OR)、异或(XOR)、或非(NOR)、取反(NOT)。 移位运算:逻辑左移(SLL)、逻辑右移(SRL)、算术右移(SRA)。
实验二一般不会要求实现全部操作,但至少会要求实现ADD、SUB、AND、OR、XOR、SLT中的若干种。设计ALU时最核心的思路是:不要为每一种运算单独建一套硬件电路,而是尽量让不同的运算复用同一个硬件结构,通过控制信号做选择。最常见的架构是"一个加法器打天下"——减法通过加补码实现(A - B = A + (~B + 1)),SLT通过比较减法结果的符号位实现,AND/OR/XOR则是独立的逻辑门组合,最后通过一个多路选择器(MUX)把选中的结果送出去。
2.2 全加器与进位传播
要理解ALU里的加法器,首先必须吃透全加器(Full Adder,FA)。全加器有三个输入(A、B、Cin)和两个输出(Sum、Cout)。它的逻辑表达式是:
Sum = A XOR B XOR Cin Cout = (A AND B) OR (Cin AND (A XOR B))用一句话解释:A和B如果都为1,不管低位有没有进位,这一位都会产生进位输出;如果A和B中只有一个是1,那么只有当低位进位Cin为1时,这一位才会产生进位输出。
这里多说一句,我见过不少同学直接复制全加器的表达式,但在Logisim里连线时把Cout和Sum的顺序搞反,或者在画门电路时把Cin AND (A XOR B)这一项漏了,导致进位链完全错乱。这个基础的逻辑一定要自己推一遍,而不是死记硬背。
把N个全加器串联起来,让低位的Cout连到高位的Cin,就得到了行波进位加法器(Ripple Carry Adder,RCA)。它的优点是结构简单,缺点也很致命:高位的结果必须等低位进位逐级传播过来才能确定。对于8位的加法器,最坏情况下需要等待8个全加器的进位延迟;如果是32位,延迟会更大。这就是为什么会出现"组间串行进位"这个优化方案——它的目的就是缩短进位传播路径。
2.3 组间串行进位:从串行到高性能的过渡
热搜词里的"组间串行进位"(Carry Lookahead,也叫超前进位,但"组间串行"这个说法更细致地描述了一种层次化设计方式)是实验二的重要得分点。在4位超前进位加法器(CLA)中,我们可以根据输入直接算出每一位的进位,而不需要等待低位逐级进位。
超前进位依赖于两个信号:生成信号G(Generate)和传播信号P(Propagate)。
G = A AND B P = A XOR B有了G和P,每一位的进位可以这样表示:
C1 = G0 OR (P0 AND C0) C2 = G1 OR (P1 AND G0) OR (P1 AND P0 AND C0) C3 = G2 OR (P2 AND G1) OR (P2 AND P1 AND G0) OR (P2 AND P1 AND P0 AND C0) C4 = G3 OR (P3 AND G2) OR (P3 AND P2 AND G1) OR (P3 AND P2 AND P1 AND G0) OR (P3 AND P2 AND P1 AND P0 AND C0)这个公式看起来很长,但它的意义是革命性的:第4位的进位C4可以直接由第一级的输入A[3:0]、B[3:0]和外部进位C0同时计算出来,不需要逐级等待。这就像在职场上,与其让每个环节都等前一个环节完成后才开始,不如让所有环节同时开工,只等最关键的几个外部条件。当然,公式越长意味着电路的门延迟并没有完全消失,但它把延迟从"位数×每级延迟"降到了"固定级别的门延迟"。
那么"组间串行"又是什么意思?以16位加法器为例,我们可以把它拆成4组,每组是一个4位超前进位加法器。组内部用超前进位加速,而组与组之间仍然采用串行方式——即第二组需要等第一组的进位输出C4达到后,才开始自己组的超前进位计算。这种设计是性能和复杂度之间的折中:相比全超前进位需要大量的与门和或门,组间串行的硬件消耗更少,电路更清晰,而相比完全行波进位,速度又有了质的提升。实验二让你实现这个结构,就是为了让你亲身体会"空间换时间"这句话在硬件设计里是怎么落地的。
2.4 控制信号:让ALU"听指挥"
如果只有硬件电路,ALU就是一堆死逻辑,是控制信号让它"活"起来。实验二里,控制信号一般用S2:S0(或者叫ALUOp)这样的编码来表示不同的操作。比如:
| S2 | S1 | S0 | 功能 |
|---|---|---|---|
| 000 | 加法(ADD) | ||
| 001 | 减法(SUB) | ||
| 010 | 与(AND) | ||
| 011 | 或(OR) | ||
| 100 | 异或(XOR) | ||
| 101 | 小于比较(SLT) |
控制信号的作用逻辑分成两条路:一条路控制数据的预处理(比如减法时需要对B取反并加1),另一条路控制输出端的多路选择器(MUX),决定最终输出哪个功能模块的结果。对于SLT功能,常见实现是:执行A - B,然后检查结果的符号位。如果符号位为1(且没有溢出),说明A < B,输出1;否则输出0。在无符号数的场景下,就直接看减法结果最高位的借位信号即可。
设计控制逻辑时的关键技巧是把SUB和SLT耦合在一起:两者都需要A - B,所以可以直接复用减法结果。很多人一开始把每个功能做成完全独立的计算路径,结果电路膨胀得非常臃肿,测试时还不好排查。理解"控制信号是资源调度员"这一角色,你的ALU设计才会真正优雅起来。
3. 在Logisim里从零搭建8位ALU:完整实操路径
3.1 模块化思路:先搭底层再拼上层
如果你的实验要求是8位ALU,我强烈建议不要一口气拖8个全加器然后连线,那样调试的时候你根本不知道哪根线出了问题。正确的做法是自底向上、模块化搭建。
我习惯把工程拆成四个子电路:
FA(1位全加器)CLA4(4位超前进位加法器)ALU8(8位ALU)PC_Counter(利用ALU实现的计数器,用于后续实验或验收加分项)
这种分层方式有三个好处:第一,每一层可以在隔离的环境里单独测试,确认无误后再往上拼装;第二,即使高层出了问题,也可以通过替换低层模块来快速定位是哪个模块的锅;第三,需要复用模块时可以直接拖一个子电路实例,避免重复劳动。
3.2 实现1位全加器与4位CLA组
在Logisim里新建一个FA子电路,输入引脚A、B、Cin,输出引脚Sum、Cout。用XOR门两个、AND门两个、OR门一个即可搭出上面提到的全加器电路。注意连线的颜色:输入引脚默认是绿色线,输出引脚默认是黄色线,如果你发现引脚连进去变成蓝色,那很可能是位宽不对,Logisim会自动把位宽不匹配的线显示为蓝色并提示错误。
接着新建CLA4子电路。它接收A[3:0]、B[3:0]、Cin,输出S[3:0]和Cout(即第4位的进位)。实现方式是用G、P生成逻辑先算出C1~C4,然后用四个三输入异或门计算S[i] = A[i] XOR B[i] XOR C[i]。这一步千万别偷懒用级联全加器充当CLA——那样虽然有进位输出,但没有体现超前进位的设计思想,验收老师一眼就能看出来。
为了验证CLA4的正确性,我会临时接几个常量输入测试:A=0x3,B=0x5,Cin=0时结果S=0x8,Cout=0;A=0x7,B=0x7,Cin=1时结果S=0xF,Cout=0(4位范围内);A=0x9,B=0x9,Cin=1时结果S=0x3,Cout=1。把这几组值测过之后,CLA4模块就可以放心使用了。
3.3 组装8位ALU:加法、减法、逻辑运算
有了两个CLA4组,组装8位加法器就是一句话的事:把低4位的CLA4的Cout接到高4位的CLA4的Cin,输入分别接A[3:0]、B[3:0]和A[7:4]、B[7:4]。这就是"组间串行"的体现:组内超前进位,组间串行连接。
接下来在这个8位加法器的基础上实现减法。减法A - B等价于A + (~B + 1)。因此需要加一个SubMode控制信号。当SubMode=0时,B保持原值,加法器执行加法;当SubMode=1时,B的每一位取反,同时加法器的Cin输入设为1,这样就完成了"取反加一"的补码操作。在Logisim里可以用NOT门阵列加MUX实现,也可以直接用受控取反器——即用XOR门,把B的每一位与SubMode异或,因为B XOR 0 = B,B XOR 1 = NOT B。这样连NOT门都省了。
AND、OR、XOR这三个逻辑运算相对独立,对A和B的每一位进行对应的门运算即可。最后用一个8位宽的MUX,把加法结果、减法结果、AND结果、OR结果、XOR结果汇总,由S2:S0选择哪一路作为最终输出。这里的技巧是减法结果可以直接复用加法器的输出路径,不需要给减法单独写一套逻辑——因为减法本质上就是加补码,所以只需要在输入B前面加XOR门,在Cin输入上加一条SubMode的线就行。
SLT功能稍微特殊一点。我在实验里是这么处理的:执行减法A - B,观察结果的最高位(符号位)。如果最高位是1,说明A < B(在有符号数补码表示下),输出0x01;否则输出0x00。这里要注意一个常见问题:减法溢出的情况下,符号位判断可能失效。为了稳妥,我加了一个溢出标志V = Cin_out XOR Cout_msb(即最高位进位与次高位进位不一致时表示溢出),把溢出修正逻辑也做进去了。实验原则上不要求处理溢出修正SLT,但做上之后不仅正确性更有保障,答辩时还能体现你的深入理解。
3.4 用ALU实现计数器:把PC递增逻辑落地
实验要求利用ALU实现计数器,这部分很有价值。计数器的逻辑很简单:在每一个时钟上升沿,PC = PC + 1(或者PC = PC + 4,取决于你的指令字长是1字节还是4字节)。如果用ALU来实现计数器,就是把寄存器的当前值连接到ALU的A输入,B输入端固定接常数1,控制信号S设为ADD,ALU的输出再送回寄存器的数据输入,同时用时钟信号控制寄存器的更新。
在Logisim里,这需要用到寄存器(Register)元件。寄存器的"时钟"输入端是三角形标记,数据输入(D)和输出(Q)的位宽要设为与PC一致。我在做这一步时踩了一个大坑:把寄存器的使能端(Enable)和时钟信号搞混了。Enable是电平触发,时钟是边沿触发;如果用Enable来控制计数,你会看到数值在电平为高时反复跳变,根本停不下来。正确的做法是让时钟只负责边沿触发,Enable始终为高(或者不使用Enable)。
这个计数器的应用场景可以在之后的实验三(CPU设计)里直接复用。程序计数器(PC)就是CPU一条指令执行完成后需要更新指令地址的部件,提前把计数器在实验二里做对,实验三会轻松很多。
3.5 封装成可复用子电路
模块全部做好后,建议新建一个顶层电路,把ALU8和PC_Counter分别拖进来,按照数据流摆好位置。给每个子电路实例加上有意义的标签(Label),比如主ALU、程序计数器。
在给子电路设置外观时,可以右键选择Edit Circuit Appearance,手动排列引脚位置。这一步虽然不能直接加分,但看起来整洁的电路图会让验收时老师对你的印象好不少。更重要的是,把引脚按功能分组排列(比如左边输入、右边输出、上面控制、下面时钟),能减少你后续布线时连线交叉的概率。
封装完成后,再用之前设计的测试向量整体跑一遍。我会把测试用例做成一张表格,每测完一组就顺手记录结果,这样验收时把表格打印出来,直接给老师展示,既省时间又显得专业。别小看这个细节,很多组做了满满一屏幕的电路,但问起来能否保证所有运算全部正确,自己都心虚。有记录、有验证,才是工程化的做事方式。
4. 验证环节:测试用例比功能本身更重要
4.1 测试的前置检查:连线检查清单
开始测试之前,先做一遍静态检查,能避免大量浪费时间。我总结了一张连线检查清单:
- 悬空检查:所有输入引脚有没有连接到固定电平(GND/VCC)?尤其是减法模式下的
Cin,有没有正确连到SubMode? - 位宽检查:有没有某个输入是8位宽,但只连了4位的线?Logisim里蓝色线通常表示位宽不匹配。
- 控制信号检查:MUX的地址输入有没有连反?是把
S2:S0接到了MUX的选择端,还是接到了数据端? - 输出冲突检查:有没有把两个输出引脚直接连在一起?如果有,Logisim会报红色"冲突"线,必须处理。
- 时钟检查:使用寄存器的电路,时钟引脚是否连到了时钟源,而不是手动改变电平?
做完这些检查再上电测试,几乎可以把低级错误排除掉80%。
4.2 构造边界用例:全0/全1/溢出/符号位
真正的测试考验的不是那些"一眼就知道结果对不对"的普通用例,而是边界情况。下面是我在实验二里用到的一组核心测试用例,含覆盖的测试意图:
| A | B | 操作 | 预期结果 | 测试意图 |
|---|---|---|---|---|
| 0x00 | 0x00 | ADD | 0x00, Z=1 | 全0操作,验证零标志 |
| 0xFF | 0x01 | ADD | 0x00, C=1 | 无符号溢出,验证进位 |
| 0x7F | 0x01 | ADD | 0x80, V=1 | 有符号溢出,验证符号溢出 |
| 0x0F | 0xF0 | OR | 0xFF | 位运算常规用例 |
| 0xFF | 0x0F | AND | 0x0F | 位运算掩码用例 |
| 0x5A | 0xA5 | XOR | 0xFF | 异或全1用例 |
| 0x03 | 0x05 | SUB | 0xFE, C=0 | 负数结果,验证补码减法 |
| 0x05 | 0x03 | SUB | 0x02 | 正数结果 |
| 0x02 | 0x05 | SLT | 0x01 | 小于关系成立 |
| 0x05 | 0x02 | SLT | 0x00 | 小于关系不成立 |
注意看,0x7F + 0x01产生符号位溢出,这在某些同学的设计里会被忽略,因为Verilog或Logisim的仿真并不会主动告诉你这里溢出了,你需要自己观察。别小看溢出,它关系到后续CPU实验里标志位寄存器的设计——如果现在不搞清楚加法器最高位进位与次高位进位的关系,后面做带标志位的运算器时会很痛苦。
测试时不要只盯着输出引脚。把Zero(零标志)和Carry(进位标志)也分别接到探针上。Zero标志的实现是用一个8输入NOR门,当所有输出位为0时,Zero=1。有多余精力的话,我还建议把Overflow(溢出标志)也做出来,尽管实验指导书不强制要求,但在做后续CPU实验时,条件跳转指令(如BEQ、BNE)往往需要用到这些状态位。
4.3 大招:借助Logisim的探针与位分线器调试
当你真的遇到输出不对时,不要慌,这里有几个Logisim调试的实用技巧。
- 探针(Probe):把探针接到中间信号线上,实时查看它的值。比如你可以单独查看CLA4里的
G0、P0,确认进位生成逻辑是否正常。 - 位分线器(Splitter):当你想单独看某个8位信号的某一位时,用位分线器把总线拆开。比如想检查减法结果的最高位,只需把
S[7]拉一根线出来接LED。 - 仿真步进(Step Simulation):在Logisim里可以单步推进仿真,每次时钟边沿看寄存器值的变化。这个功能在调试计数器时特别有用。我曾经用这个方法发现,我的计数器在某个时钟沿之后不是加1,而是直接跳到了0,后来定位到是寄存器复位端的复位信号在时钟沿前瞬间被意外拉高了。
- 分时修改输入:在组合逻辑测试时,用虚拟按钮(Button)作为输入信号,每按一次改一个值。别用Poke工具一直拖着引脚改值,那样容易手滑改错位。
我的一个个人习惯是:每测试完一组操作,就用系统自带的"标注工具"(Text)在旁边加一个注释,例如ADD-正常、SUB-溢出、SLT-通过。等到提交实验报告时,这些注释可以直接整理成一份漂亮的测试记录表,不需要再花时间重新设计。
5. 翻车记录:这些坑我替你踩过了
5.1 进位输入悬空的"薛定谔输出"
我第一次搭好8位加法器后,兴奋地接上输入,结果发现结果完全不对,而且每次仿真结果还不一样——有时输出是正常值,有时是一堆红色乱码。这种"薛定谔的输出"让我一度怀疑Logisim有bug。
后来我发现,问题出在加法器最低位的Cin引脚上。我没有把它接地(GND),导致它悬空。在数字逻辑中,悬空的输入引脚会被仿真软件当作"错误值"(X)处理。X在传播过程中会污染整个进位链,而且某些运算中X会被当作1,某些运算中会被当作0,所以结果会不稳定。
解决办法是给Cin接一个固定为0的常量。但更重要的是养成习惯:所有输入引脚必须有明确的驱动源,绝对不允许悬空。这条原则适用于所有数字电路设计。之后我用同样的思路排查了MUX未用到的数据输入端,把它们也接到了0,果然问题立刻消失。
5.2 位宽不一致导致的取值错乱
第二个大坑来自Logisim对位宽的宽容处理。我在布局时把寄存器输出设为8位,但在某个中间节点用了一个4位的常量,Logisim并不会报错,而是自动把高位截断。这个"自动截断"非常阴险,因为它不会在界面上显示明显的红色错误,导致你看到的输出偶尔对、偶尔错,很容易怀疑是电路逻辑本身的问题。
举一个具体的例子:我在实现SLT输出时,想输出8位的0x01或0x00。我直接把比较结果slt_cond(1位)接到了MUX的数据输入端,而MUX其他数据输入是8位。Logisim把1位信号自动扩展成8位时,如果用的是"零扩展",高位补0,结果是正确的;但如果某一条路径是"符号扩展",高位补1,那么在slt_cond=1时输出会变成0xFF而不是0x01。
这个问题的排查要诀是:把所有总线上都放探针,注意看是不是有哪根线的值出现0x1F、0xFF这类"意料之外又合情合理"的值。一旦发现位宽不一致,马上用位分线器去做显式的扩展,而不是依赖Logisim的默认行为。
5.3 减法实现时忽略了"加补码"的完整链条
我在实现减法时,一开始只对B进行了取反,忘记在Cin上加1。结果A - B变成了A + (~B),比正确结果少1。比如5 - 3,我算出来是1而不是2。这个问题单独看组合逻辑不好发现,因为减法结果取反加一的链条是完整的:取反→加一→相加。
后来我在纸面上逐步推导了补码减法的公式:A - B = A + (-B) = A + (~B + 1)。CPU能统一用加法器做减法,靠的就是这个数学恒等式。请务必确保你的设计里完整的"取反加一"链路都实现了,缺了任何一个环节,结果必然差1。我把它写在这个位置,是希望第一次做实验的学弟学妹别在同一个地方浪费两小时。
5.4 时序模块中时钟边沿的粗心
在接计数器模块时,我最初用了一个鼠标点击按钮(Button)来模拟时钟。用按钮点击看似方便,但有一个致命问题:Logisim中按钮按下和释放会产生两个边沿(下降沿和上升沿)。如果你的寄存器是上升沿触发,你可能只期望一次计数,但实际上按一次按钮可能造成两次计数。
为什么会这样?因为在真实电路中,按钮是一个机械开关,存在"抖动"现象,按下和释放会有多次电平波动。Logisim虽然不会模拟机械抖动,但按钮本身就是电平信号,会产生一个完整的脉冲(从0到1再从1到0),如果你的设计里同时存在上升沿和下降沿敏感的触发器,或者你在仿真步进中反复点击,就会多计数。
正确的做法是使用Logisim自带的时钟(Clock)元件,设置合适的分频频率,让时钟自动产生边沿。如果确实需要手动单步,我建议使用"仿真步进"按钮(Step Simulation),而不是多次点击按钮。这一点我在实验验收时亲测有效,老师看到计数器按一次稳步加一,比看到按一次跳两个数,印象分高下立判。
另外,时钟频率设得太快,肉眼看不到输出变化。我的经验是把频率调到1Hz左右(即每秒一个脉冲),这样LED的变化节奏刚好肉眼可辨,又不会因为太多脉冲导致仿真卡顿。
6. 实验之后的一些真实感想
做完实验二,我最直观的感受是:以前学计算机组成原理,总觉得它是"一堆寄存器和门电路的拼接",跟自己日常写的代码关系不大。但当你亲手用Carry Lookahead把8位加法器的速度提起来,看到一条减法指令在硬件层面转化成"取反加一"的三级操作,你会突然意识到——编程语言里的a + b,在底层并不是什么魔法,而是一排半导体开关按固定时序完成的一组动作。这种"从抽象到底层"的贯通感,是这门课最值钱的部分,也是实验二真正想带给你的东西。
如果你正准备做这个实验,我给你四个小建议:多做小模块、自测再组装、记录每个测试结果、看着时序图去理解现象。做完之后,建议再想一想:如果要扩展到32位,进位链应该怎么设计?如果要支持乘法和除法,你会在ALU里加哪些模块?如果你能把这些想明白,后面做实验三(完整CPU设计)时,就会觉得顺理成章,而不是再从头开始摸索。