简介:华中科技大学《计算机组成原理》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 条,功能与约束如下表。这张表建议原样放进报告的“设计要求”一节,后面所有数据通路和控制信号都能从中推导出来。
| 序号 | 指令 | 格式 | 语义 | 需要特别注意的点 |
|---|---|---|---|---|
| 1 | add | R 型 | R[rd] ← R[rs] + R[rt] | 溢出时产生异常,且不修改 rd |
| 2 | slt | R 型 | R[rd] ← (R[rs] < R[rt]) ? 1 : 0 | 有符号比较,结果写 0 或 1 |
| 3 | addi | I 型 | R[rt] ← R[rs] + SignExt16(imm) | 溢出产生异常,立即数要符号扩展 |
| 4 | lw | I 型 | R[rt] ← Mem4B(R[rs] + SignExt16(imm)) | 地址由 ALU 计算,读回数据要过 MUX |
| 5 | sw | I 型 | Mem4B(R[rs] + SignExt16(imm)) ← R[rt] | 只写存储器,不回写寄存器堆 |
| 6 | beq | I 型 | if (R[rs] == R[rt]) PC ← PC + SignExt18({imm, 00}) | 偏移量左移 2 位,按字节编址 |
| 7 | bne | I 型 | if (R[rs] != R[rt]) PC ← PC + SignExt18({imm, 00}) | 条件取反,其余同 beq |
| 8 | syscall | — | 系统调用,本实验用于停机 | 单周期禁 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,只有取指周期和分支成立时才写;
- 原来的专用加法器取消,PC+4 和分支目标地址计算都交给 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、使能、复位;输出 pc | 32 | 输出的是字节地址 |
| IMEM | 地址 in、输出 inst | 10 / 32 | 输入是字地址,需要 PC[11:2] |
| DMEM | 地址、写数据、写使能、读数据 | 10 / 32 | 同样按字编址 |
| Regfile | rs、rt、rd、写数据、写使能 | 5 / 32 | 两个读端口一个写端口 |
| ALU | A、B、AluOP;输出 result、equal | 32 | equal 供分支指令使用 |
| SignExt | in、out | 16 → 32 | 有符号扩展 |
地址对齐是这里最常见的第一个坑:PC 是 32 位字节地址,而存储器地址端口只有 10 位、按字编址,所以要用分离器(Splitter)取 PC 的第 2 到第 11 位送进存储器。这个操作在单周期和多周期里都要做,漏掉的话现象是程序一开始就跑飞,取出来的指令全是 0 或者乱码。
还有一处容易漏:复位信号要接到 IR 和 DR 上,否则仿真开始时这两个寄存器是随机值,偶尔能跑对、偶尔跑不对,非常像玄学。
3. 单周期数据通路与硬布线控制器:从逐条指令通路到九个控制信号
单周期的设计顺序建议是:先一条一条指令画通路,再做综合,最后才写控制器。反过来做的话,控制器写完发现通路缺一根线,前面的逻辑全要推倒。
3.1 逐条指令画通路,再用 MUX 做综合
每条指令单独看,数据通路都很短。取指阶段是 PC → IMEM → IR,同时 PC+4 回写;add 和 slt 是 Regfile 两个读端口出 A、B,进 ALU,结果写回 rd;addi 是 B 端口换成符号扩展后的立即数;lw 和 sw 是 ALU 算地址,前者读存储器写寄存器、后者写存储器;beq 和 bne 是 ALU 的 equal 输出决定 PC 要不要跳到 C。
逐条画完之后的综合,本质是回答一个问题:同一个部件的输入端有多个来源时,用哪个信号去选。R 型指令的 rd 和 I 型指令的 rt 都要写回,所以要一个 RegDst 选择器;ALU 的第二操作数可能是 rt,也可能是扩展立即数,所以要一个 AluSrcB 选择器;写回寄存器的数据可能来自 ALU 也可能来自存储器读出的值,所以要一个 MemToReg 选择器;PC 的下一个值可能是 PC+4,也可能是分支目标地址,所以要一个分支选择器。
分支信号不是简单地把 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 字段 |
|---|---|---|
| LW | 0x23 | — |
| SW | 0x2b | — |
| BEQ | 0x04 | — |
| BNE | 0x05 | — |
| ADDI | 0x08 | — |
| ADD | 0x00 | 0x20 |
| SLT | 0x00 | 0x2a |
| SysCall | 0x00 | 0x0c |
R 型指令的 OP 都是 0,必须再看 Func 才能区分,所以 R 型信号的生成要两级:先与运算判 OP==0,再与 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 九个控制信号的功能表与“或”实现
拿到译码信号之后,控制信号就是把对应的译码信号做逻辑或。原始报告给出的控制信号功能表如下,这张表是整个硬布线控制器的骨架:
| 序号 | 控制信号 | 含义 | 有效条件 |
|---|---|---|---|
| 1 | MemToReg | 写回数据来自存储器 | lw |
| 2 | MemWrite | 写存储器 | sw |
| 3 | Beq | beq 译码信号 | beq |
| 4 | Bne | bne 译码信号 | bne |
| 5 | AluOP | 运算器操作选择 | 加法 / 比较 |
| 6 | AluSrcB | ALU 第二输入选择 | lw、sw、addi |
| 7 | RegWrite | 寄存器写使能 | R_TYPE、addi、lw |
| 8 | RegDst | 写回寄存器选择 | R_TYPE |
| 9 | Halt | 停机信号 | 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 ← PC+4;译码周期完成 A ← R[rs]、B ← R[rt]、C ← PC + (SignExt(imm) << 2),也就是把两个源操作数和分支目标地址一次性算好存起来。执行阶段则按指令类型展开:
| 指令 | 执行阶段微操作 | 额外周期数 |
|---|---|---|
| add | C ← A + B;R[rd] ← C | 2 |
| slt | C ← (A < B);R[rd] ← C | 2 |
| lw | C ← A + SignExt(imm);DR ← Mem[C];R[rt] ← DR | 3 |
| sw | C ← A + SignExt(imm);Mem[C] ← B | 2 |
| beq | if (A == B) PC ← C | 1 |
| bne | if (A != B) PC ← C | 1 |
| addi | C ← A + SignExt(imm);R[rt] ← C | 2 |
| syscall | 空操作,状态自锁 | 1 |
可以看到 lw 是最长的,总共 5 个周期,这与单周期里“最慢指令决定时钟周期”的瓶颈是同一个道理,区别在于多周期只让 lw 自己慢,其他指令该几个周期就几个周期。
表里有一处容易写错:lw 的访存周期,存储器地址应该取 ALU 算出来的 C,而不是 PC,这靠 IorD 信号切换;sw 的写数据来自 B 寄存器,而不是 DR。这两处写反了,现象是 lw 读到的永远是刚刚写过的那条指令,或者 sw 把数据写到了指令区。
按上表给每个状态编号,就得到状态变换:0 取指,1 译码;译码之后按指令译码信号跳到 2(LW1)、5(SW1)、7(R1)、9(BEQ)、10(BNE)、11(ADDI1)、13(SYSCALL);执行阶段的最后一条微指令统一回到 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: 0=PC, 1=R[rs] # AluSrcB: 00=R[rt], 01=常数4, 10=符号扩展立即数, 11=扩展立即数左移2位 # AluControl: 00=加法, 01=比较, 10=由 Func 决定 MICRO = [ ("FETCH", dict(AluSrcA=0, AluSrcB=0b01, MemRead=1, IrWrite=1, PcWrite=1, P=1)), ("DECODE", dict(AluSrcA=0, AluSrcB=0b11, P=0)), # P=0 表示按译码信号跳转 ("LW1", dict(AluSrcA=1, AluSrcB=0b10, P=3)), ("LW2", dict(IorD=1, MemRead=1, P=4)), ("LW3", dict(MemToReg=1, RegWrite=1, P=0)), # P=0 表示回取指 ("SW1", dict(AluSrcA=1, AluSrcB=0b10, P=6)), ("SW2", dict(IorD=1, MemWrite=1, P=0)), ("R1", dict(AluSrcA=1, AluSrcB=0b00, AluControl=0b10, P=8)), ("R2", dict(RegDst=1, RegWrite=1, P=0)), ("BEQ", dict(PcWrite=1, PcSrc=1, BEQ=1, P=0)), ("BNE", dict(PcWrite=1, PcSrc=1, BNE=1, P=0)), ("ADDI1", dict(AluSrcA=1, AluSrcB=0b10, P=12)), ("ADDI2", dict(RegWrite=1, P=0)), ("SYSCALL", dict(P=13)), # 下址指向自己,实现自锁停机 ] 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 个二进制位,每一位就是一个“或”表达式:
| 指令信号 | 入口地址(十进制) | 入口地址(二进制 S3S2S1S0) |
|---|---|---|
| R_TYPE | 7 | 0111 |
| ADDI | 11 | 1011 |
| LW | 2 | 0010 |
| SW | 5 | 0101 |
| BEQ | 9 | 1001 |
| BNE | 10 | 1010 |
| SYSCALL | 13 | 1101 |
按位或出来就是:S3 = ADDI + BEQ + BNE + SYSCALL,S2 = R_TYPE + SW + SYSCALL,S1 = R_TYPE + ADDI + LW + BNE,S0 = R_TYPE + ADDI + SW + BEQ + SYSCALL。把这四行填进 Logisim 的组合逻辑分析器,直接生成电路,比手工连七个比较器快得多。
ALU 控制信号则需要重新设计。多周期里 ALU 要承担加法、比较、以及由 Func 决定三种情况,所以 ALU_Control 改成 2 位:00 加法、01 比较、10 由 Func 决定。实现上是一个 4 路选择器,第一路接加法操作码,第二路接比较操作码,第三路由一个 2 路选择器输出——Func 等于 SLT 的 Func 值时选比较操作码,否则选加法操作码,第四路默认接加法操作码防止悬空。取指和译码状态都填 00,R 型运算状态填 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 没算对”或者“存储器地址错了”这两个源头。这套实验报告里包含的数据通路图、控制信号表、微指令表和调试记录,基本可以按章节对照着改,希望能帮到你。
本文还有配套的精品资源,点击获取