简介:这份资源是《计算机组成原理》实验课的单周期CPU设计完整作业包,面向正在学习MIPS32指令系统与Verilog HDL硬件描述语言的本科生及自学者,帮助解决从指令理解到逻辑功能落地的全过程实现难题。包内共90个文件,以Verilog源码、Quartus工程配置(qsf、qpf)、仿真与综合报告(rpt、summary)、波形文件(vwf)以及实验截图和docx实验报告为主,压缩包约2.37MB,覆盖设计、仿真、综合到验证的完整流程。资源实现了十六条指令的单周期CPU,包含完整代码与详细实验报告,读者可据此掌握单周期CPU的工作原理与逻辑功能实现,并通过运行状况观察分析加深理解。目前已有12292人学习下载,适合作为课程实验参考与查漏补缺的实操范本。
1. 单周期CPU设计:从一条指令的完整执行路径讲起
很多人做《计算机组成原理》课设时,第一反应是去网上找一份现成的Verilog代码,改改寄存器、跑个仿真波形就算交差。但真正答辩时被问一句“beq指令在哪个阶段比较、比较结果怎么影响PC”,就答不上来了。单周期CPU设计的核心不是写代码,而是把每条MIPS32指令的执行过程拆解成取指、译码、执行、访存、写回五个阶段的组合逻辑,再用Verilog HDL把这条数据通路固化下来。这份实验报告方案面向正在做计组课设的本科生,也适合需要快速搭出一套可演示、可验收的CPU原型的从业者。整套方案覆盖从指令集裁剪、数据通路设计、控制器编码到仿真验证的完整链路,最终交付一份能跑通不少于10条指令、带完整波形和测试用例的实验报告。
2. 需求分析与指令集裁剪:先定清楚CPU要支持什么
2.1 从MIPS32指令集中选出一条最小可用子集
单周期CPU不可能也没必要实现完整的MIPS32指令集。通常的做法是选一个覆盖R型、I型、J型三类格式的最小指令子集,既能体现数据通路设计的完整性,又不至于让控制信号复杂到无法调试。我一般会选以下12条指令作为课设的基准集合:
| 指令 | 格式 | 功能 | 涉及模块 |
|---|---|---|---|
| add | R型 | 寄存器加法 | ALU、寄存器堆 |
| sub | R型 | 寄存器减法 | ALU、寄存器堆 |
| and | R型 | 按位与 | ALU、寄存器堆 |
| or | R型 | 按位或 | ALU、寄存器堆 |
| slt | R型 | 小于则置1 | ALU、寄存器堆 |
| lw | I型 | 加载字 | 数据存储器、ALU |
| sw | I型 | 存储字 | 数据存储器、ALU |
| beq | I型 | 相等跳转 | ALU、PC逻辑 |
| addi | I型 | 立即数加法 | ALU、符号扩展 |
| j | J型 | 无条件跳转 | PC逻辑 |
| nop | 特殊 | 空操作 | 全通路 |
| halt | 特殊 | 停机 | 控制逻辑 |
这个子集的好处是:R型指令覆盖了ALU的多种运算功能,I型指令覆盖了访存和分支,J型指令覆盖了跳转逻辑。答辩时老师问“为什么选这12条”,你可以回答:它们构成了一个图灵完备的最小子集,足以验证数据通路和控制器的正确性。
2.2 单周期设计的时序约束与时钟策略
单周期CPU的最大特点是每条指令在一个时钟周期内完成。这意味着时钟周期必须大于等于最慢指令的关键路径延迟。通常lw指令的路径最长:PC → 指令存储器 → 寄存器堆读 → ALU计算 → 数据存储器读 → 寄存器堆写。在FPGA上综合后,这条路径的延迟决定了最高时钟频率。
实际做的时候,我建议在仿真阶段把时钟周期设为10ns(100MHz),综合后根据时序报告再调整。如果关键路径超时,优先优化数据存储器的读延迟,比如用分布式RAM替代块RAM。注意:单周期CPU不需要考虑流水线冒险,这是它比流水线CPU简单的地方,但也是它频率上不去的根本原因。
2.3 实验报告的交付物清单
一份完整的实验报告通常包含以下内容,建议在动手写代码之前就把框架搭好:
- 指令集说明表(格式、编码、功能)
- 数据通路图(标注每条指令的活跃路径)
- 控制器真值表(opcode/funct到控制信号的映射)
- Verilog源码(分模块组织,带注释)
- 测试用例及仿真波形(每条指令至少一组)
- 综合报告(资源占用、时序分析)
- 问题与调试记录(至少3个踩坑点)
3. 数据通路设计:从PC到写回的全链路拆解
3.1 取指阶段:PC、指令存储器与下一条地址逻辑
取指阶段的核心是PC寄存器和指令存储器。PC在每个时钟上升沿更新,输出当前指令地址;指令存储器根据地址读出32位指令字。下一条PC地址的逻辑是:
// 下一条PC地址计算 always @(*) begin if (branch_taken) next_pc = pc + 4 + (sign_ext_imm << 2); // 分支目标 else if (jump) next_pc = {pc[31:28], jump_addr, 2'b00}; // 跳转目标 else next_pc = pc + 4; // 顺序执行 end这里有个容易翻车的点:分支偏移量要先符号扩展再左移2位,因为MIPS32的指令按字对齐,偏移量是以字为单位的。跳转地址则是取PC高4位拼接26位立即数再左移2位。仿真时如果发现beq跳转到了错误地址,先检查这两处移位有没有漏掉。
3.2 译码阶段:寄存器堆与立即数扩展
译码阶段要完成三件事:读出rs和rt寄存器的值、解析指令的opcode和funct字段、对I型指令的立即数做符号扩展。寄存器堆的设计要注意:读操作是组合逻辑(异步读),写操作是时序逻辑(同步写)。这意味着在同一个时钟周期内,可以先写后读,但实际单周期CPU中写回发生在周期末尾,所以读出的还是旧值。
// 寄存器堆:两个读端口,一个写端口 module regfile ( input clk, input [4:0] rs_addr, rt_addr, wr_addr, input [31:0] wr_data, input reg_write, output [31:0] rs_data, rt_data ); reg [31:0] regs [0:31]; assign rs_data = (rs_addr == 0) ? 32'b0 : regs[rs_addr]; assign rt_data = (rt_addr == 0) ? 32'b0 : regs[rt_addr]; always @(posedge clk) begin if (reg_write && wr_addr != 0) regs[wr_addr] <= wr_data; end endmodule注意$zero寄存器(地址0)必须硬连线为0,写入操作要忽略对它的写使能。这个细节在仿真时如果漏掉,会导致add $t0, $zero, $zero这类指令结果异常。
3.3 执行阶段:ALU功能编码与标志位生成
ALU是执行阶段的核心,需要根据ALUOp和funct字段生成具体的运算操作。通常的做法是用一个4位ALUControl信号来编码8种以上的运算:
| ALUControl | 运算 | 使用指令 |
|---|---|---|
| 0000 | AND | and |
| 0001 | OR | or |
| 0010 | ADD | add, lw, sw, addi |
| 0110 | SUB | sub, beq |
| 0111 | SLT | slt |
| 1100 | NOR | 可选扩展 |
beq指令的比较操作实际上是用SUB实现的:如果两个寄存器值相减结果为0,则zero标志位置1,触发分支。这个设计技巧在答辩时经常被问到,值得在报告里专门写一段。
3.4 访存与写回:数据存储器接口与写回多路选择
访存阶段只有lw和sw指令会用到数据存储器。写回阶段需要一个多路选择器来决定写回数据来自ALU结果还是存储器读出数据:
// 写回数据选择 always @(*) begin case (mem_to_reg) 1'b0: write_back_data = alu_result; // R型、addi 1'b1: write_back_data = mem_read_data; // lw endcase end数据存储器的地址来自ALU计算结果,写数据来自rt寄存器。注意sw指令不需要写回,所以RegWrite信号为0。这个多路选择器的控制信号MemToReg直接来自控制器,在真值表中要明确列出。
4. 控制器设计:真值表驱动的硬布线方案
4.1 主控制器与ALU控制器的两级译码结构
控制器采用两级结构:主控制器根据opcode生成各类控制信号(RegWrite、MemRead、MemWrite、Branch、Jump、MemToReg、ALUSrc等),ALU控制器根据ALUOp和funct字段生成具体的ALUControl信号。这种两级结构的好处是主控制器不需要关心R型指令的具体运算类型,简化了真值表。
主控制器的真值表通常这样组织:
| opcode | RegWrite | MemRead | MemWrite | Branch | Jump | MemToReg | ALUSrc |
|---|---|---|---|---|---|---|---|
| 000000 (R型) | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 100011 (lw) | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
| 101011 (sw) | 0 | 0 | 1 | 0 | 0 | X | 1 |
| 000100 (beq) | 0 | 0 | 0 | 1 | 0 | X | 0 |
| 000010 (j) | 0 | 0 | 0 | 0 | 1 | X | X |
| 001000 (addi) | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
4.2 控制信号的Verilog实现与优先级处理
把真值表翻译成Verilog的case语句时,要注意两个优先级问题:一是jump优先级高于branch,二是halt指令要能冻结PC。我一般会这样写:
// 主控制器:opcode译码 always @(*) begin // 默认值:防止锁存器 reg_write = 0; mem_read = 0; mem_write = 0; branch = 0; jump = 0; mem_to_reg = 0; alu_src = 0; case (opcode) 6'b000000: begin // R型 reg_write = 1; alu_op = 2'b10; end 6'b100011: begin // lw reg_write = 1; mem_read = 1; mem_to_reg = 1; alu_src = 1; alu_op = 2'b00; end // ... 其余指令 endcase end注意:case语句前必须给所有输出赋默认值,否则综合工具会推断出锁存器,导致仿真结果与硬件行为不一致。这是新手最容易踩的坑之一。
4.3 分支与跳转逻辑的PC选择
分支和跳转的PC选择逻辑要处理三种情况:顺序执行(PC+4)、分支跳转(PC+4+offset<<2)、直接跳转({PC[31:28], addr, 2'b00})。beq指令还要结合ALU的zero标志:
assign branch_taken = branch & zero; assign next_pc = jump ? jump_target : branch_taken ? branch_target : pc_plus_4;这里有个调试技巧:在仿真时把next_pc也加到波形窗口里,一旦发现PC跳到了非法地址,先看是branch_taken误触发还是jump_target计算错误。通常前者是因为zero标志在非beq指令时没有清零,后者是因为跳转地址拼接时位宽搞错了。
5. 仿真验证与避坑:从波形到上板的排查手册
5.1 测试用例设计与波形检查清单
仿真验证要覆盖每条指令的典型场景。我一般会写一个测试程序,按顺序执行以下操作:用addi加载初始值、用add/sub验证ALU、用and/or验证逻辑运算、用sw/lw验证访存、用beq验证分支、用j验证跳转、最后halt。每条指令执行后检查目标寄存器的值是否正确。
波形检查的重点:PC是否按预期递增或跳转、RegWrite信号是否在正确的周期有效、写回数据是否与预期一致、MemRead/MemWrite是否只在访存指令时有效。建议把关键信号分组显示,避免波形窗口太乱。
5.2 常见问题排查:现象、原因与解决
现象一:PC在复位后没有从0开始。原因通常是复位信号没有正确连接到PC寄存器的异步复位端,或者复位值写成了其他地址。解决方法是检查PC模块的always块,确保复位时pc <= 32'b0。
现象二:lw指令读出的数据总是0。原因可能是数据存储器的读地址计算错误,或者MemToReg信号没有正确置1。先检查ALU的第二个操作数是否来自符号扩展后的立即数(ALUSrc=1),再检查写回多路选择器的选择信号。
现象三:beq指令跳转到了错误地址。原因通常是分支偏移量没有左移2位,或者符号扩展做成了零扩展。检查符号扩展模块的输出位宽是否为32位,以及分支目标计算时是否执行了<<2操作。
现象四:仿真时寄存器堆写入不生效。原因可能是写地址为0($zero寄存器被忽略),或者RegWrite信号在写回阶段已经变为0。检查控制器的时序,确保RegWrite在时钟上升沿时仍然有效。
现象五:综合后时序不满足,最高频率远低于预期。原因通常是关键路径太长,尤其是lw指令的路径。解决方法包括:把数据存储器改为分布式RAM、在ALU输出加一级流水寄存器(但这会破坏单周期语义)、或者降低时钟频率要求。
5.3 上板调试的注意事项
如果课设要求上FPGA开发板,有几个额外注意点:引脚约束要正确映射时钟、复位、LED和数码管;时钟信号要用全局时钟缓冲;复位按键要做消抖处理。上板后如果行为异常但仿真正确,优先检查约束文件中的引脚分配和时钟频率设置。
6. 验收与答辩:怎么证明你的CPU真的跑通了
验收环节通常分两部分:仿真波形演示和现场提问。仿真演示时,不要只跑一条add指令就完事,要按测试程序的顺序完整跑一遍,让老师看到每条指令的波形都符合预期。我一般会提前把关键波形截图整理成一张对照表,标注每条指令执行时的PC值、寄存器变化和控制信号状态。
现场提问的高频问题集中在几个地方:为什么单周期CPU的频率上不去(答:关键路径延迟决定时钟周期)、beq指令的比较是怎么实现的(答:用SUB运算的zero标志)、控制器是硬布线还是微程序(答:硬布线,真值表驱动)、如果增加一条指令需要改哪些模块(答:主控制器真值表、ALU控制器、可能还要改数据通路)。提前把这些问题过一遍,答辩时就不会卡壳。
| 验收项 | 合格标准 | 检查方法 |
|---|---|---|
| 指令覆盖 | 不少于10条指令正确执行 | 跑完整测试程序 |
| 波形完整 | 每条指令有对应波形 | 仿真截图 |
| 代码规范 | 分模块、有注释 | 代码审查 |
| 报告结构 | 含数据通路图、真值表 | 文档检查 |
| 现场答辩 | 能解释关键设计决策 | 提问回答 |
有个血泪经验:答辩前一定要把仿真工程重新跑一遍,确认没有因为改代码导致回归失败。我有一次答辩前夜改了一个控制信号,结果第二天现场演示时lw指令直接读出了错误数据,当场翻车。从那以后我每次提交前都强制走一遍完整回归测试,把所有指令的波形重新过一遍。希望这套方案能帮你少走弯路,顺利交出那份能经得起追问的实验报告。
本文还有配套的精品资源,点击获取