单周期CPU设计全解析:MIPS32指令集、数据通路与Verilog实现
发布时间:2026/10/15 12:53:19 锦皓数字建站

简介面向计算机组成原理课程学习者与Verilog HDL入门者这份实验报告完整记录MIPS32单周期CPU从指令分析、模块设计到仿真验证的全过程。资源以Verilog HDL为主语言实现了十六条常用指令的取指、译码、执行与写回通路并展示了寄存器堆、ALU、控制单元、数据存储器等核心模块的代码组织与连接方式可帮助读者系统理解单周期处理器的工作原理与逻辑功能实现。包体共90个文件以v源码、qpf/qsf工程配置、rpt编译报告、vwf仿真波形、docx实验报告及jpg运行截图为主压缩包仅2.37MB文件类型涵盖Quartus工程各阶段生成产物其中docx报告详细记录了设计思路仿真波形与测试截图则便于逐段对照调试。已有12292人学习下载适合正在完成同类实验、复试备考或初次接触MIPS32架构的同学参考借鉴。1. 单周期CPU设计教学实验里最硬的骨头也是理解MIPS32的钥匙单周期CPU设计几乎是每个计算机组成原理实验里的终极大作业也是我见过挂科率最高的一个实验。很多同学把数据通路图画了三个星期还没搭好问题往往不在代码而在动手前没把指令集和信号之间的关系想清楚。这份《计算机组成原理》实验报告正好覆盖了从MIPS32指令子集选定、数据通路设计、Verilog HDL代码实现到仿真验证的全过程连实验报告怎么排版、波形图怎么贴、问题分析怎么写得像回事都有完整示例。它适合两类人一类是被课程设计逼到墙角、需要一份能复现的完整参考的本科生另一类是面试前想快速把单周期CPU整条链路串起来的求职者——这类资源不是让你抄是让你在有限时间里看清一个完整CPU到底由哪些部件构成。2. 指令集子集选定先定8条指令再谈数据通路映射2.1 为什么选8条而不是全量MIPS32做单周期CPU的第一步永远不是写代码而是决定CPU支持哪些指令。全量MIPS32有几十条指令单周期实验中全部实现既不现实也没有必要。这份实验报告选择的是一个经过剪裁的8条指令子集这也是国内多数高校课程设计里的常规选择。8条指令刚好覆盖三种指令格式R型、I型、J型又能让控制信号的数量控制在十个左右数据通路图也不会乱到看不清。选指令有个原则每条指令都要带来新的控制信号组合。比如lw和sw这对访存指令决定了需要MemRead、MemWrite、MemtoReg三个信号beq引入了Branch信号和ALU减法比较逻辑j引入了跳转地址拼接逻辑。如果只选算术逻辑指令数据通路会简单一半但你也学不到访存和分支的核心机制评分通常也不会高。2.2 指令格式拆解与译码信号的来源MIPS32指令统一为32位格式固定这是它适合教学的根本原因。R型指令的31到26位是opcode25到21位是rs20到16位是rt15到11位是rd10到6位是shamt5到0位是funct。I型指令没有rd和shamt15到0位直接是16位立即数。J型指令的26到0位是跳转目标地址的低26位。这个实验报告里最值得看的部分是它把每条指令分列成一张表标记opcode、funct、需要的控制信号。我一般会让读者先自己做一遍这张表因为后面写Verilog控制单元时代码几乎就是这张表的逐行翻译。比如add指令的opcode是6b000000funct是6b100000RegDst为1ALUSrc为0RegWrite为1而addi的opcode是6b001000RegDst为0ALUSrc为1RegWrite为1——两者差别集中在RegDst和ALUSrc上这就是译码时区分R型和I型的关键。2.3 每条指令需要的数据通路资源清单确定指令集后下一步是逐条列出数据通路资源。我习惯画一张表横向是部件指令存储器、寄存器堆、ALU、数据存储器、PC、立即数扩展单元纵向是每条指令交叉格子里打勾。这份报告里的设计覆盖了如下资源需求指令指令存储器寄存器堆读寄存器堆写ALU运算数据存储器读数据存储器写PC更新方式add是rs, rtrd加否否PC4sub是rs, rtrd减否否PC4and是rs, rtrd与否否PC4or是rs, rtrd或否否PC4slt是rs, rtrd比大小否否PC4lw是rsrt加读否PC4sw是rs, rt否加否写PC4beq是rs, rt否减否否条件跳转这张表的好处是写Verilog前你就能发现寄存器堆必须有两个读端口和一个写端口ALU必须支持加、减、与、或、比较五种运算数据存储器只有lw和sw用得到。如果你发现某个部件只有一条指令用到就该想想这算不算浪费——单周期CPU里每个部件每个周期都在工作哪怕当前指令用不到某个部件它的输入输出也必须保持无害状态。这就是控制信号里为什么要有ALUOp、MemRead这类使能信号的原因。3. 数据通路搭建从PC到寄存器堆的连线先画图再写码3.1 经典单周期结构的三个核心部件很多同学拿到实验报告第一步就看代码这是本末倒置。单周期CPU的灵魂是数据通路图代码只是数据通路的文字化表达。这份报告里有完整的数据通路图核心可以拆成三块取指部分、执行部分、访存回写部分。取指部分由PC、指令存储器、加法器组成。PC输出当前指令地址指令存储器读出32位指令PC4作为下一条指令的地址。关键点在于PC更新和指令读取是同一周期完成的beq跳转时需要把跳转地址通过MUX选择进入PC。执行部分由寄存器堆、立即数扩展单元、ALU组成。寄存器堆读出rs和rt的值立即数扩展把16位立即数符号扩展成32位ALU根据控制信号决定做加法还是其他运算。beq和lw/sw都要经过ALU做加法但beq加出来的是地址lw/sw加出来的是访存地址二者共用同一个ALU反而是单周期结构最优雅的地方——硬件资源不会因为指令不同而改变。访存回写部分是单周期CPU相对复杂的一环。lw读出的数据要写回寄存器堆而add的结果来自ALU所以寄存器堆写数据端口前要放一个MUX用MemtoReg信号选择写回的是内存读出的数据还是ALU结果。同时要注意寄存器堆的写地址也需要MUXR型指令写rdI型指令写rt这个选择信号是RegDst。3.2 PC与指令存储器的读写时机单周期CPU只有一个时钟周期所有操作在一个周期内完成因此PC的更新逻辑最关键。由于没有流水线寄存器每个周期开始时PC输出当前指令地址组合逻辑完成译码、执行、访存后在时钟上升沿到来时把下一条指令地址写入PC。这个“边沿触发写、电平期间读”的机制是整个设计的基石。写Verilog时PC部分最常见的写法是这样的always (posedge clk or posedge rst) begin if (rst) pc 32h0000_0000; // 复位PC到0 else pc next_pc; // next_pc由组合逻辑计算 end这里的next_pc是在组合逻辑里计算的。默认情况下是pc 4当Branch信号为1且ALU输出的Zero信号为1时next_pc应该变成pc 4 (sign_extended_offset 2)当Jump信号为1时next_pc是{pc[31:28], instruction[25:0], 2b00}。注意J型指令的跳转地址是绝对地址拼接不是相对偏移容易和beq搞混。3.3 寄存器堆、ALU与数据存储器的信号关系寄存器堆是异步读、同步写的典型部件。异步读意味着读地址一变化读数据马上跟着变不依赖时钟同步写意味着只有在时钟上升沿且RegWrite为1时写地址对应的寄存器才会被更新。这个特性要求写代码时把读逻辑放在组合逻辑的always块里把写逻辑放在时序逻辑的always块里二者不能混在同一个块里。ALU的控制需要单独理解。ALUOp是两级信号由控制单元根据opcode生成再交给ALU控制单元结合funct字段生成最终的ALU控制信号。实验中比较通用的映射表是ALUOp为2b00时ALU做加法对应lw和swALUOp为2b01时ALU做减法对应beqALUOp为2b10时需要查看funct字段决定加法、减法、与、或、比较中的哪一种。ALU控制信号一般用4位比如0010代表加0110代表减0000代表与0001代表或0111代表小于比较。数据存储器的读和寄存器堆类似读是组合逻辑写是同步逻辑。特别要注意的是数据存储器的读地址是ALU的运算结果而不是PC。lw和sw的访存地址都是寄存器值加立即数偏移所以数据存储器的地址线直接接ALU输出MemRead和MemWrite分别控制读使能和写使能。4. Verilog实现控制信号真值表与可综合代码的五个边界4.1 控制单元真值表先写Excel再写代码控制单元是单周期CPU最容易写乱的部分。常见误区分两类一类是把控制逻辑写成if_opcode_equals...的瀑布结构代码冗长且容易漏信号另一类是想用case一把梭但没有整理过真值表写到一半发现某个指令的控制信号对不上。我见过最稳的流程是先建一个Excel表格把8条指令对应9个控制信号全部列出再照着表格写case语句。这份实验报告里也有同样的痕迹——它的控制信号表做得非常规整甚至可以直接拿来当测试基准。以这份报告的8条指令为例控制信号真值表的关键几行如下指令RegDstALUSrcMemtoRegRegWriteMemReadMemWriteBranchJumpALUOpR型1001000010lw0111100000swX1X0010000beqX0X0001001jXXX00001XX注意sw的RegDst和MemtoReg是X无关项因为这条指令不写寄存器堆这两个信号是什么无所谓。写代码时X可以直接填0不影响正确性。j的ALUOp同理可能是X因为跳转指令不需要ALU运算填00也不会出错。有了这张表控制单元的代码只是翻译工作always (*) begin // 默认所有控制信号为0逐条指令覆盖 RegDst 1b0; ALUSrc 1b0; MemtoReg 1b0; RegWrite 1b0; MemRead 1b0; MemWrite 1b0; Branch 1b0; Jump 1b0; ALUOp 2b00; case (opcode) 6b000000: begin // R型指令按funct子译码 RegDst 1b1; RegWrite 1b1; ALUOp 2b10; end 6b100011: begin // lw ALUSrc 1b1; MemtoReg 1b1; RegWrite 1b1; MemRead 1b1; ALUOp 2b00; end 6b101011: begin // sw ALUSrc 1b1; MemWrite 1b1; ALUOp 2b00; end 6b000100: begin // beq Branch 1b1; ALUOp 2b01; end 6b000010: begin // j Jump 1b1; end default: begin // 预留未定义指令保持默认值 end endcase end这段代码的默认赋值习惯值得学习。很多人喜欢在每个case分支里把9个信号全部重新赋值一遍代码既长又容易漏。把默认值放在case之前case里只需要写“与默认值不同”的信号可读性和可维护性都高一个档次。这里lw的MemtoReg和ALUSrc设成了1而RegDst用的是默认值0——因为lw写回寄存器堆时写的是rt不是rd。4.2 可综合代码写法避免仿真与上板不一致仿真通过但上板不工作的案例在单周期CPU实验里极其常见绝大多数根因出在代码风格上。报告里最典型的可综合代码问题有两个一是initial块里的初始化语句二是clk和rst在多个always块里被重复使用。针对仿真和上板行为不一致的问题我一般坚持这些习惯// 全局通用参数定义 define OP_WIDTH 6 define FUNC_WIDTH 6 define DATA_WIDTH 32 module cpu( input wire clk, input wire rst, output wire [31:0] pc_out, // 对外可观察PC便于调试 output wire [31:0] alu_out // 对外可观察ALU结果 );// 寄存器堆读采用组合逻辑写采用时序逻辑 reg [31:0] regfile [0:31]; integer i; always (posedge clk or posedge rst) begin if (rst) begin for (i 0; i 32; i i 1) begin regfile[i] 32b0; // 不是用initial而是用复位清零 end pc 32b0; end else if (RegWrite (write_reg ! 5b0)) begin regfile[write_reg] write_data; end end很多人喜欢用initial begin ... end对寄存器堆和PC做初始赋值。仿真时initial确实有效但上板时initial块经常被综合工具跳过导致上电后寄存器堆是随机值仿真里永远复现不出来的现象随之出现。正确做法是在rst信号的时序块里做初始化上电后复位一次所有状态回到已知值。另外要留意写寄存器堆的if里多了个write_reg ! 5b0的条件。MIPS32的$0寄存器恒为0即使RegWrite为1也不应该被写入。这个边界在处理lw $0这类指令时能避免莫名其妙的数值改乱。5. 仿真与避坑波形检查、初始化问题与最常翻车的四个细节5.1 仿真波形先检查这三个信号写完代码第一件事不是急着上板而是跑仿真。仿真时只看最终结果是不行的必须看波形。我建议按顺序检查三个信号PC是否按预期递增、控制信号是否在正确周期拉高、寄存器堆写数据是否与指令语义一致。这个顺序的依据是PC错了说明取指链路存在问题控制信号错了说明译码链路存在问题写回错了说明回写链路存在问题逐层排查才能定位根因。仿真testbench里可以对照指令序列写一份期望输出。比如程序第一条指令是addi $t0, $zero, 5那么第一个周期结束后$t0应该变成5第二条指令是add $t1, $t0, $t1那么第二个周期结束后$t1应该变成5加初值。写期望输出时不要自己算要用MIPS模拟器或手工按规则算清楚避免“仿真结果和手算结果一致但手算本来就是错的”这种尴尬局面。5.2 初始化与复位黑匣子启动的玄学单周期CPU上板后的“玄学”问题九成是复位和初始化造成的。很多同学在testbench里用initial给所有信号赋了个初值仿真正常一上板就乱跑。原因在上章已经提过initial块不可综合。另一个常见玄学是复位信号的异步复位和同步释放没有处理好。实验报告里常用的做法是让rst信号在仿真文件里先拉高几个时钟周期再拉低initial begin rst 1; #100; rst 0; // 之后开始执行程序 end这行代码的意义是让CPU进入一个已知状态。单独看它很简单但很多人会犯一个低级错误rst只拉高了1个时间单位还没等到第一个时钟上升沿就拉低复位没起到作用。如果testbench里同时有rst和clk我习惯让rst保持至少两个完整时钟周期保证第一个上升沿到来时CPU处于复位状态。5.3 常见翻车现象与解决记录这一节是文章里最值得反复看的部分我在做这个实验时踩过的坑和帮别人调试时见的坑集中记录如下现象一程序前几条指令执行正确从某条指令开始寄存器值全部错乱原因寄存器堆的写地址MUX接错。很多人在做R型指令时明明RegDst应该为1选rd结果选成了rt或反过来。lw写rt、add写rd如果两个地址混了后续所有依赖该寄存器的指令都会算错。 解决把每条指令的写寄存器地址打印到波形里和真值表逐条对照。R型看instruction[15:11]I型看instruction[20:16]确认MUX选择信号和指令类型对齐。现象二beq跳转不生效但控制信号明明是Branch1原因Zero信号没有接对。beq比较两个操作数是否相等ALU做减法后输出Zero为1表示相等。如果ALU减法结果位宽和Zero信号位宽不一致或者Zero信号取自ALU结果的最低位而不是所有位做或非跳转条件判定就会失效。 解决Zero必须是ALU结果32位按位或后取非不是某一位。检查ALU结果最大位宽是32位Zero信号应该用assign Zero ~|alu_result;这个写法在报告里出现过确认自己没写反。现象三j指令跳转地址错了跳到一个奇怪的位置原因J型指令的目标地址拼接错误。MIPS32的j指令目标地址是{pc[31:28], instruction[25:0], 2b00}为什么要拼PC的高4位因为32位地址空间里跳转目标不能脱离当前256MB区域。很多人直接把26位指令字段左移两位没有拼上PC高4位地址自然错乱。 解决检查拼接表达式一定是四个部分拼接顺序是pc[31:28]在最前面指令字段中间末尾补两个0。不要忘了2b00。现象四lw读出的数据永远不变或者读出来的数不对原因把数据存储器的读端口写成了时序逻辑。MIPS32单周期里数据存储器读是组合逻辑地址一变数据马上变如果读数据也在always (posedge clk)里更新读结果会晚一拍出现“当前指令读的是上一条指令地址的数据”。 解决数据存储器要么用wire和assign做读端口要么用两个always块读端口全组合逻辑只有写端口用时钟。用reg声明存储数组时读数据用assign mem_data mem[mem_addr];写数据用always (posedge clk)。现象五上板后板子无输出仿真完全正常原因寄存器堆或PC没有复位逻辑。上电后FPGA内部寄存器是随机值没有复位端口的状态机或寄存器阵列开始乱跑。 解决确认所有状态相关模块都有rst分支。检查PC和寄存器堆的时序块里有没有if (rst) begin ... end没有的情况下上板翻车概率极高。6. 上板验证与调试信号观察、板上自检程序与时间预算6.1 板上调试的环境准备仿真全过之后真正的考验才开始。上板调试时你面对的是一个没有波形的黑匣子只能通过有限的LED或数码管观察结果。这时候我的一套流程已经被验证过多次先说结论先用最基础的指令自检程序再跑复杂程序先观察关键信号到外设再做全功能验证。代码里把pc_out和alu_out引到板子上的LED是性价比最高的调试手段。LED显示PC是否按预期递增或ALU结果是否符合预期比JTAG和逻辑分析仪更快。如果板子LED足够多可以同时接入PC低8位和ALU结果低8位两个信号一起看能迅速判断卡死在哪个周期。6.2 自检程序的设计路线自检程序是上板前的最后一道防线也是这份实验报告里比较出彩的部分。我通常建议按难度分三层设计第一层基础运算。执行addi、add、sub各几条把结果写进不同的寄存器通过这些寄存器的值判断ALU通路和控制单元通路是否正确。第二层访存指令。执行sw把一组已知数据写入内存连续地址再执行lw读回来比较读回值和写入值是否一致。第三层跳转指令。先设计一个简单循环用一个寄存器做计数器循环若干次后跳出观察PC跳转过程中是否有异常停顿。6.3 我保留到现在的检查习惯做完单周期CPU实验后我养成了一个习惯每次写带状态机的Verilog模块都会先整理一张与本章第4节类似的“控制信号真值表”再把它转化成注释写进代码顶部最后才开始写逻辑。从那以后凡是按这个流程走的模块Debug时间至少缩短了三分之二。复现这份实验报告的过程也证实了同一件事单周期CPU的设计没有玄学只有逻辑没理清就动手的仓促。按指令集、数据通路、控制真值表、代码、仿真、上板这个顺序推进每一步都是上一步的验证翻车概率自然大幅下降。希望这份实验报告能帮你把单片机的“单周期”三个字彻底看清楚——有限指令集、固定数据通路、组合逻辑译码、边沿触发更新理解了这四件事单周期CPU也就没有什么秘密了。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。