简介华中科技大学《计算机组成原理》CPU设计实验报告面向计算机专业本科生及正在完成组成原理课程实验的学习者聚焦32位MIPS单周期与多周期CPU设计这一难点任务可作为实验报告撰写与Logisim实现思路的参考。文档围绕8条核心指令展开覆盖数据通路搭建、硬布线控制器与微程序控制器设计、溢出异常处理以及运行sort.hex冒泡排序测试程序后的验证分析目录包含设计要求、方案设计、实验步骤、故障与调试、测试与分析、总结心得和参考文献并配有数据通路示意图、控制信号功能说明表与指令译码逻辑读者可据此梳理从取指、译码到执行、写回的关键控制关系便于按实验阶段对照查阅。资源包共1个docx文件大小约2.8MB内容完整、结构清晰适合课程实验复盘与报告整理。已有12128人学习说明其在同类实验资料中具有较高参考价值。1. 从一条 beq 指令讲起这份 32 位 MIPS CPU 设计实验要交什么计算机组成原理的 CPU 设计实验难的地方往往不在画电路真正卡住人的是电路画完之后说不清数据通路为什么这么接、控制信号为什么这个周期为 1。这份实验报告覆盖的正是这条完整链路在 Logisim 平台上先搭一棵 32 位 MIPS 单周期 CPU支持 8 条核心指令并配硬布线控制器再改成多周期 CPU存储器复用、加中间寄存器控制器分别用微程序和硬布线两种方式实现最后在冒泡排序程序 sort.hex 上跑出正确结果。它适合正在赶课程设计的学生、要给实验报告补方案章节的人也适合想把数据通路从头复现一遍的从业者。下面按“需求 → 设计 → 实现 → 验收”四段推进每一段都给到能直接抄进报告的表、逻辑表达式和调试方法。2. 指令集与总体架构8 条指令如何切出单周期和多周期两条路线整套实验的边界由一张指令表划定。指令少不代表简单8 条指令刚好覆盖 R 型运算、立即数运算、访存、条件分支和系统调用五类行为每一类在数据通路上都要单独设计一条路径最后再用 MUX 综合。先确定指令集再讨论架构顺序不能颠倒。2.1 8 条核心指令的语义与隐藏约束实验给定的核心指令集共 8 条功能与约束如下表。这张表建议原样放进报告的“设计要求”一节后面所有数据通路和控制信号都能从中推导出来。序号指令格式语义需要特别注意的点1addR 型R[rd] ← R[rs] R[rt]溢出时产生异常且不修改 rd2sltR 型R[rd] ← (R[rs] R[rt]) ? 1 : 0有符号比较结果写 0 或 13addiI 型R[rt] ← R[rs] SignExt16(imm)溢出产生异常立即数要符号扩展4lwI 型R[rt] ← Mem4B(R[rs] SignExt16(imm))地址由 ALU 计算读回数据要过 MUX5swI 型Mem4B(R[rs] SignExt16(imm)) ← R[rt]只写存储器不回写寄存器堆6beqI 型if (R[rs] R[rt]) PC ← PC SignExt18({imm, 00})偏移量左移 2 位按字节编址7bneI 型if (R[rs] ! R[rt]) PC ← PC SignExt18({imm, 00})条件取反其余同 beq8syscall—系统调用本实验用于停机单周期禁 PC 使能多周期状态自锁表里有三个容易被忽略的细节。第一add 和 addi 的“溢出产生异常”在单周期实验里通常不做到完整异常处理常规做法是把 ALU 的 overflow 输出取反后串进 RegWrite 通路溢出时禁止写回报告里说明这个简化即可。第二beq/bne 的立即数是 16 位左移两位后参与加法所以分支目标地址 PC 4 (imm 2) 还是 PC (imm 2)取决于你们数据通路里 PC 的更新时机这一点必须在报告里写清楚否则验收时对不上。第三syscall 不产生任何数据操作它只做一件事——让 PC 停下来。2.2 单周期与多周期不是把电路拆开那么简单单周期 CPU 的假设是“一条指令一个时钟周期”所有部件串行流过一遍指令存储器和数据存储器分开PC 每个周期无条件加 4。它的好处是控制信号只由指令译码决定组合逻辑写完就完事坏处是时钟周期必须迁就最慢的那条指令lw 要穿过取指、译码、执行、访存、写回五段其他指令全被它拖着跑。多周期 CPU 换了一套思路一条指令拆成“取指 → 译码 → 执行”三个阶段取指和译码是所有指令的公共前缀执行阶段按指令类型占用不同的周期数。由此带来一组结构性改动这些改动是报告“方案设计”一节的主干指令和数据共用同一个存储器取指和访存分时使用靠 IorD 信号选择地址来源是 PC 还是 ALU 输出存储器、ALU、寄存器堆都可以在一条指令的多个周期里被反复使用主要部件的输出端必须加寄存器锁存中间结果具体是 DR 存访存读出的数据、IR 存取出的指令、A 和 B 存寄存器堆读出的两个源操作数、C 存 ALU 的输出PC 不再每周期自增而是增加独立的写使能 PcWrite只有取指周期和分支成立时才写原来的专用加法器取消PC4 和分支目标地址计算都交给 ALU于是 ALU 的 B 输入端扩到 4 路、A 输入端扩到 2 路停机不再需要 halt 信号靠状态机在 syscall 状态上自循环实现。下面这张对比表可以直接放在报告里答辩时被问到“为什么多周期要加这么多寄存器”照着答就行。对比项单周期 CPU多周期 CPU存储器指令存储器与数据存储器分离统一存储器分时复用时钟周期由最慢指令决定所有指令等长按阶段划分不同指令周期数不同中间结果组合逻辑直通无需锁存必须锁存在 DR、IR、A、B、C 中PC 控制每周期 4无写使能增加 PcWrite分支成立才写控制器硬布线一种微程序、硬布线两种停机方式halt 信号禁用 PC 使能状态自锁2.3 组件清单与工程组织先定接口再连线实验包给出的组件是 PC、IMEM指令存储器、Regfile通用寄存器组、ALU、DMEM数据存储器和待实现的 Controller。动手连线之前建议先把每个组件的端口位宽列成一张表贴在自己的工程说明里因为后面 90% 的调试时间都花在位宽不匹配和地址错位上。组件关键端口位宽说明PC输入 next、使能、复位输出 pc32输出的是字节地址IMEM地址 in、输出 inst10 / 32输入是字地址需要 PC[11:2]DMEM地址、写数据、写使能、读数据10 / 32同样按字编址Regfilers、rt、rd、写数据、写使能5 / 32两个读端口一个写端口ALUA、B、AluOP输出 result、equal32equal 供分支指令使用SignExtin、out16 → 32有符号扩展地址对齐是这里最常见的第一个坑PC 是 32 位字节地址而存储器地址端口只有 10 位、按字编址所以要用分离器Splitter取 PC 的第 2 到第 11 位送进存储器。这个操作在单周期和多周期里都要做漏掉的话现象是程序一开始就跑飞取出来的指令全是 0 或者乱码。还有一处容易漏复位信号要接到 IR 和 DR 上否则仿真开始时这两个寄存器是随机值偶尔能跑对、偶尔跑不对非常像玄学。3. 单周期数据通路与硬布线控制器从逐条指令通路到九个控制信号单周期的设计顺序建议是先一条一条指令画通路再做综合最后才写控制器。反过来做的话控制器写完发现通路缺一根线前面的逻辑全要推倒。3.1 逐条指令画通路再用 MUX 做综合每条指令单独看数据通路都很短。取指阶段是 PC → IMEM → IR同时 PC4 回写add 和 slt 是 Regfile 两个读端口出 A、B进 ALU结果写回 rdaddi 是 B 端口换成符号扩展后的立即数lw 和 sw 是 ALU 算地址前者读存储器写寄存器、后者写存储器beq 和 bne 是 ALU 的 equal 输出决定 PC 要不要跳到 C。逐条画完之后的综合本质是回答一个问题同一个部件的输入端有多个来源时用哪个信号去选。R 型指令的 rd 和 I 型指令的 rt 都要写回所以要一个 RegDst 选择器ALU 的第二操作数可能是 rt也可能是扩展立即数所以要一个 AluSrcB 选择器写回寄存器的数据可能来自 ALU 也可能来自存储器读出的值所以要一个 MemToReg 选择器PC 的下一个值可能是 PC4也可能是分支目标地址所以要一个分支选择器。分支信号不是简单地把 Beq 接到选择器使能上通常写成下面这个形式# 分支成立条件beq 时 equal 有效bne 时 equal 无效 # 注意 bne 用的是 equal 取反不是单独一根 bne 线直接接使能 branch (beq and equal) or (bne and not equal)写成一个与或式就是 branch beq·equal bne·~equal直接对应 Logisim 里两个与门加一个或门。这个式子在单周期和多周期里都要用是报告里少数值得单独列出来的组合逻辑。3.2 指令译码逻辑把 OP 和 Func 翻译成七个信号硬布线控制器的第一级是译码。输入是指令的 Op 字段6 位和 Func 字段6 位输出是 LW、SW、BEQ、BNE、ADDI、ADD、SLT、SysCall 这些信号。查实验包内的 MIPS32 指令手册可以得到各字段的值指令OP 字段Func 字段LW0x23—SW0x2b—BEQ0x04—BNE0x05—ADDI0x08—ADD0x000x20SLT0x000x2aSysCall0x000x0cR 型指令的 OP 都是 0必须再看 Func 才能区分所以 R 型信号的生成要两级先与运算判 OP0再与 Func 比较器输出相与。# 指令译码把 OP/Func 常量比较翻译成 Logisim 里的比较器 与门 OP_CODE {LW: 0x23, SW: 0x2b, BEQ: 0x04, BNE: 0x05, ADDI: 0x08} FUNC_CODE {ADD: 0x20, SLT: 0x2a, SYSCALL: 0x0c} # I 型指令一位 OP 比较器输出就够 for name, code in OP_CODE.items(): print(f{name} (OP 0x{code:02x})) # R 型指令OP 必须为 0且 Func 匹配 for name, code in FUNC_CODE.items(): print(f{name} (OP 0x00) (FUNC 0x{code:02x})) # R_TYPE 是 ADD 或 SLT但要排除 SysCall它的 OP 同样是 0 print(R_TYPE (ADD SLT) ~SYSCALL)这段输出直接对应三块电路一块 6 位比较器阵列判 OP一块判 Func再用与门和或门拼出 R_TYPE。注意 R_TYPE 必须排除 SysCall否则 syscall 会被当成 R 型指令去写寄存器堆现象是排序结果里莫名其妙多出一个被改掉的寄存器值。ALU 控制逻辑只做一件事决定 ALU 执行加法还是比较。实验包里 ALU 的加法和比较各有一个操作码把这个操作码做成 2 路选择器的两个输入用 SLT 信号做选择端即可。其余时刻一律加法。这一步在报告里写起来很短但省掉它后面会很难受。3.3 九个控制信号的功能表与“或”实现拿到译码信号之后控制信号就是把对应的译码信号做逻辑或。原始报告给出的控制信号功能表如下这张表是整个硬布线控制器的骨架序号控制信号含义有效条件1MemToReg写回数据来自存储器lw2MemWrite写存储器sw3Beqbeq 译码信号beq4Bnebne 译码信号bne5AluOP运算器操作选择加法 / 比较6AluSrcBALU 第二输入选择lw、sw、addi7RegWrite寄存器写使能R_TYPE、addi、lw8RegDst写回寄存器选择R_TYPE9Halt停机信号syscall按“或”展开就是# 控制信号 由哪些指令触发就做逻辑或 RegWrite R_TYPE or ADDI or LW # 三条写回路径漏一个就丢结果 RegDst R_TYPE # 只有 R 型写 rd其余写 rt MemToReg LW # lw 的写回数据来自存储器 MemWrite SW AluSrcB LW or SW or ADDI # 这三条指令的 ALU 第二输入是立即数 Beq, Bne BEQ, BNE Halt SYSCALL # 接 PC 使能的取反端最后一行是关键Halt 通常接到 PC 寄存器使能端的取反上syscall 有效时 PC 停止更新整个 CPU 就停在原地。这个做法在单周期里成立在多周期里则改用状态自锁两种停机方式的区别是答辩常问题。4. 多周期数据通路与微程序控制器状态划分与控存文件生成多周期部分的工作量大概是单周期的两倍但套路更固定先把每条指令拆成若干微操作再把微操作按周期分组每个周期对应一个状态每个状态对应一条微指令。做完这一步微程序控制器和硬布线控制器只是同一张状态表的两种实现方式。4.1 阶段划分所有指令共享取指和译码取指和译码是两个公共状态任何指令都要走。取指周期完成 IR ← Mem[PC] 和 PC ← PC4译码周期完成 A ← R[rs]、B ← R[rt]、C ← PC (SignExt(imm) 2)也就是把两个源操作数和分支目标地址一次性算好存起来。执行阶段则按指令类型展开指令执行阶段微操作额外周期数addC ← A BR[rd] ← C2sltC ← (A B)R[rd] ← C2lwC ← A SignExt(imm)DR ← Mem[C]R[rt] ← DR3swC ← A SignExt(imm)Mem[C] ← B2beqif (A B) PC ← C1bneif (A ! B) PC ← C1addiC ← A SignExt(imm)R[rt] ← C2syscall空操作状态自锁1可以看到 lw 是最长的总共 5 个周期这与单周期里“最慢指令决定时钟周期”的瓶颈是同一个道理区别在于多周期只让 lw 自己慢其他指令该几个周期就几个周期。表里有一处容易写错lw 的访存周期存储器地址应该取 ALU 算出来的 C而不是 PC这靠 IorD 信号切换sw 的写数据来自 B 寄存器而不是 DR。这两处写反了现象是 lw 读到的永远是刚刚写过的那条指令或者 sw 把数据写到了指令区。按上表给每个状态编号就得到状态变换0 取指1 译码译码之后按指令译码信号跳到 2LW1、5SW1、7R1、9BEQ、10BNE、11ADDI1、13SYSCALL执行阶段的最后一条微指令统一回到 0。这个状态图是后面两种控制器的共同输入。4.2 微指令表与控存文件的生成方式微程序控制器的核心是一张表每一行是一条微指令包含控制字段和下址字段。控制字段里每个二进制位对应一根控制线下址字段指向下一条微指令的地址。手写这张表容易出错常规做法是用脚本从字典生成改一个字段只改一行数据不用重新画电路。# 把微指令表翻译成控存 ROM 的初始化文件每行一条微指令 # 字段顺序必须与 ROM 数据位排列一致高位在前 FIELDS [(IorD, 1), (PcSrc, 1), (AluSrcA, 1), (AluSrcB, 2), (MemToReg, 1), (RegDst, 1), (IrWrite, 1), (PcWrite, 1), (RegWrite, 1), (MemWrite, 1), (MemRead, 1), (BEQ, 1), (BNE, 1), (AluControl, 2), (P, 4)] # AluSrcA: 0PC, 1R[rs] # AluSrcB: 00R[rt], 01常数4, 10符号扩展立即数, 11扩展立即数左移2位 # AluControl: 00加法, 01比较, 10由 Func 决定 MICRO [ (FETCH, dict(AluSrcA0, AluSrcB0b01, MemRead1, IrWrite1, PcWrite1, P1)), (DECODE, dict(AluSrcA0, AluSrcB0b11, P0)), # P0 表示按译码信号跳转 (LW1, dict(AluSrcA1, AluSrcB0b10, P3)), (LW2, dict(IorD1, MemRead1, P4)), (LW3, dict(MemToReg1, RegWrite1, P0)), # P0 表示回取指 (SW1, dict(AluSrcA1, AluSrcB0b10, P6)), (SW2, dict(IorD1, MemWrite1, P0)), (R1, dict(AluSrcA1, AluSrcB0b00, AluControl0b10, P8)), (R2, dict(RegDst1, RegWrite1, P0)), (BEQ, dict(PcWrite1, PcSrc1, BEQ1, P0)), (BNE, dict(PcWrite1, PcSrc1, BNE1, P0)), (ADDI1, dict(AluSrcA1, AluSrcB0b10, P12)), (ADDI2, dict(RegWrite1, P0)), (SYSCALL, dict(P13)), # 下址指向自己实现自锁停机 ] def encode(field_dict): bits 0 for name, width in FIELDS: bits (bits width) | (field_dict.get(name, 0) ((1 width) - 1)) return bits with open(micro.txt, w) as f: for name, fields in MICRO: f.write(f{encode(fields):05x}\n) # 20 位控存字写成 5 位十六进制这段脚本做三件事按固定字段顺序把控制信号拼成一个二进制串、转成十六进制、逐行写入 micro.txt。Logisim 的控存 ROM 组件支持直接加载这个文件加载完微程序控制器就完成了一半。几个参数要说清楚。P 字段是 4 位下址取 0 时有两种含义处于译码状态时表示“下一状态由指令译码信号决定”处于执行阶段最后一个状态时表示“回到状态 0 重新取指”靠当前状态编号区分实现上是一个 2 路选择器。SYSCALL 的 P 指向 13 也就是自己PC 不再更新效果等同于停机这也是多周期里不再需要 halt 信号的原因。字段编码如果和你们实验包给的 ALU 输入端定义不一致只改 dict 里的值编码脚本一行都不用动这是用脚本而不是手填表的主要理由。4.3 地址转移逻辑与 ALU 控制信号的改造译码状态之后跳到哪由地址转移逻辑决定。输入是七个指令译码信号输出是 4 位微程序入口地址。各指令入口地址按 4.1 的状态编号取R_TYPE 到 7、ADDI 到 11、LW 到 2、SW 到 5、BEQ 到 9、BNE 到 10、SYSCALL 到 13。把这七个地址拆成 4 个二进制位每一位就是一个“或”表达式指令信号入口地址十进制入口地址二进制 S3S2S1S0R_TYPE70111ADDI111011LW20010SW50101BEQ91001BNE101010SYSCALL131101按位或出来就是S3 ADDI BEQ BNE SYSCALLS2 R_TYPE SW SYSCALLS1 R_TYPE ADDI LW BNES0 R_TYPE ADDI SW BEQ SYSCALL。把这四行填进 Logisim 的组合逻辑分析器直接生成电路比手工连七个比较器快得多。ALU 控制信号则需要重新设计。多周期里 ALU 要承担加法、比较、以及由 Func 决定三种情况所以 ALU_Control 改成 2 位00 加法、01 比较、10 由 Func 决定。实现上是一个 4 路选择器第一路接加法操作码第二路接比较操作码第三路由一个 2 路选择器输出——Func 等于 SLT 的 Func 值时选比较操作码否则选加法操作码第四路默认接加法操作码防止悬空。取指和译码状态都填 00R 型运算状态填 10这样 add 和 slt 共用一条微指令到 ALU 内部再分叉。微程序控制器的优点正在这里加一条新指令只要在控存里加几行不用动组合逻辑缺点是控存 ROM 有访问延迟且微指令格式一旦定死扩展控制信号要改位宽、重新生成整个文件。5. 硬布线控制器与全流程避坑状态机落地和 5 个最容易翻车的点硬布线控制器和微程序控制器吃的是同一张状态表区别在于前者把状态表烧进组合逻辑后者烧进存储器。这一章先讲状态机怎么写再讲我在这个实验里踩过和见过的高频翻车点。5.1 用 Moore 型状态机实现次态逻辑多周期的硬布线控制器是一个有限状态机状态寄存器用 4 位触发器记录当前状态输出只与现态有关与输入无关属于 Moore 型。状态机的输入是现态加一组指令译码信号和分支条件输出是次态。实现上分两步。第一步写次态逻辑把现态作为地址、把各译码信号作为条件列出“在状态 S、遇到信号 X 时跳到状态 T”的真值表用 Logisim 的组合逻辑分析器从真值表自动生成电路。这一步的关键是状态 1译码的出边要按七个译码信号分支而其他状态的出边基本是固定的顺序推进。第二步写输出逻辑控制信号只与现态有关把 4.2 那张微指令表里每个状态的控制字段拿出来做成“状态 → 控制信号”的或逻辑。比如 MemRead 在 FETCH、LW2 两个状态为 1就写成 MemRead (state FETCH) (state LW2)。这部分的表达式数量比单周期多但结构更规整属于体力活。硬布线的代价是电路规模随状态数增长改一条指令要重画真值表好处是没有控存访问延迟时序更干净。报告里把两种控制器的实现差异、电路规模、可扩展性各写一段对比基本就是“方案设计”里最有分量的一节。5.2 五个高频翻车点现象、原因与解决下面五条是这个实验里复现率最高的故障每一条都按“现象 → 原因 → 解决”写可以直接抄进报告的“故障与调试”一节。第一程序跑飞取出的指令全是 0 或乱码。现象是仿真一开始 PC 就跳到奇怪地址IMEM 输出恒为 0。原因通常是 PC 到存储器的地址没有做字地址转换32 位字节地址直接接到了 10 位地址端口高位被截断。解决是用分离器取 PC 的第 2 到第 11 位送入存储器地址端并在报告里注明“字节地址转字地址”。同类问题在 DMEM 上也存在只是现象更隐蔽表现为 lw 读到相邻数据。第二排序结果几乎正确但个别元素错位。现象是 sort.hex 跑完后数组大体有序只有一两个元素位置不对。原因多半是分支偏移没左移两位或者分支目标地址算成了 PC imm 而不是 PC 4 imm。解决是把 beq/bne 的立即数左移两位后再参与加法并在数据通路上用一个专门的移位器或直接拼接 00报告里把这一步单独画出来。第三lw 读到的永远是上一条指令或上一次访存的值。现象是测试程序里 load 指令返回值明显滞后。原因有两种一是多周期里忘记把 ALU 算出的地址锁进 C 寄存器导致访存周期用的是旧地址二是 IorD 信号接反访存时存储器地址还指向 PC。解决是检查 IorD 在 LW2、SW2 状态是否为 1以及 C 寄存器在 LW1、SW1 状态是否被写入。第四微程序控制器每次停在同一条指令上状态不推进。现象是单步运行时状态寄存器的值一直不变或者所有指令都跳到同一个入口。原因通常是译码状态的下址字段填成了具体地址而不是 0或者地址转移逻辑的输出接到了控存数据端而不是地址端。解决是确认 DECODE 微指令的 P 字段为 0并检查地址转移逻辑的选择器使能是否只在该状态有效。第五仿真结果不稳定重新跑一次答案就变。现象是同样的程序有时对有时错非常玄学。原因一般是复位信号没有接到 IR、DR 和寄存器堆起始状态是随机值或者存在组合逻辑环路例如分支信号直接反馈到 PC 使能而中间缺了时钟控制。解决是把复位信号铺到所有带状态的部件上并且在仿真开始前先跑几个周期让 PC 归零。这类问题的血泪经验是只要结果不稳定先怀疑复位和未初始化寄存器不要怀疑指令逻辑。6. 测试与验收sort.hex 跑通之后怎么自证与答辩很多人的实验在冒泡排序出结果那一刻就结束了但报告能不能拿高分、答辩能不能扛住追问取决于你有没有留下分层测试的证据。我的习惯是按三层推进每一层都截图存证报告里配一张测试记录表。6.1 三层测试策略第一层是部件级单独给 ALU 灌数据覆盖加法、比较、溢出三种情况单独给寄存器堆灌数据验证写使能为 0 时写入无效。第二层是指令级给每条指令写一段四到六行的十六进制小程序只观察寄存器堆和存储器的变化比如 add 只验证 rd 的值lw 只验证读回的值。第三层才是程序级跑 sort.hex检查排序结果和停机状态。跳过前两层直接跑排序出问题就只能在几万条指令里瞎找。6.2 验收清单与答辩问题下面这张表建议直接作为报告的“测试与分析”一节左侧是检查项右侧是判定标准。检查项判定标准8 条指令功能每条单指令测试通过寄存器或存储器变化符合语义溢出处理add 溢出时 rd 不被修改分支跳转beq 相等时跳、bne 不等时跳偏移量正确停机syscall 后 PC 停止更新排序程序sort.hex 输出完全有序与手工推演一致周期数对比单周期每条指令 1 周期多周期 lw 5、sw 4、R 型 4、beq/bne 3、addi 4答辩常问的几个点也一并准备多周期为什么要加 A、B、C 三个寄存器——因为部件分时复用中间值不锁存就会在下个周期丢失微程序和硬布线怎么选——控存容量换组合逻辑规模扩展性好的选微程序时序紧的选硬布线为什么 syscall 在多周期里不用 halt 信号——状态自锁已经实现了停机halt 是单周期为了禁用 PC 使能才引入的补丁。从那以后我每次画完数据通路都先单独跑一条 add 和一条 lw确认这两条通了再上排序程序因为所有复杂问题最后都会收敛到“ALU 没算对”或者“存储器地址错了”这两个源头。这套实验报告里包含的数据通路图、控制信号表、微指令表和调试记录基本可以按章节对照着改希望能帮到你。本文还有配套的精品资源点击获取