
做多周期 MIPS CPU 这件事最有意思的地方不是最后能跑起来那几个指令而是你把“取指、译码、执行、访存、写回”这五个阶段真正拆开之后会发现 CPU 的“思考过程”竟然可以被几个逻辑门和状态寄存器描述得明明白白。这篇文章我想用 Logisim 带你完整走一遍硬布线多周期 MIPS CPU 的设计流程指令集定为 8 条lw、sw、add、sub、and、or、beq、j。适合正在做计算机组成原理课程设计的学生、准备复试的考研党以及单纯想搞明白“控制器到底是怎么硬布线出来”的爱好者。我会从选型思路讲起把数据通路的每一个关键部件、每一个多路选择器、每一根控制信号的来龙去脉都拆开说清楚然后重点讲硬布线控制器的状态机和真值表实现最后给出我实际调试时踩过的坑和排查方法。文章里也会附上方便直接照抄的机器码测试程序和电路连接表让你在 Logisim 里能真正把这个 CPU 搭出来、跑起来。1. 动手之前先把“多周期 硬布线 MIPS”这三件事想透1.1 为什么选多周期而不是单周期做过单周期 CPU 的人应该都有体会单周期方案最难受的地方在于时钟周期必须按最慢的那条指令来定。比如 lw 要经过取指、读寄存器、ALU 算地址、访存、写回五个环节其他指令可能只需要其中两三个环节但在单周期里所有人都必须等 lw 走完一个时钟周期才结束。这意味着 CPU 的实际速度被最长路径死死压住大部分时候电路都在空转等待。多周期方案把一条指令的执行拆成多个时钟周期每个周期只做一小步。好处有两个第一时钟周期可以做得更短因为每个周期内的组合逻辑路径短了第二硬件资源可以复用比如那一个 ALU 既要负责 PC4又要负责地址计算还要负责真正的运算只要在不同周期里把输入切换对就行不用像单周期那样复制多份部件。我当年做课设时选多周期还有一个非常现实的原因Logisim 里布线空间有限单周期数据通路把所有部件一次性并联起来连线密密麻麻容易看错。多周期虽然状态机复杂一点但数据通路清爽很多调试时定位问题也更快。1.2 硬布线控制器到底是什么控制器的作用就是给数据通路发号施令告诉各个部件在什么时刻做什么动作。实现控制器有两条主流路线微程序控制器和硬布线控制器。微程序控制器本质上是用一块 ROM 存控制信号每一条微指令对应一组控制信号执行时从头到尾读一遍就行。它灵活、修改方便但代价是“控制”这个动作本身也要靠存储器跑速度慢而且逻辑上有点绕。硬布线控制器则是直接用组合逻辑和状态寄存器把控制信号“算”出来不存在“读微指令”这一步。它的核心是一张真值表当前状态 指令类型 → 下一状态 所有控制信号。只要你把这张真值表捋清楚了剩下的就是搭门电路的问题。硬布线的优点是速度快、电路结构贴近真实 CPU缺点是设计一旦定了就很难改加一条指令几乎要重画一片逻辑。从课设的角度讲硬布线的“工作量”比微程序更集中在逻辑设计和真值表化简上而这恰恰是计算机组成原理课程最希望训练的内容。所以我在这篇文章里带你做硬布线版不是为了显得高级而是因为它能逼你把每一条控制信号的作用都搞清楚。1.3 8 条指令怎么选、怎么编码标题里写“8 条指令”这个数量不是随便凑的。我选的这 8 条刚好覆盖 MIPS 的三种指令格式和五种执行阶段R 型add、sub、and、or覆盖 ALU 运算和寄存器写回I 型lw、sw、beq覆盖访存、写回、分支转移J 型j覆盖无条件跳转R 型四条可以让你验证 ALU 的多种运算以及 funct 字段的译码lw 和 sw 验证访存地址计算和数据存储器读写beq 验证条件分支j 验证跳转地址拼接。整个指令集合基本覆盖了多周期 CPU 的所有工作模式。指令类型格式功能add rd, rs, rtR000000 rs rt rd 00000 100000加法sub rd, rs, rtR000000 rs rt rd 00000 100010减法and rd, rs, rtR000000 rs rt rd 00000 100100按位与or rd, rs, rtR000000 rs rt rd 00000 100101按位或lw rt, offset(rs)I100011 rs rt offset读存储器sw rt, offset(rs)I101011 rs rt offset写存储器beq rs, rt, offsetI000100 rs rt offset相等跳转j targetJ000010 target无条件跳转有一点必须提醒你MIPS 的标准字长是 32 位指令也是 32 位。我知道很多课程设计为了画图方便会把数据通路缩成 8 位甚至 4 位指令格式也跟着改。如果你只是想快速交差那没问题但如果你想真正理解 MIPS建议直接按 32 位来做。Logisim 的组件位宽是完全支持 32 位的画起来并不会比 8 位麻烦太多只是屏幕上看着宽一点而已。我先按 32 位讲如果你确实想用 16 位做验证版后半部分我也会提到怎么缩放。2. 基础部件在 Logisim 里把这些零件先做扎实2.1 工具版本选择与工程规划做这种 CPU 设计推荐使用 Logisim Evolution 版本。相比老牌的 2.7.xEvolution 版本对 RAM/ROM 的初始化文件支持更好元件的引脚命名更清晰而且自带隧道Tunnel功能可以大幅减少交叉线。中文汉化版本也能用但有些菜单翻译不一样你如果照着教程对不上界面优先检查是不是版本差异。新建工程之后我习惯把整个设计拆成几个子电路ALU、寄存器堆、符号扩展、控制器、顶层数据通路。每个子电路做好封装最后在顶层只需要拉线和放多路选择器。这样做的好处是调试时可以单独测试某个子电路不用每次都被其他部分干扰。子电路的引脚设置要注意方向输入引脚放在电路左边输出放在右边位宽要跟实际数据宽度一致。Logisim 里引脚没有严格的“必须左右放”的规则但养成这个习惯以后封装出来的子电路在使用时特别好认。2.2 ALU 与标志位加减与、或、零标志ALU 是多周期 CPU 里所有算术逻辑操作的核心它需要支持四种运算加、减、与、或。实现方式不复杂用加法器做加法减法用“取反加一”的思路也就是把 B 输入取反再置进位为 1与和或直接用门电路最后用一个多路选择器根据控制信号选出结果。ALU 的控制信号我这里设计成一个 2 位的 ALUOp加上 R 型指令的 funct 字段共同决定具体操作。ALUOp 的编码可以这样定义00 表示加法用于取指阶段的 PC4、S1 阶段的分支地址计算、S2 阶段的 lw/sw 地址计算01 表示减法用于 beq 比较10 表示由 funct 决定操作用于 R 型指令。在 ALU 内部再加一层小的组合逻辑把 ALUOp 和 funct 转换成最终的选择线。零标志 Z 也要在 ALU 里做出来当 ALU 的 32 位输出全为 0 时Z 输出 1。beq 能不能跳转就看这条零标志。我习惯用 Logisim 的“比较器”组件把结果和常数 0 比较用比较器输出更直观但如果你想让电路更“教学化”也可以自己搭一个 32 输入或门再取反效果一样。这里有个小细节你必须注意ALU 是一个纯组合逻辑电路它本身不保存任何数据。多周期 CPU 里 ALU 的输出必须在执行周期结束后被一个叫 ALUOut 的寄存器锁存否则下一个周期 ALU 输入一变上一步的结果就丢了。很多新手第一次搭多周期 CPU 跑不出正确结果问题往往就出在这里。2.3 寄存器堆两个读端口、一个写端口怎么搭MIPS 有 32 个通用寄存器寄存器地址是 5 位。寄存器堆要支持在同一时刻读出两个寄存器的值rs 和 rt并且支持写入一个寄存器rd 或 rt由指令决定。这在 Logisim 里可以有两种实现路线。路线一直接用 Logisim 自带的高级组件“寄存器文件”。右键组件可以设置寄存器数量和数据位宽它会自动生成两个读端口和一个写端口使用非常方便。缺点是你对内部结构没有控制权课程答辩时如果老师问“寄存器堆的原理是什么”你得能答上来。路线二自己用寄存器组件和译码器搭。写端口用 5 位地址接一个 5 到 32 译码器译码输出和 RegWrite 信号相与再分别接到 32 个寄存器的 Load 端。两个读端口分别用 32 选 1 的多路选择器根据 rs 和 rt 地址选出对应的寄存器值。这条路线的电路面积比较大但它把原理展示得明明白白答辩时非常好讲。我的建议是如果你想稳先用路线一快速把 CPU 跑通拿到一个可用的整体结果之后再用路线二替换成手搭寄存器堆。反正寄存器堆是做成了子电路替换顶层不需要改任何其他部分。2.4 程序存储器和数据存储器ROM 与 RAM 的初始化程序存储器用只读存储器 ROM数据存储器用 RAM两者分开不要混用。多周期数据通路里取指阶段用 PC 作为地址去 ROM 取指令访存阶段用 ALUOut 作为地址去 RAM 读写数据。如果合并成一个存储器虽然靠 IorD 选择器也能做但会让电路更难懂而且 RAM 和 ROM 的初始化方式不同容易出错。ROM 在 Logisim 里右键可以打开“编辑内容”或“载入镜像”。我建议用 Hex 文件的方式载入机器码因为手写 32 位数据在表格里输起来太费劲。Hex 文件的格式很简单每行写一个 32 位十六进制数比如 8C100000。ROM 的位宽需要设置成 32 位地址位宽根据你的程序大小来定一般设置 8 位地址就够用了256 字节能放 64 条指令。RAM 的初始化方法和 ROM 类似但有一点要特别注意Logisim 的 RAM 可以在编辑内容里直接写入初始数据也可以载入镜像文件。做测试时我习惯在数据存储器地址 0 放一个数、地址 4 放另一个数这样程序执行 lw 之后马上能看到效果不用靠猜测验证。3. 数据通路把零件连成一个能跑多周期的整体3.1 多周期数据通路的核心思路多周期数据通路的设计核心是“资源共享”。整个 CPU 只有一个 ALU、一个存储器、一个寄存器堆但每个部件在不同周期服务不同的任务。为了让这些共享部件知道当前为谁服务就需要在输入端加入多路选择器由控制信号决定选哪条数据来源。我把多周期数据通路的骨架按“数据流方向”拆成几段来理解取指段PC 送给 ROMROM 输出送到 IR 寄存器和下一条地址计算逻辑译码段IR 中的 rs、rt 字段送寄存器堆读端口A、B 锁存器保存读出的数据同时 PC 与符号扩展并左移 2 位后的立即数相加结果存入 ALUOut为 beq 做准备执行段根据指令类型选择 ALU 输入运算结果存入 ALUOut访存段用 ALUOut 作为地址访问 RAM写回段从 ALUOut 或 MDR 中选择数据写入寄存器堆这套数据通路的连线关系我可以整理成一张表。你照着表连基本不会乱信号名选择方式用途IorD0 选 PC1 选 ALUOut决定存储器地址来源RegDst0 选 rt1 选 rd决定写回寄存器编号MemToReg0 选 ALUOut1 选 MDR决定写回数据来源ALUSrcA0 选 PC1 选 A 寄存器ALU 的 A 输入来源ALUSrcB00 选 B01 选 410 选扩展立即数11 选左移 2 位后的立即数ALU 的 B 输入来源PCSource00 选 ALUOut 的 PC4 结果01 选 ALUOut 的分支目标10 选跳转地址PC 的写入来源3.2 取指阶段和数据暂存元件取指阶段发生在 S0 状态。此时 PC 通过 IorD 选择器被选为 ROM 地址ROM 输出指令写入 IR 寄存器同时 PC 与常数 4 相加结果写入 PC实现 PC4。这里有个非常重要的操作PC 的更新必须在取指周期完成所以 PCWrite 信号要在这个周期为 1其他周期为 0。在 Logisim 中PC 我直接用寄存器组件实现。这个寄存器有一个 Load 使能端只有 PCWrite 为 1 时时钟上升沿才会写入。时钟信号不能手动用与门去“门控”因为这样会产生毛刺导致寄存器偶然写入错误数据。正确的做法是让所有寄存器的时钟都接全局时钟用 Load 端控制是否写入。除 PC 之外还需要准备几个暂存寄存器IR 锁存指令MDR 锁存从数据存储器读出的数据A 和 B 锁存寄存器堆的读出值ALUOut 锁存 ALU 的计算结果。它们全部由时钟边沿触发在对应周期的上升沿锁存新值。为什么要这么多锁存器因为多周期的每一步在下一个周期开始时上一步的结果必须还在否则整个流水就断了。比如 S2 算出的 lw 访存地址放在 ALUOut 里S3 直接用 ALUOut 作为 RAM 地址这期间 ALU 可能已经在为其他事情计算了如果没有 ALUOut 保护地址早就被覆盖。3.3 符号扩展与跳转地址拼接MIPS 的 16 位立即数需要符号扩展成 32 位。Logisim 有一个便捷组件叫“扩展器”可以指定输入输出位宽和扩展方式。选择“符号扩展”模式输入 16 位立即数输出就是 32 位。如果你要手动搭也很简单把第 15 位复制 16 份作为高 16 位再与低 16 位拼接。分支地址计算发生在 S1 状态把符号扩展后的立即数左移 2 位再和当前 PC 相加结果存入 ALUOut。左移 2 位在 Logisim 里直接用“左移位器”组件固定移位量为 2 即可。等到 beq 指令的 S2 状态如果零标志为 1就把 ALUOut 写进 PC实现跳转。跳转指令 j 的地址拼接稍微特殊一点指令中 26 位地址左移 2 位后低 28 位是目标地址的低 28 位高 4 位来自当前 PC 的高 4 位。对课设级别的程序来说程序都放在低地址区域高 4 位保持 0 就能工作。所以你可以在 Logisim 里直接把信号 0 和左移后的 26 位地址拼接凑成 32 位目标地址。如果后续想加载到高地址再改成拼接 PC 的高 4 位。4. 硬布线控制器状态机与真值表才是主角4.1 五个状态怎么划分、怎么跳转多周期 CPU 的五步操作对应五个状态我沿用的是经典教材的状态划分方式S0取指。所有指令都在这里取指令并计算 PC4S1译码。读取寄存器堆到 A、B计算分支目标地址存入 ALUOutS2执行。R 型算数运算、lw/sw 地址计算、beq 比较、j 跳转S3访存。只有 lw 和 sw 会进入S4写回。只有 R 型和 lw 会进入状态机的跳转规则可以严格归纳成一张转移表当前状态转移条件下一状态S0无条件S1S1opcode 为 lw/sw/beq/R型S2S1opcode 为 jS2在 S2 完成跳转后回 S0S2opcode 为 R型S4S2opcode 为 lwS3S2opcode 为 swS3S2opcode 为 beqS0S2opcode 为 jS0S3opcode 为 lwS4S3opcode 为 swS0S4无条件S0注意 j 指令这里我为了保持状态机整齐把它放在 S2 阶段执行跳转然后再回到 S0。你也可以在 S1 阶段直接完成跳转并回到 S0少用一个周期但对状态机的整齐性有影响。课设里两者都能过我建议按表格里的方案来因为 S1 到 S2 是无条件转移的控制器逻辑简单很多。4.2 状态编码与次态逻辑实现5 个状态至少需要 3 位状态寄存器来编码。我用 S0000、S1001、S2010、S3011、S4100。在 Logisim 里状态寄存器就是一个 3 位寄存器时钟接全局时钟复位信号接寄存器的 CLR 端让它在初始化时回到 000。次态逻辑是硬布线控制器的第一个难点。它的输入是当前状态的 3 位编码和指令的 opcode 字段输出是下一状态的 3 位编码。你可以把上面那两张状态转移表展开成真值表然后用 Karnaugh 图化简也可以直接交给 Logisim 的组合逻辑分析功能去生成电路。我推荐一个非常省事的方法在 Logisim 的“组合逻辑分析”窗口里把次态逻辑的真值表逐行输入然后让它自动生成电路。生成之后把电路接在状态寄存器的后面次态逻辑就完成了。这样虽然不是你亲手化简的布尔表达式但真值表是你自己填的原理上也说得清楚课设答辩时你至少能解释每一行的含义。如果你想让门电路更精简可以自己在纸上化简。以次态最低位为例状态 000 无条件到 001状态 011 到 100状态 100 到 000再结合其他若干条转移能整理出一个布尔表达式。不过说实话对于课设而言用组合逻辑分析工具生成电路是效率最高的做法等整个 CPU 跑通了再慢慢手工化简也不迟。4.3 控制信号真值表一张表搞定所有输出控制信号的真值表是整个硬布线控制器最核心的产物。我把自己用到的 12 个控制信号列在下面。X 表示“无所谓”工程上接 0 通常也不影响功能。控制信号S0S1S2(R型)S2(lw)S2(sw)S2(beq)S2(j)S3(lw)S3(sw)S4(R型)S4(lw)PCWrite10000110000PCWriteCond00000100000IorD0XXXXXX11XXMemRead10000001000MemWrite00000000100MemToRegXXXXXXXXX01IRWrite10000000000RegDstXXXXXXXXX10RegWrite00000000011ALUSrcA011111XXXXXALUSrcB011100101000XXXXXXXXXXALUOp000010000001XXXXXXXXXXPCSource00XXXXXXXX0110XXXXXXXX这里最容易被忽略的地方是 ALUSrcB。在 S0 阶段要选常数 4 实现 PC4在 S1 阶段要选左移 2 位后的立即数实现分支目标计算在 S2 阶段R 型和 beq 要选 B 寄存器而 lw/sw 要选符号扩展后的立即数。所以 ALUSrcB 不是只由状态决定还要由指令类型决定。PCWrite 的控制也要注意取指阶段需要把 PC4 的结果写回 PC所以 PCWrite 在 S0 为 1beq 跳转在 S2 无条件写 PC所以 S2 且 opcode 为 beq 时 PCWrite 为 1j 跳转同理。而 PCWriteCond 是“条件写 PC”的信号它在 beq 的 S2 阶段与 ALU 零标志相与后再并联到 PC 的写控制上。换句话说PC 的最终写使能是 PCWrite 或上“PCWriteCond 与零标志”。4.4 用 ROM 偷懒行不行我的建议很多同学做到控制器这一步会想到一个“曲线救国”的办法用 ROM 存控制信号地址输入就是当前状态和 opcode输出就是控制信号。这个方法在工程上完全可行但在课设里要慎重。如果你用 ROM 实现控制器实际上已经变成了微程序控制的思路硬布线就名存实亡了。答辩时老师一眼就能看出来。我的建议是核心状态机和控制信号必须用门电路生成可以用组合逻辑分析工具辅助化简但最终电路里不能出现 ROM 作为控制信号来源。这个原则你守住项目性质就不会跑偏。5. 联调与测试从单条指令到完整程序5.1 先验证取指周期是否正常CPU 整体搭好之后第一步不是急着跑完整程序而是先验证 S0 取指周期是否正常。方法很简单在 ROM 里随便放一条指令然后手动触发一次时钟脉冲观察 IR 寄存器的值是否等于 ROM 中那条指令观察 PC 是否加 4。我一般会在 IR 和 PC 后面各接一个十六进制显示器这样一眼就能看到当前值。如果你发现时钟触发后 IR 没有变化优先检查 IRWrite 信号是否为 1如果 PC 没有加 4检查 PCWrite 和 ALUSrcB 的选择是否正确。5.2 单条指令逐个过R 型、lw/sw、beq、j取指正常后开始验证第一条指令。我建议从 add 这类 R 型指令开始因为它最短只需经历 S0、S1、S2、S4 四个周期。先在寄存器堆里给 rs 和 rt 准备两个数然后执行指令看 rd 是否为两数之和。如果不对用探针看 S2 之后 ALUOut 的值就能定位是 ALU 错了还是写回阶段的选择错了。lw 和 sw 要一起测。先在数据存储器地址 0 和地址 4 初始化两个数让程序先执行 lw 读取再执行 sw 写回。观察 MDR 在 S3 锁存的值是否正确观察数据存储器地址 8 是否在 sw 之后变成期望值。MDR 锁存是新手比较容易忽略的一步在 S3 阶段RAM 的输出必须被 MDR 捕获如果 MemRead 信号在 S3 不为 1MDR 就会一直在变化。beq 的验证建议用一个“故意相等”和“故意不等”的用例分别测。相等时看 PC 是否被 ALUOut 中的分支目标地址覆盖不等时看 PC 是否继续走 PC4。j 的验证最简单看 PC 是否被拼出来的跳转地址覆盖。5.3 完整测试程序机器码手把手换算这里我给你一个可以直接用的 8 条指令测试程序。它从数据存储器读两个数到 $s0 和 $s1做加法、减法、与、或把和写回地址 8然后 beq 判断两个数是否相等最后 j 跳回开头。为了方便验证 beq我让 $s0 和 $s1 相等所以 beq 会跳过中间两条指令直接去执行地址 24 的写回指令。地址汇编机器码0x00lw $s0, 0($zero)0x8C1000000x04lw $s1, 4($zero)0x8C1100040x08add $s2, $s0, $s10x021190200x0Csub $s3, $s0, $s10x021198220x10and $s4, $s0, $s10x0211A0240x14or $s5, $s0, $s10x0211A8250x18sw $s2, 8($zero)0xAE1200080x1Cbeq $s0, $s1, 20x121000020x20j 0x000x08000000我来示范一个换算过程。看 add $s2, $s0, $s1opcode 是 000000rs 是 $s0 即 10000rt 是 $s1 即 10001rd 是 $s2 即 10010shamt 是 00000funct 是 100000。拼起来是 000000 10000 10001 10010 00000 100000按字节切开就是 0000 0010 0001 0001 1001 0000 0010 0000即 0x02119020。其他指令的换算逻辑完全一样。数据存储器初始化时在地址 0 放一个数比如 10在地址 4 也放 10。这样 beq 条件成立程序会跳过地址 20 的 j 指令直接执行地址 24 的 sw 写回。注意 beq 的偏移量是相对 PC4 的指令数而不是字节数。beq 在地址 0x1CPC4 是 0x20偏移 2 表示跳到 0x20 2×4 0x28。0x28 正好是地址 24也就是 sw 指令所在位置。你会看到 beq 的机器码低 16 位是 0000000000000010偏移就是 2。5.4 运行与观察时钟步进Logisim 的模拟工具栏里有一个时钟组件默认是自动脉冲。我强烈建议你在调试阶段把自动脉冲关掉用“单击触发”的方式手动走一个周期。每按一次观察一次状态寄存器的值和控制信号的变化这样能清晰看到 CPU 一步步走完一条指令。等确认一条指令能走通后再打开自动脉冲跑完整程序。自动脉冲的频率建议从 1Hz 或 2Hz 开始太快了你根本看不清中间信号只看到最终结果。频率可以慢慢调高但 Logisim 本身不是性能模拟器调太高也没意义2Hz 到 10Hz 足够教学演示了。6. 调试实录我踩过的坑和排错技巧6.1 三个容易让 CPU 整体崩溃的经典坑第一个坑是寄存器堆写使能没控制好。寄存器堆的写端口如果在 RegWrite 为 0 时也写入数据就会乱七八糟。检查方法很简单把所有寄存器的 Load 端都接上 RegWrite 与写地址译码结果的与门不要漏掉任何一条线。第二个坑是 PC 的更新时机。多周期里 PC 只能在 S0 更新其他周期都要锁住。如果 PCWrite 信号没有设计好比如在 S1 或 S2 也为 1PC 就会在指令执行中间跳变直接导致整个程序乱套。这也是为什么我前面强调 PC 的 Load 端必须由一个组合逻辑统一控制而不是简单接个常高电平。第三个坑是 MDR 锁存缺失。很多人在 S3 访存阶段直接让 RAM 的输出连到写回数据线的 MUX 上忘了中间要过一个 MDR 寄存器。这样在 S4 写回阶段如果 RAM 输入地址变化输出数据也跟着变写回的数据就错了。6.2 常见问题速查表现象可能原因排查方法IR 一直不更新IRWrite 没在 S0 置 1或时钟没接上看 IR 的 Load 端信号手动触发一次时钟PC 每周期都乱跳PCWrite 没按状态限制用探针看 PCWrite 在每个状态的值R 型运算结果正确但寄存器不写入RegWrite 或 RegDst 不对检查 S4 阶段 RegDst 和 RegWritelw 读出来是 0数据存储器没初始化或 MDR 没锁存检查 RAM 初始内容和 MemRead/MDR 逻辑beq 跳不到目标S1 阶段分支目标没算对或零标志没连上看 ALUOut 在 S1 结束后的值看 ALU 的 Z 输出程序运行到一半状态机卡死有未处理的状态组合检查次态逻辑是否存在无定义跳转自动脉冲一开就乱信号竞争或时钟频率太高先手动步进验证再降低自动脉冲频率6.3 快速定位信号的实用技巧Logisim 里我最常用的调试手段是探针和十六进制显示器。探针可以放在任何导线上随时看信号值十六进制显示器适合接在总线末端比如 IR、PC、ALUOut、MDR 这些关键寄存器输出。另一个技巧是“分段验证”。不要等到整个 CPU 搭完再调试而是每连完一小段就测试一小段。比如先测 ALU 单独功能再测寄存器堆读写再测符号扩展最后再连起来。实在不行可以把控制器的输出信号手动设成固定值强制数据通路某一时刻做某件事看它会不会按预期走。用这种方法很容易排除是数据通路的问题还是控制器的问题。7. 还有几个经验想单独拿出来说做这个项目的时候我自己最大的体会是别急着一次把电路全画完。先画控制器和状态机的真值表把每一个信号的时序想清楚再动手连线效率比边画边想要高得多。很多同学上来就拉线结果线拉了一堆状态机逻辑没想明白最后全推倒重来。如果你做的是课设答辩时常被问的问题无非是这几个为什么 PCWrite 和 PCWriteCond 要分开为什么 ALUOut 不能直接用 ALU 的输出beq 分支目标为什么在 S1 算而不是在 S2 才算这些问题在这篇文章里其实都已经给了答案你只要把自己亲手搭的电路说明白就没有过不去的坎。后续想扩展的话可以在这个基础上去掉硬布线控制器换成微程序或者把单周期改成五级流水线再深入一点可以加入中断和异常处理机制。多周期 CPU 是所有复杂控制逻辑的地基把这块打扎实了后面学流水线就像看地图一样清晰。如果你在搭的过程中遇到了什么奇怪的问题或者发现哪个状态跳转和我写的不太一样欢迎把你的 Logisim 截图和现象描述发出来我们一起看看问题到底出在哪个周期。