简介这份资源是《计算机组成原理》实验课的单周期CPU设计完整作业包面向正在学习MIPS32指令系统与处理器实现的高校学生及自学者。内容围绕Verilog HDL编写的单周期CPU展开覆盖十六条指令的实现过程帮助读者理解单周期CPU的工作原理与逻辑功能并通过运行观察加深对指令执行流程的认识。压缩包共90个文件约2.37MB包含Verilog源码、Quartus工程配置与综合报告文件、仿真波形与RTL截图以及一份详细的docx实验报告从代码到文档形成完整闭环。目前已有12292人学习下载热度较高。读者可据此获得可直接参考的CPU设计源码、工程配置与仿真验证材料配合实验报告梳理数据通路与控制逻辑的设计思路适合作为课程实验的对照范例或自主设计的起步模板。1. 单周期CPU设计为什么一条指令必须在一个时钟周期内跑完很多人第一次接触单周期CPU设计时都会觉得它“太简单了”——一条指令一个周期没有流水线没有冒险没有分支预测能有什么门道但恰恰是这个“简单”的模型藏着理解整个计算机体系结构最关键的钥匙。单周期CPU设计的核心约束是每条指令的所有操作包括取指、译码、执行、访存、写回必须在一个时钟周期内全部完成。这意味着时钟周期长度由最慢的那条指令决定比如访存指令lw。这个约束看起来是性能上的妥协但它换来了控制逻辑的极度清晰——没有状态机没有中间寄存器所有数据通路在一个周期内“流过”组合逻辑控制信号完全由指令译码直接产生。搞懂单周期CPU你才能真正理解数据通路为什么长那样、控制信号为什么那样给、时钟周期为什么不能无限缩短。这份《计算机组成原理》实验报告-单周期CPU设计全过程就是围绕MIPS32指令集用Verilog HDL把这条“一个周期跑完一条指令”的路径完整搭出来从取指到写回每一步都落在代码和波形上。2. 数据通路指令在一个周期里到底走了哪条路2.1 从PC到指令存储器取指阶段的硬件逻辑单周期CPU的取指阶段看起来只是“把PC送进指令存储器读出指令”但这里有个容易被忽略的细节PC的更新和指令的读取是同时发生的。在Verilog里通常这样写// 取指阶段PC寄存器与指令存储器 module fetch_stage( input clk, input rst, input [31:0] next_pc, output reg [31:0] pc, output [31:0] instr ); // PC寄存器每个时钟上升沿更新 always (posedge clk or posedge rst) begin if (rst) pc 32h00000000; // 复位后从地址0开始 else pc next_pc; // 下一个周期跳转到新地址 end // 指令存储器组合逻辑读出地址由当前PC决定 // 注意这里用pc作为地址而不是next_pc assign instr imem[pc[31:2]]; // 按字寻址忽略低2位 endmodule这段代码的关键在于instr用的是当前pc而pc在时钟沿更新。也就是说在时钟上升沿到来之前pc保持旧值指令存储器已经稳定输出旧pc对应的指令上升沿到来时pc更新为新值但当前周期的指令已经锁存进后续阶段。这种“组合逻辑读指令、时序逻辑更新PC”的搭配是单周期CPU能在一个周期内完成取指的前提。如果指令存储器也用时序逻辑那就需要两个周期才能完成取指单周期模型直接崩塌。2.2 译码与寄存器堆读取控制信号是怎么“长”出来的译码阶段的核心任务是把32位指令拆成操作码、寄存器索引和立即数同时产生控制信号。在单周期CPU里控制单元通常是一个纯组合逻辑模块输入是指令的opcode和funct字段输出是一组控制信号RegWrite、MemRead、MemWrite、ALUOp、MemToReg、ALUSrc、Branch等。// 控制单元根据opcode和funct产生控制信号 module control_unit( input [5:0] opcode, input [5:0] funct, output reg reg_write, output reg mem_read, output reg mem_write, output reg [1:0] alu_op, output reg mem_to_reg, output reg alu_src, output reg branch ); always (*) begin // 默认值防止锁存器推断 reg_write 0; mem_read 0; mem_write 0; alu_op 2b00; mem_to_reg 0; alu_src 0; branch 0; case (opcode) 6b000000: begin // R型指令 reg_write 1; alu_op 2b10; // 由funct决定具体运算 end 6b100011: begin // lw reg_write 1; mem_read 1; alu_op 2b00; mem_to_reg 1; alu_src 1; end 6b101011: begin // sw mem_write 1; alu_op 2b00; alu_src 1; end 6b000100: begin // beq branch 1; alu_op 2b01; end // 其他指令省略 endcase end endmodule这里有个设计取舍alu_op只给出粗粒度的运算类别加、减、由funct决定具体的ALU操作码再由二级译码产生。这种两级译码结构在单周期CPU里很常见因为它把R型指令的funct字段和I型指令的opcode字段解耦了控制单元不用为每条R型指令单独写一个case分支。寄存器堆的读取也是组合逻辑两个读端口根据rs和rt索引直接输出数据写端口在时钟沿写入。注意写端口用的是时钟沿读端口是组合逻辑这样才能在同一个周期内完成“读旧值、算新值、写回”的闭环。2.3 执行与访存ALU和Data Memory的时序配合执行阶段的核心是ALU它根据alu_op和funct计算出结果。对于lw和swALU的结果是内存地址对于R型指令结果是运算结果对于beq结果是零标志。访存阶段只在lw和sw时有效lw从Data Memory读出数据sw把数据写入Data Memory。// ALU组合逻辑根据alu_op和funct计算结果 module alu( input [31:0] a, input [31:0] b, input [1:0] alu_op, input [5:0] funct, output reg [31:0] result, output zero ); always (*) begin case (alu_op) 2b00: result a b; // 加法lw/sw/ addi 2b01: result a - b; // 减法beq 2b10: begin // R型指令由funct决定 case (funct) 6b100000: result a b; // add 6b100010: result a - b; // sub 6b100100: result a b; // and 6b100101: result a | b; // or 6b101010: result (a b) ? 1 : 0; // slt default: result 32b0; endcase end default: result 32b0; endcase end assign zero (result 32b0); // beq使用 endmoduleData Memory的读写时序是单周期CPU里最容易翻车的地方。读操作是组合逻辑地址稳定后数据直接输出。写操作是时序逻辑在时钟沿写入。这意味着lw指令在同一个周期内ALU算出地址Data Memory组合逻辑读出数据然后通过MemToReg选择器写回寄存器堆。而sw指令在时钟沿把数据写入Data Memory同时寄存器堆不写。这种“读组合、写时序”的搭配保证了单周期内完成访存。3. 控制信号为什么单周期CPU的控制逻辑是“纯组合”的3.1 控制单元的输入输出关系从指令字段到硬件动作控制单元的本质是一个查找表输入是指令的opcode和部分funct输出是一组控制信号。在单周期CPU里这个查找表完全由组合逻辑实现没有状态寄存器。为什么可以这样因为单周期CPU没有“当前处于哪个阶段”的概念——每个周期都是完整的取指、译码、执行、访存、写回。控制信号只需要根据当前指令产生不需要记住上一条指令做了什么。这种设计的好处是控制逻辑极其简单坏处是时钟周期必须迁就最慢的指令。常见的实现思路是先列出所有指令的控制信号真值表然后用case语句或与或门实现。比如指令RegWriteMemReadMemWriteALUOpMemToRegALUSrcBranchR型10010000lw11000110sw00100X10beq00001X01这张表就是控制单元的全部逻辑。写Verilog时用case语句把opcode映射到这些信号即可。注意sw和beq的MemToReg是无关项因为RegWrite0写回数据不会被使用。3.2 主控制与ALU控制的分离两级译码的设计动机单周期CPU的控制单元通常拆成两个模块主控制单元和ALU控制单元。主控制单元根据opcode产生RegWrite、MemRead、MemWrite、MemToReg、ALUSrc、Branch和ALUOpALU控制单元根据ALUOp和funct产生具体的ALU操作码。这种分离的设计动机是R型指令的opcode都是000000具体运算由funct决定如果主控制单元直接产生ALU操作码就需要同时看opcode和functcase分支会爆炸。分离之后主控制单元只负责“这是R型指令ALUOp10”ALU控制单元再根据funct细分。// ALU控制单元根据ALUOp和funct产生具体操作码 module alu_control( input [1:0] alu_op, input [5:0] funct, output reg [2:0] alu_ctrl ); always (*) begin case (alu_op) 2b00: alu_ctrl 3b010; // 加法lw/sw/addi 2b01: alu_ctrl 3b110; // 减法beq 2b10: begin // R型看funct case (funct) 6b100000: alu_ctrl 3b010; // add 6b100010: alu_ctrl 3b110; // sub 6b100100: alu_ctrl 3b000; // and 6b100101: alu_ctrl 3b001; // or 6b101010: alu_ctrl 3b111; // slt default: alu_ctrl 3b010; endcase end default: alu_ctrl 3b010; endcase end endmodule这种两级译码的代价是多了一层组合逻辑延迟但在单周期CPU里时钟周期本来就由最慢路径决定多一层ALU控制逻辑通常不是关键路径。关键路径往往是取指后的译码、寄存器堆读取、ALU计算、Data Memory读取、写回选择器这一长串组合逻辑。3.3 分支指令的处理单周期CPU的“后悔药”问题beq指令在单周期CPU里有个特殊问题分支是否跳转取决于ALU的零标志而ALU的结果要到周期后半段才稳定。但PC的更新是在时钟沿也就是说当前周期的beq指令只能影响下一个周期的PC。这没问题因为单周期CPU本来就是每个周期更新一次PC。但分支延迟槽的概念在这里不存在——MIPS32的beq指令在单周期CPU里就是“当前周期算出零标志下一个周期PC跳转或不跳转”。// PC选择逻辑根据Branch和Zero决定next_pc module pc_mux( input [31:0] pc_plus4, input [31:0] branch_target, input branch, input zero, output [31:0] next_pc ); // 分支条件Branch1且Zero1时跳转 assign next_pc (branch zero) ? branch_target : pc_plus4; endmodule这里有个常见的踩坑点branch_target的计算。MIPS32的分支目标地址是PC4 (sign_ext(imm) 2)注意是PC4而不是PC。因为PC在取指阶段已经指向当前指令而分支偏移是相对于下一条指令的。如果写成PC (imm 2)分支目标会偏移4字节导致跳转到错误地址。这个坑在仿真波形里表现为beq跳转后指令序列错位执行了不该执行的指令。4. 避坑与排查单周期CPU设计里那些让人抓狂的瞬间4.1 现象仿真波形里寄存器值全是X原因寄存器堆的写端口没有初始化或者复位逻辑不完整。在Verilog里reg类型变量默认是X如果寄存器堆的写操作没有在复位时清零仿真时读出的就是X。解决在寄存器堆模块里加复位逻辑或者用initial块给寄存器赋初值仅仿真用。更稳妥的做法是寄存器堆的写操作只在时钟沿且RegWrite1时发生读操作组合逻辑输出但读出的值在复位后应该是0。4.2 现象lw指令写回的数据不对总是差一个地址原因Data Memory的地址对齐问题。MIPS32的lw指令要求地址4字节对齐但ALU算出的地址可能不是4的倍数。如果Data Memory按字节寻址而lw按字读取就需要把地址的低2位忽略。常见的错误是直接拿ALU结果作为Data Memory的地址索引没有做addr[31:2]的截断。解决在Data Memory的读地址上做字对齐处理或者确保ALU算出的地址本身就是4的倍数。4.3 现象beq指令跳转后下一条指令执行了两次原因PC更新逻辑和指令存储器读取时序不匹配。如果指令存储器用时序逻辑读取PC更新后指令要下一个周期才输出导致beq跳转后多执行一条指令。解决确保指令存储器是组合逻辑读取PC在时钟沿更新这样当前周期的指令已经稳定输出下一个周期PC跳转后指令存储器立即输出新地址的指令。4.4 现象控制信号出现毛刺导致寄存器堆误写原因组合逻辑控制单元在输入变化时产生毛刺如果毛刺出现在时钟沿附近可能导致RegWrite瞬间为1寄存器堆误写。解决在控制单元的输出加寄存器打拍或者确保时钟沿到来时控制信号已经稳定。更常见的做法是控制单元的输出直接连到寄存器堆的写使能但寄存器堆的写操作只在时钟沿采样毛刺在时钟沿之前已经稳定。4.5 现象仿真通过但综合后时序不满足原因单周期CPU的关键路径太长组合逻辑延迟超过时钟周期。解决分析关键路径通常是取指后的译码、寄存器堆读取、ALU计算、Data Memory读取、写回选择器这一串。优化方法包括把Data Memory的读操作提前或者把ALU的某些运算拆成两级。但在单周期CPU里这些优化空间有限更实际的做法是降低时钟频率或者改用多周期/流水线设计。5. 从单周期到流水线这个“慢”设计到底值不值得深挖单周期CPU设计的最大价值不在于性能而在于它把计算机体系结构里最核心的概念——数据通路、控制信号、指令周期——用最直白的方式呈现出来。你不需要理解流水线冒险、分支预测、乱序执行就能看到一条指令从取指到写回的完整路径。这份实验报告用Verilog HDL把MIPS32单周期CPU的每个模块都拆开从PC到指令存储器从控制单元到ALU从寄存器堆到Data Memory每一段代码都对应着数据通路上的一段物理连线。从设计取舍的角度看单周期CPU的“一个周期跑完一条指令”是一个极端的约束。它牺牲了时钟频率因为周期长度由最慢指令决定换来了控制逻辑的极度简化纯组合逻辑无状态机。这种取舍在教学场景下是合理的因为它让初学者能在一张图里看到所有数据流动。但在实际处理器里这种设计不可行——时钟周期会被lw指令拖到很长而add指令本来可以跑得更快。流水线设计正是为了解决这个矛盾把一条指令拆成多个阶段每个阶段一个周期不同指令在不同阶段重叠执行。但流水线带来的冒险问题数据冒险、控制冒险、结构冒险又需要额外的转发、停顿、分支预测逻辑。单周期CPU没有这些问题因为它每个周期只执行一条指令所有操作在一个周期内完成不存在指令重叠。如果你能拿到这份实验报告的完整代码和文档建议重点看三个地方一是控制单元的真值表它直接对应指令集架构的定义二是数据通路的连线图它展示了指令字段如何一步步变成硬件动作三是仿真波形它验证了每个周期内信号的变化是否符合预期。我一般会这么理解单周期CPU是计算机体系结构的“ Hello World”它不追求性能但追求逻辑的完整性。从那以后我每次看流水线处理器的冒险逻辑都会先回想单周期CPU里那条“一个周期跑完”的路径然后问自己如果拆成五个周期哪些信号需要锁存哪些冒险需要处理。希望帮到你。本文还有配套的精品资源点击获取