ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

计算机组成原理实验二:从零搭建8位ALU与进位链

2026/10/4 13:51:47 拓冰建站 浏览量
计算机组成原理实验二:从零搭建8位ALU与进位链 计算机组成原理这门课对软件学院的学生来说一直有种最熟悉的陌生人的感觉。理论课上讲的流水线、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在调试时非常直观。第四步预设测试向量在设计之前就把测试用例想好。比如A0x0F, B0xF0, ADD结果应该是什么A0x7F, B0x01, 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能干哪些事算术、逻辑、移位实验二的核心模块是ALUArithmetic Logic Unit算术逻辑单元。它就像一个小型计算中心输入两个操作数根据控制信号决定执行哪种运算然后把结果送出去。经典的ALU操作可以分成三类算术运算加法ADD、减法SUB、加一INC、减一DEC、比较SLTSet 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 AdderFA。全加器有三个输入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 AdderRCA。它的优点是结构简单缺点也很致命高位的结果必须等低位进位逐级传播过来才能确定。对于8位的加法器最坏情况下需要等待8个全加器的进位延迟如果是32位延迟会更大。这就是为什么会出现组间串行进位这个优化方案——它的目的就是缩短进位传播路径。2.3 组间串行进位从串行到高性能的过渡热搜词里的组间串行进位Carry Lookahead也叫超前进位但组间串行这个说法更细致地描述了一种层次化设计方式是实验二的重要得分点。在4位超前进位加法器CLA中我们可以根据输入直接算出每一位的进位而不需要等待低位逐级进位。超前进位依赖于两个信号生成信号GGenerate和传播信号PPropagate。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这样的编码来表示不同的操作。比如S2S1S0功能000加法ADD001减法SUB010与AND011或OR100异或XOR101小于比较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个全加器然后连线那样调试的时候你根本不知道哪根线出了问题。正确的做法是自底向上、模块化搭建。我习惯把工程拆成四个子电路FA1位全加器CLA44位超前进位加法器ALU88位ALUPC_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的正确性我会临时接几个常量输入测试A0x3B0x5Cin0时结果S0x8Cout0A0x7B0x7Cin1时结果S0xFCout04位范围内A0x9B0x9Cin1时结果S0x3Cout1。把这几组值测过之后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控制信号。当SubMode0时B保持原值加法器执行加法当SubMode1时B的每一位取反同时加法器的Cin输入设为1这样就完成了取反加一的补码操作。在Logisim里可以用NOT门阵列加MUX实现也可以直接用受控取反器——即用XOR门把B的每一位与SubMode异或因为B XOR 0 BB 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设为ADDALU的输出再送回寄存器的数据输入同时用时钟信号控制寄存器的更新。在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/溢出/符号位真正的测试考验的不是那些一眼就知道结果对不对的普通用例而是边界情况。下面是我在实验二里用到的一组核心测试用例含覆盖的测试意图AB操作预期结果测试意图0x000x00ADD0x00, Z1全0操作验证零标志0xFF0x01ADD0x00, C1无符号溢出验证进位0x7F0x01ADD0x80, V1有符号溢出验证符号溢出0x0F0xF0OR0xFF位运算常规用例0xFF0x0FAND0x0F位运算掩码用例0x5A0xA5XOR0xFF异或全1用例0x030x05SUB0xFE, C0负数结果验证补码减法0x050x03SUB0x02正数结果0x020x05SLT0x01小于关系成立0x050x02SLT0x00小于关系不成立注意看0x7F 0x01产生符号位溢出这在某些同学的设计里会被忽略因为Verilog或Logisim的仿真并不会主动告诉你这里溢出了你需要自己观察。别小看溢出它关系到后续CPU实验里标志位寄存器的设计——如果现在不搞清楚加法器最高位进位与次高位进位的关系后面做带标志位的运算器时会很痛苦。测试时不要只盯着输出引脚。把Zero零标志和Carry进位标志也分别接到探针上。Zero标志的实现是用一个8输入NOR门当所有输出位为0时Zero1。有多余精力的话我还建议把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_cond1位接到了MUX的数据输入端而MUX其他数据输入是8位。Logisim把1位信号自动扩展成8位时如果用的是零扩展高位补0结果是正确的但如果某一条路径是符号扩展高位补1那么在slt_cond1时输出会变成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设计时就会觉得顺理成章而不是再从头开始摸索。