Logisim搭建8指令多周期MIPS CPU教程:从状态机到数据通路 提到华中科技大学的计算机组成原理课程设计绕不开一道经典任务用 Logisim 搭一台能跑通 8 条指令的多周期 MIPS CPU。这台 CPU 不算复杂但非常考验对数据通路、控制信号、状态机的综合理解。很多同学第一次看到题目会懵Logisim 不是画逻辑电路的工具吗CPU 那么大怎么搭别慌这篇教程就是面向新手从指令集设计、数据通路搭建、状态机控制到测试代码带你一步一步搭出一台能跑通的 8 指令多周期 MIPS CPU。做这个实验之前我自己的状态是MIPS 指令格式背得滚瓜烂熟但真让我说清楚“多周期 CPU 为什么需要状态机”“IR、A、B、ALUOut 这些寄存器分别在什么时刻被写”其实是一团浆糊。当时我照着教程敲完整个电路之后仿真各种不跳转、寄存器乱写排查了两天。后来发现几乎所有问题都出在状态机的“信号时序”上。所以这篇文章我想把设计思路、状态机真值表、Logisim 搭建细节、排错经验一次性讲透适合正在做华科计组实验、以及任何正在用 Logisim 做 MIPS CPU 课设的同学参考。1. 整体设计与指令集规划做 CPU 之前一定要先定指令集。指令集没定好后面数据通路全是乱的。我选的是最常见的 8 条指令组合覆盖 R 型、I 型、J 型三类格式也覆盖了取数、存数、加减、逻辑、分支、跳转这些 CPU 必备操作。1.1 8条指令怎么选我实现的 8 条指令如下指令类型功能说明lwI型从内存读字到寄存器访存读swI型把寄存器写到内存访存写addR型寄存器加法算术运算subR型寄存器减法算术运算andR型寄存器按位与逻辑运算oriI型寄存器与立即数按位或逻辑/立即数运算beqI型相等则分支条件跳转jJ型无条件跳转无条件跳转这 8 条指令选得比较讲究。首先 lw/sw 是访存指令多周期 CPU 最典型的特点就是访存和运算分步完成没有 lw/sw 就体现不出多周期的意义add/sub 覆盖算术and/ori 覆盖逻辑运算其中 ori 是 I 型立即数运算用于测试“零扩展”这是一个很容易踩坑的点beq 和 j 覆盖分支和跳转用于测状态机里的 PC 更新逻辑。实际实验要求可能略有差别比如有的版本要求用加立即数 addi 而不是 ori。这时只需要把指令编码和扩展方式做调整电路结构完全不用改。我建议优先选择 ori因为 ori 的零扩展和 lw/sw/beq 的符号扩展形成对比更能帮你理解 MIPS 的立即数扩展规则。1.2 多周期CPU的结构思路多周期 CPU 和单周期 CPU 最大的区别是单周期一条指令在一个时钟周期内完成硬件上所有功能部件必须同时准备好多周期把“取指、译码、执行、访存、写回”拆成多个时钟周期每个周期只做一件事。这样做的好处很明显硬件可以复用。比如 ALU 第一周期算 PC4第二周期算分支目标地址第三周期算 R 型指令结果第四周期算访存地址。同一个 ALU不同周期干不同的事靠的就是状态机来切换 MUX 的选通信号。多周期 CPU 的核心结构可以拆成这几块程序计数器 PC保存当前指令地址S0 状态自动加 4指令寄存器 IR保存当前正在执行的指令寄存器堆 RegFile32 个 32 位通用寄存器提供一个写口两个读口ALU 模块负责算术逻辑运算输出 Zero 标志A 寄存器、B 寄存器存放从寄存器堆读出的两个操作数ALUOut 寄存器存放 ALU 的计算结果MDR 寄存器存放从内存读出的数据数据存储器 DataRAM存放程序运行中的变量数据指令存储器 InstROM存放机器码控制器 Controller核心状态机输出所有控制信号教材上严格的多周期 CPU 通常用一个统一存储器指令和数据都放在同一个 RAM 里通过 IorD 信号选择地址来自 PC 还是 ALUOut。但这个实验在 Logisim 里我建议先用分离的 ROM 和 RAMROM 放指令RAM 放数据。这样连线更直观、排错更容易而且不影响你对多周期控制逻辑的理解。IorD 信号我可以保留在控制器里但暂时不接入数据通路。1.3 Logisim里的设计约定开始搭建之前先把 Logisim 里的几个关键约定说清楚否则后面仿真会出现各种莫名其妙的问题。全局时钟用一个 Clock 组件作为整个 CPU 的唯一时钟源接到 PC、IR、A、B、ALUOut、MDR、状态寄存器、寄存器堆的时钟输入端。所有时序部件都按同一个时钟沿更新。时钟极性Logisim 默认时钟上升沿触发。建议所有寄存器都用上升沿避免有的上升沿有的下降沿导致时序混乱。复位信号给 PC 和状态机加一个全局复位。Logisim 的 Register 组件左上角带一个 Reset 端口连一个按钮或者全局 Reset 引脚仿真前先按一下归零。否则 PC 初始值是随机未知值ROM 直接取到乱码。数据位宽CPU 是 32 位的所有数据总线、寄存器、ALU 都设为 32 位数据宽度PC 输出、ROM 地址线也都是 32 位。立即数扩展lw/sw/beq 用符号扩展ori 用零扩展。Logisim 里有现成的 Sign Extender 组件零扩展可以自己用 16 位输入接高 16 位常量 0 的方式实现。2. 状态机CPU的“大脑”多周期 CPU 最核心、也最容易出错的就是控制器状态机。控制器负责在每个时钟周期输出正确的控制信号指挥数据通路干活。我把它拆成五个状态IF取指、ID译码、EX执行、MEM访存、WB写回。下面逐一讲清楚每个状态做什么、产生什么控制信号、往哪个状态跳。2.1 五个状态的微操作详解S0 状态IF取指这是每条指令的第一步。ROM 按当前 PC 地址输出指令IR 在时钟沿锁存这条指令同时 PC 自增 4指向下一条指令。这一步和指令类型无关所有指令都做同样的操作。S1 状态ID译码从寄存器堆读取 rs 和 rt 对应的两个寄存器的值分别存到 A、B 寄存器同时用 ALU 计算分支目标地址PC 符号扩展立即数左移 2 位结果存到 ALUOut。这里注意虽然当前指令可能是 add 也可能是 beq但我们提前把分支地址算好备用多周期 CPU 就喜欢“提前算、后面看情况用”。S2 状态EX执行这是整条指令的分岔路口。根据 opcode控制器决定当前指令要干什么R 型指令add/sub/andALU 用 A 和 B 做运算结果写进 ALUOut。下一步跳到 WB。lw/swALU 计算地址 A 符号扩展立即数结果写进 ALUOut。下一步跳到 MEM。beqALU 用 A 和 B 做减法输出 Zero 标志。如果 Zero1说明两个操作数相等PC 从 ALUOutS1 算好的分支地址加载如果 Zero0PC 已经在上一个取指周期加过 4自然顺序执行。无论是否跳转下一步都回到 IF。jPC 直接加载跳转目标地址 {PC[31:28], 指令低26位, 2b00}。下一步回到 IF。S3 状态MEM访存这个状态只对 lw 和 sw 有意义。lw以 ALUOut 为地址读取数据存储器数据存入 MDR。下一步跳到 WB。sw把 B 寄存器的值写入以 ALUOut 为地址的数据存储器。下一步回到 IF。R 型指令不会进入这个状态。S4 状态WB写回把结果写回寄存器堆。R 型指令把 ALUOut 写回 rd 号寄存器。lw把 MDR 写回 rt 号寄存器。写回完成后回到 IF开始下一条指令。这里有一个核心思想要和你说透多周期 CPU 之所以按状态分步是因为每个周期只能做“一小件”事。比如 lw 需要先取指再读寄存器再算地址再访存再写回一条指令要 5 个周期而 add 只需要取指、译码、执行、写回 4 个周期。状态机就是在安排这条流水线让不同指令在不同周期干不同事。2.2 状态转移表与转移条件我用二进制编码五个状态S0000S1001S2010S3011S4100。状态转移规则很简单当前状态转移条件下一状态S0 (000)无条件S1S1 (001)无条件S2S2 (010)opcode R型 (000000)S4S2 (010)opcode lw (100011) 或 sw (101011)S3S2 (010)opcode beq (000100)S0S2 (010)opcode j (000010)S0S3 (011)当前指令是 lwS4S3 (011)当前指令是 swS0S4 (100)无条件S0注意 beq/j 在 S2 结束时直接回 S0中间不需要 S3、S4。它们的分支地址在 S1 已经算好S2 判断完就可以直接更新 PC。这就是多周期 CPU 的“复用”思想状态数越少指令执行周期越短但每个状态塞的操作也越多控制逻辑更复杂。2.3 控制信号与MUX选择控制器不仅要决定“下一个状态去哪”还要在每一个状态输出正确的控制信号。这些信号控制了数据通路上每一个 MUX 和寄存器的写使能。我整理成以下表格方便你对照着搭电路控制信号含义S0取指S1译码S2执行(R型)S2执行(lw/sw)S2执行(beq)S2执行(j)S3访存(lw)S3访存(sw)S4写回(R型)S4写回(lw)PCWrite允许PC更新10000beq用PCWriteCond10000PCWriteCond允许条件PC更新00001Zero1时00000IRWrite允许IR更新1000000000RegWrite允许寄存器堆写入0000000011MemWrite允许数据存储器写入0000000100MemRead数据存储器读使能0000001000MemtoReg寄存器写数据来源0ALUOut1MDRXXXXXXXX01ALUSrcAALU的A端来源0PC1A寄存器X0111XXXXXALUSrcBALU的B端来源00B01符号扩展imm10符号扩展imm211零扩展immX10000100XXXXXALUOpALU运算控制0000加0001减0010与0011或X0000按funct00000001XXXXXRegDst寄存器写地址来源0rt1rdXX1XXXXX10PCSourcePC来源00PC401ALUOut分支地址10跳转地址XXXX0110XXXX这张表是你在 Logisim 里搭控制器的“施工图”。我强烈建议你先完全理解这张表再动手不然接完线你会一脸懵为什么 PCWrite 在 S0 是 1在 S2 的 beq 里又变成了 PCWriteCond因为你 S0 要无条件自增 PC而 beq 在 S2 要根据 Zero 条件决定 PC 是否更新所以走的是另一条使能路径。MUX 的选通也是同样的道理。ALUSrcA 是 ALU 的 A 端选择S1 算分支地址时输入 PCS2 执行运算时输入 A 寄存器ALUSrcB 有 4 个来源S1 用左移 2 位的立即数算目标地址S2 对 lw/sw 用符号扩展立即数对 R 型和 beq 用 B 寄存器对 ori 用零扩展立即数。2.4 用组合逻辑实现状态机的几种方法状态机在 Logisim 里可以用三种方式实现我分别说下优缺点。第一种是“D触发器组合逻辑真值表”的标准实现。用 3 位寄存器保存当前状态输出端接组合逻辑电路组合逻辑根据当前状态、opcode、Zero 生成下一状态和控制信号。这种方式最贴近教材也最容易理解状态机的本质但搭建真值表电路时如果用手工逻辑门会比较费时。第二种是我最推荐的“状态译码多路选择器”实现。既然只有 5 个状态可以先用 3-8 译码器把当前状态译成 5 根独热信号然后为“下一状态”准备 5 个候选值再用一个 5 选 1 的 MUX 根据当前状态选出来。这个方案的好处是每个状态的跳转逻辑相互独立不容易互相干扰排错非常方便。第三种是使用 Logisim 自带的 Finite State Machine 组件。它支持画状态图自动生成逻辑看着高大上但在复杂多周期 CPU 里生成的电路不够透明信号时序也经常需要额外调整。除非你对这个组件非常熟悉否则我不建议第一次做就用它出了问题反而不好排查。控制器输出信号也可以用同样的组合逻辑方法生成。核心思路是根据当前状态state和指令 opcode用与门、或门、译码器、MUX 搭出所有控制信号。信号不多但要注意别漏接。3. Logisim搭建实操思路理清楚之后下面进入 Logisim 实操。我会按搭电路的顺序来写尽量让你照着就能连出来。3.1 组件准备与画布布局建议建议先建一个主电路然后在 Project → Add Circuit 里建几个子电路分别放寄存器堆、ALU、控制器、数据存储器、指令存储器。主电路里只放顶层数据通路连线这样界面不会爆炸。Logisim 的子电路可以在“左栏第二组”创建之后在任何地方都能直接拖入使用。每个子电路都要设置好输入输出引脚寄存器堆的子电路要有 clk、RegWrite、读地址1、读地址2、写地址、写数据输出两个 32 位读数据ALU 子电路要有 A、B、ALUOp 输入输出 Result 和 Zero。引脚尽量全部命名不然后期连线上百根线你会分不清哪个是哪个。画布布局上我建议从左到右按“指令/数据通路顺序”摆最左边是 PC 和 ROM中间是寄存器堆、A/B 寄存器、ALU右边是数据存储器 RAM。控制器可以放在最下方输出信号用隧道标签Tunnel传到上面避免几十根线交叉。实际上 Logisim 的 Tunnel 是整理线路的神器可以把控制信号“无线”连到目标位置强烈建议多用。3.2 寄存器堆RAM三件套方案寄存器堆是很多同学的第一道坎32 个 32 位寄存器要一个写口、两个读口手搭 32 个寄存器阵列不仅累而且容易接错线。这里我提供一个非常实用、很适合课设的偷懒方案用三个 RAM 组件模拟一个两读一写的寄存器堆。具体做法是复制三个 32×32 位 RAMLogisim 的 RAM 组件地址位宽设 5数据位宽设 32它们的写地址、写数据、写使能全部接到同一个信号上读地址1也就是 rs 接 RAM_A 的地址端口当作 read port 1读地址2也就是 rt 接 RAM_B 的地址端口当作 read port 2RAM_C 也接同样的写端口和读地址作为冗余。每个 RAM 的写使能都连到 RegWrite。这样写的时候三个 RAM 同步写入相同数据读的时候 RAM_A 和 RAM_B 分别按 rs、rt 输出就模拟出了双读单写的寄存器堆。你可能会觉得“三个 RAM 也算寄存器堆”但实验要求本质是验证 CPU 的控制逻辑和指令执行用三个同步 RAM 在功能上完全等效而且搭建速度飞快出错概率低。如果想挑战手搭寄存器阵列可以在做完整个 CPU 后再替换不影响主体验证。RAM 的写使能要注意Logisim RAM 的写操作是时钟边沿触发的写使能必须在时钟上升沿之前稳定。所以 RegWrite 信号不要直接连到 RAM 的 CLK 端而是连到写使能端CLK 统一接全局时钟。这样在 S4 状态RAM 在时钟沿把数据写进去。3.3 ALU与扩展器设计ALU 是数据通路的中枢。由于这 8 条指令只涉及加、减、与、或四种运算ALU 控制信号我用 4 位直接由控制器输出0000加0001减0010按位与0011按位或这样省掉了教材里“ALUOp → ALU 控制模块 → 真正控制位”的两级译码对课设来说更简单直接。如果你想让电路更接近教材也可以保留两级译码但第一次做我建议直接 4 位 ALUOp。ALU 在 Logisim 里可以直接用一个 Adder/Subtractor 组件做加减用 And/Or 门做逻辑再用一个 4 选 1 的 MUX 根据 ALUOp 选择输出。同时用 Equal 组件或者自己搭一个“结果是否为 0”的判断电路生成 Zero 信号。Zero 信号在 beq 指令中非常关键它告诉控制器两个操作数是否相等。立即数扩展这块用 Logisim 的 Sign Extender 组件把指令低 16 位符号扩展到 32 位这是给 lw/sw/beq 用的。另外自己做一个零扩展把 16 位立即数接到低 16 位高 16 位接常量 0。之后用一个 2 选 1 MUX由控制器 ExtOp 信号选择符号扩展还是零扩展。这个 MUX 的输出再接到 ALUSrcB 的 MUX 作为候选输入之一。别小看这点很多同学用 ori 的时候结果不对就是因为扩展方式选错负数或高位是 1 的立即数会直接导致结果错乱。3.4 数据通路连线顺序数据通路的连线建议按以下顺序进行每连一部分就测一部分第一步搭 PC 和指令存储器。PC 输出接 ROM 的地址输入ROM 的数据输出接 IR 寄存器的输入PC 的输入来自 PCSource MUXMUX 的三路候选分别是专用加法器输出的 PCPlus4、ALUOut、跳转地址拼接值。PCWrite 信号和全局时钟接 PC 的时钟使能控制。注意 j 指令的跳转地址由 IR 的低 26 位左移 2 位后与 PC[31:28] 拼接得到这个拼接可以用“Splitter”组件完成。第二步搭寄存器堆、A/B 寄存器和 MUX。寄存器堆的两个读输出分别接到 A 寄存器和 B 寄存器的输入A/B 在 S1 状态写入所以它们的写使能接 stateS1 对应的控制信号。A 寄存器输出接 ALUSrcA MUX 的输入端 1B 寄存器输出接 ALUSrcB MUX 的输入端 0。第三步搭 ALU 和 ALUOut。ALU 的 A 端接 ALUSrcA MUXB 端接 ALUSrcB MUXALUOp 接控制器的 ALUOp 信号输出 Result 接 ALUOut 寄存器Zero 标志送给控制器。ALUOut 寄存器不能用永久使能它的写使能必须按 2.3 节的表来S1 和部分 S2 状态才允许写入否则你 S1 算好的分支地址会被 S2 的 R 型结果覆盖。第四步搭数据 RAM。RAM 的地址输入接 ALUOut写数据接 B 寄存器写使能接 MemWrite数据输出接 MDR 寄存器的输入。MDR 在 S3 且当前指令为 lw 时写入写使能可以从 stateS3 中推导得到。第五步把寄存器堆的写地址和写数据接好。写地址用 RegDst MUX 在 rt 和 rd 之间选择写数据用 MemtoReg MUX 在 ALUOut 和 MDR 之间选择写使能接 RegWrite。到这里数据通路的“骨架”基本搭齐了剩下的就是控制器把所有信号串起来。3.5 控制器的搭法控制器放在主电路的下方。首先搭状态寄存器用一个 3 位 Register 保存当前状态时钟接全局时钟复位接全局复位。状态寄存器的输出接入状态译码电路我推荐用 3-8 译码器把 5 个状态译成独热信号方便后面每个状态单独控制。然后搭 next_state 电路。用 5 选 1 MUX5 个输入分别是对应状态下的跳转结果input0S0固定连 S1 的状态编码 001input1S1固定连 S2 的编码 010input2S2用一个 4 选 1 MUX根据 opcode 选择下一状态R 型连 S4、lw/sw 连 S3、beq/j 连 S0input3S3根据“当前指令是 lw 还是 sw”选择 S4 或 S0input4S4固定连 S0 的编码 000MUX 的选择端接状态译码输出就能根据当前状态选出 next_state。这个做法不需要做复杂的布尔化简逻辑清晰接错也好找。控制信号同样可以在译码后的独热状态线上用“状态线 与 指令类型”的组合生成。比如 RegWrite 等于 s4 状态信号且当前指令需要写回R型或lw的逻辑MemWrite 等于 s3 且 opcodeswPCWrite 等于 s0 状态PCWriteCond 等于 s2 状态且 opcodebeq 且 Zero1。把这些逻辑分别接到对应控制端口即可。4. 测试程序与仿真验证电路搭完之后最关键的一步是写测试程序把 8 条指令全部跑一遍验证 CPU 每一步都正确。千万不要一开始就跑去点时钟那样什么都看不出来。4.1 测试程序设计我设计了一段很小的测试程序放在 ROM 的地址 0 开始的位置。完整汇编和机器码如下地址(字节) 指令 机器码 0x00 lw $1, 0($0) 0x8C010000 0x04 lw $2, 4($0) 0x8C020004 0x08 add $3, $1, $2 0x00221820 0x0C sub $4, $1, $2 0x00222022 0x10 and $5, $1, $2 0x00222824 0x14 ori $6, $3, 8 0x34660008 0x18 sw $3, 8($0) 0xAC030008 0x1C beq $3, $6, 1 0x10660001 0x20 j 0 0x08000000 0x24 sw $3, 12($0) 0xAC03000C 0x28 j 0 0x08000000这段程序的执行逻辑是先从数据存储器地址 0 和地址 4 分别读两个数到 $1、$2然后做 add、sub、and、ori 四种运算再把 $3 存回数据存储器地址 8接着用 beq 判断 $3 是否等于 $6如果相等就跳过 j 0执行地址 0x24 的 sw把 $3 存到地址 12如果不相等就跳到地址 0x20 的 j 0回到 0x00 死循环。我故意设计了“$3 和 $6 在数值上相等”的条件见下面的 RAM 初始化这样能验证 beq 真正发生跳转而不是顺序执行兜底。如果你想观察不跳转的情况可以把 RAM 里的数改一下让 $3 和 $6 不相等即可。4.2 ROM和RAM初始化在 Logisim 里右键点击 ROM 组件选择 Edit Contents把 10 个字的机器码按地址填入。ROM 每个单元是 32 位地址按字节编址的话需要在 0x00、0x04、0x08 等位置分别填入上述值。如果 Logisim 的 ROM 地址是按字编址那从地址 0 开始顺序填即可注意和你的理解对齐。数据 RAM 也需要预先写入数据在地址 0 写入 5地址 4 写入 3。这样执行 lw $1,0($0) 后 $15执行 lw $2,4($0) 后 $23add 后 $38ori 立即数 8 后 $68|88两者相等beq 成功跳转。如果不初始化 RAM所有内存默认是 0最后的 beq 判断可能会不符合预期。RAM 初始化的方法同样是右键 Edit Contents直接把地址 0 填 5地址 4 填 3其他保持 0 即可。4.3 分步仿真与观察点接线全部完成并存入机器码后建议不要直接连按时钟而是用 Logisim 的“单步时钟”按 T 键或者手动点时钟一步步观察 CPU 的状态变化。每按一次时钟观察以下几项状态寄存器当前值应该依次出现 S0→S1→S2→S3→S4 的规律。IR 的值S0 结束后 IR 应当是当前指令机器码。A 和 B 寄存器的值S1 结束后等于从寄存器堆读出的数据。ALUOut 的值S2 结束后应当是当前指令的计算结果或访存地址。PC 的值S0 结束自动加 4beq 或 j 执行完会自动变成跳转目标。当你按到第一条 lw 指令结束S4时$1 寄存器的值应该等于 5第三条 add 结束后 $3 等于 8第六条 ori 结束后 $6 等于 8第七条 sw 结束后内存地址 8 的值是 8第八条 beq 执行完 PC 应该跳到 0x24而不是进入 j 0 死循环。这些点全对上说明 CPU 基本是通的。4.4 如何确认每条指令都执行正确我不建议只跑一段程序就看结果因为一个数据通路错误可能会被后面的指令掩盖。更稳妥的方式是分指令验证先验证 add/sub/and/ori 这类纯运算指令看 ALUOut、寄存器堆目标寄存器值是否等于手算结果。再验证 lw/sw看 MDR 的值、内存单元是否写入正确。最后验证 beq/j修改操作数让条件成立和不成立各跑一次确认 PC 跳转目标正确。如果你发现某个结果不对优先回看状态机那张控制信号表检查对应状态的控制信号是否为 1、MUX 选择是否对。绝大多数错误都出在这两个地方。5. 踩坑经验与常见问题这一部分是我自己踩过的坑也结合了身边同学的典型问题。做个速查表遇到问题先来这里找答案。现象可能原因排查方法状态机卡在某个状态不跳转组合逻辑 next_state 没接通或状态寄存器没有复位查看状态寄存器输入端是否合理先按复位再单步PC 一直不变PCWrite 没接或时钟没接到 PC 的 enable 端口确认 PCWrite 在 S0 为 1确认时钟接线IR 读到的指令是 0ROM 地址接线错误或初始内容没填检查 ROM 地址线是否接 PC 输出确认 ROM 内容寄存器堆算出来全是 0RegWrite 为 0或写地址/写数据 MUX 选错检查 S4 状态的 RegWrite确认 MemtoReg、RegDst 选择beq 不跳转Zero 信号没送到控制器或 PCWriteCond 没接检查 ALU 的 Zero 输出是否接到控制器的 Zero 输入beq 每次乱跳ALUOut 被覆盖了检查 ALUOutWrite 信号S2 的 beq 状态不应写 ALUOutori 结果不对立即数扩展用了符号扩展而不是零扩展检查扩展 MUX 的 ExtOp 选择信号sw 写不进去MemWrite 没接或 RAM 写使能时序不对确认 S3 状态 MemWrite1确认 RAM 的时钟沿仿真很慢或卡片循环太快或组件太多调低时钟频率用单步观察5.1 状态机不跳转 / 跳错状态机不跳转最常见的两个原因一是 next_state 组合逻辑没接对比如 S0 状态的 MUX 输入端没有固定接 S1 编码二是状态寄存器的时钟或复位没接好。建议按 3.5 节的方法先让状态机只跑固定跳转S0→S1→S2→S0确认状态寄存器本身没问题再逐步加入分支逻辑。我调试时用过一个很笨但有效的办法把状态寄存器的 3 位输出引到三个 LED 上每按一次时钟就能看到状态变化。如果是 001、010、100 这种乱序说明 next_state 逻辑有误如果一直是 000说明状态寄存器完全没更新。另一个常见问题是 beq/j 执行完 PC 更新后状态机回到 S0但 ROM 地址已经变成新的目标。如果跳转指令后面跟着的指令不是预期内容回查 PCSource MUX 和 PCWriteCond 信号准没错。5.2 寄存器堆不写入 / 写入错误寄存器堆不写入九成是 RegWrite 信号时序问题。记住一点RAM 的写是时钟沿触发的RegWrite 必须在一个时钟周期内保持稳定并且写使能要在时钟上升沿前建立。用 Logisim 时如果 RAM 的写使能和时钟沿冲突可以把 RAM 的触发方式调成下降沿或者调整全局时钟的高电平持续时间。如果写入了错误数据重点查两处写地址 MUX 选的是 rd 还是 rt以及写数据 MUX 选的是 ALUOut 还是 MDR。R 型指令写 rdlw 写 rt。我在第一次做的时候把 RegDst 方向接反了导致 add 的结果写进了 $0 以外的随机寄存器排查了很久才发现是 MUX 选择端接反了。5.3 beq和ori结果不对beq 跳错大概率是分支地址计算的问题。分支地址计算发生在 S1用 PC 符号扩展立即数左移 2 位。注意这里必须是左移 2 位MIPS 的立即数是相对下一条指令地址的偏移需要乘以 4。如果你只接了符号扩展没左移PC 会跳到完全错误的位置。ori 结果不对核心是扩展方式。ori 的立即数是零扩展不是符号扩展。如果 16 位立即数是 0x0008符号扩展和零扩展结果一样都是 8但如果立即数是负数比如 0xFFFF符号扩展会变成 0xFFFFFFFF零扩展还是 0x0000FFFF。所以即使你的测试立即数是正数能跑通我也建议你换成负数再测一次这个细节在答辩时很容易被问到。5.4 时钟竞争与仿真闪退Logisim 的仿真引擎和真正的 Verilog 时序仿真不同所有组件在同一 tick 内按内部顺序更新。如果多个寄存器在同一时钟沿竞争更新可能出现数据写入旧值而不是新值的情况。我的经验是把全局时钟的 High Duration 和 Low Duration 都调到 1 以上给组合逻辑留出稳定时间如果还出现竞争可以把状态寄存器的触发沿设成下降沿其他数据寄存器用上升沿错开更新时刻一般就能解决。仿真闪退或者卡死通常是电路里有组合逻辑环。比如控制器的输出信号又反过来影响控制器的输入形成无时钟的循环。检查一下所有“反馈回路”是否都经过寄存器如果不是赶紧断开来避免 Logisim 仿真器死循环。5.5 如何把分离存储器改成统一存储器如果你想把实验升级成教材标准版可以把 ROM 和 RAM 合并成一块 RAM。方法是增加一个 IorD 信号控制地址 MUXIorD0 时地址选 PCIorD1 时地址选 ALUOutRAM 数据输出同时接到 IR 和 MDR 的输入IRWrite 和 MDRWrite 分别控制锁存。S0 取指时 IorD0S3 访存时 IorD1。这样指令和数据存在同一块内存才是严格意义上的多周期 CPU。但说实话课设阶段用分离存储器就能满足绝大多数要求也不影响对多周期控制逻辑的考察。改统一存储器属于锦上添花有余力再动。最后再分享一点个人心得说实话这个实验我做完之后最大的感受是多周期 CPU 的难点不在电路规模而在“时序”。数据是怎么从一个寄存器流向另一个寄存器、哪个信号必须在哪个状态保持、哪个寄存器千万不能被覆盖这些想清楚了Logisim 连起来其实很快。我当时踩的最大的坑就是 ALUOut 的写使能没有单独控制导致 beq 执行时 S1 算好的分支地址被 S2 的减法结果覆盖分支跳转永远跳到错的地方。这个问题几乎不可能靠“盯着波形”发现只能靠对状态机每一个信号的准确把握。所以如果你时间紧张我强烈建议你先把状态机控制信号表在纸上完整推一遍确认每个状态的每个控制信号为 0 还是 1再动手连线。纸上推对一遍抵得上仿真排错三小时。另外如果后面想进一步挑战可以在这个多周期 CPU 的基础上改成五级流水线。核心变化就是把“状态机”换成“流水线寄存器”每个周期同时处理不同指令的不同阶段。理解清楚多周期的状态划分对理解流水线数据通路会特别有帮助。这也是我做这个实验时觉得最有价值的地方——它不是一门孤立的课设而是通往更高层 CPU 设计的台阶。