
简介面向计算机组成原理课程设计与FPGA入门者的32位CPU Quartus工程代码基于Verilog实现覆盖从代码编写、仿真验证到下载配置的主要环节。压缩包共917个文件约27.63MB既包含v、vhd等核心源码文件也有qpf、qsf工程配置、vwf仿真波形、mif初始化数据以及sof比特流文件便于快速打开工程并复现CPU运行流程已有605人学习下载。其中cpu1.1与cpu目录分别对应早期和优化版本可对照学习处理器取指、译码、执行、访存等阶段以及算术逻辑单元、控制单元、寄存器组之间的协同工作。通过阅读和运行代码还能掌握CPU如何解析指令、管理内存访问、生成控制信号并巩固指令集体系结构、存储层次、总线协议等计算机组成原理核心概念压缩包内还附有txt与readme说明文件方便定位各目录功能。整体适合课程设计、毕业设计或自学提升也可在现有工程基础上扩展指令集或尝试流水线改造。 玩FPGA的应该都见过这类资源——“32位CPU-Quartus代码完整版.rar”看到这个名字就知道压缩包里放的是一套可以在Quartus里直接打开、仿真、甚至烧到板子上的32位处理器设计。用了一段时间这类代码之后我最大的感受是网上流传的版本水平参差不齐有些确实能跑通有些打开全是报错还有些所谓“完整版”其实只是把某本教材的示例代码重新打个包。这篇博文我想结合自己折腾这类工程的经验把这套32位CPU工程从设计思路、代码结构、仿真调试到上板验证的全流程梳理一遍重点说清楚那些压缩包老师不会告诉你的细节。这类项目适合两类人一是正在学计算机组成原理想做课程设计但不知从哪下手的同学二是刚接触FPGA想通过一个较完整的复杂设计来理解时钟、复位、状态机和总线这些概念的自学者。不管你是哪类把这套代码吃透远比重新造一个轮子更有价值。1. 项目核心定位代码背后到底是一颗什么样的CPU1.1 从压缩包名字能读出哪些信息先说名字里的“32位CPU”到底指什么。这里的32位指的是CPU的数据总线宽度和寄存器位宽也就是处理器一次能处理32比特的数据ALU的输入输出、通用寄存器组、数据存储器的接口全都是32位的。这不等于指令长度一定是32位很多教学型CPU采用定长32位指令但也有一些设计会做成16位指令、32位数据以减少程序存储器占用。拿到工程先看pc寄存器每次加几就能判断指令编码方式。“Quartus代码”说明这个工程是为Altera/Intel FPGA准备的主要使用Verilog HDL或VHDL编写。Quartus是FPGA的综合与布局布线工具和Xilinx的Vivado是对应关系。一个完整的工程文件应该是.qpf和.qsf结尾前者是工程文件后者是设置文件里面记录了芯片型号、引脚分配、编译选项这些信息。如果压缩包里只有.v后缀的源码没有工程文件那你就需要自己新建工程并手动添加源文件后面我会详细说这个操作流程。1.2 这类CPU工程通常采用什么架构课程设计级别的32位CPU十有八九是经典MIPS五级流水线结构也就是取指IF、译码ID、执行EX、访存MEM、写回WB这五个阶段。之所以MIPS架构这么流行是因为它的指令格式规整三种格式 —— R型、I型、J型 —— 字段位置固定译码逻辑非常简单特别适合在FPGA上实现。不过我在实际打开多个版本后发现很多“完整版”实际上是单周期而非流水线实现。单周期的特点是每条指令在一个时钟周期内完成控制信号用一个大case语句根据opcode和funct字段生成结构清晰但主频很低。流水线版本则会多出IF/ID、ID/EX、EX/MEM、MEM/WB四组流水线寄存器还要处理数据冒险和控制冒险。如果你在代码里看到if_id_reg、id_ex_reg这类命名基本可以确定是流水线设计。1.3 为什么网上的工程多是教学型CPU而不是开源硬核这个问题很关键。你下载到的工程大概率不会实现完整的MIPS III指令集而是会裁剪到十几条核心指令比如lw、sw、add、sub、and、or、slt、beq、j这些。原因很好理解教学目的只需要让学习者掌握数据通路和控制器的设计方法而不是处理异常、中断、MMU这些工业级CPU才需要的复杂机制。还有一个原因这套代码设计出来主要是配合教材使用的。国内很多计算机组成原理教材比如唐朔飞、袁春风老师的版本都有配套的Verilog代码网上的“完整版”很多就是这些代码的整理或改写。这类代码的特点是仿真友好、注释量适中、模块划分清晰。换句话理解它不是用来跑Linux的而是用来跑一段排序程序或循环累加程序验证CPU功能正确性。2. 核心细节解析与实操要点2.1 模块划分拿到代码后先看哪些文件打开压缩包不要急着一个个点开看代码先按文件类型做分级整理。一个规范的32位CPU工程通常包含以下几类文件文件/目录作用重要程度top.v或cpu_top.v顶层模块例化所有子模块并连接最先看alu.vALU算术逻辑单元核心运算部件重点看regfile.v寄存器堆32个32位寄存器重点看ctrl.v或control.v控制器生成所有控制信号重点看ins_mem.v/data_mem.v指令存储器和数据存储器了解即可pc.v程序计数器了解即可tb_*.v或*_testbench.v测试激励文件非常重要.qsf/.qpfQuartus工程文件先验证版本兼容性.do或.tclModelSim脚本或者Quartus脚本可选看代码的正确顺序是先找顶层模块看子模块例化关系弄清楚数据流方向。然后看alu.v因为ALU的功能最直观——加减法、逻辑运算、移位、比较。接着看ctrl.v这个模块决定了每条指令对应什么控制信号。最后再看存储器和寄存器堆因为这两个模块结构比较固定不容易出错。2.2 控制器设计一条指令是怎么被翻译成控制信号的控制器是CPU设计的灵魂也是最容易写乱的部分。常见的实现方式是组合逻辑加case语句根据opcode[5:0]和funct[5:0]生成reg_write、alu_src、mem_write、mem_to_reg、branch这些控制信号。以一条add指令为例它的opcode是000000funct是100000那么控制器会拉高reg_write让alu_src 0操作数来自寄存器而不是立即数alu_op设置为加法运算mem_write拉低mem_to_reg拉低写回数据来自ALU输出而不是存储器读取值。这里有一个实操中经常踩的坑有些代码会直接用opcode判断所有指令忽略了R型指令需要额外看funct字段。如果只判断opcode 6b000000那么add和sub的控制信号完全一样ALU功能选择就会出错。正确的做法是设计一个内部信号alu_control由控制器根据opcode和funct共同确定然后传给ALU模块。检查代码时重点看这两者的组合逻辑是否写全。2.3 数据通路中的关键抉择单周期还是流水线如果你拿到的是单周期代码整个设计最大的特点就是一个时钟周期完成一条指令的全部操作。比如一条lw指令需要依次完成取指、读寄存器、ALU计算地址、读数据存储器、写回寄存器全部在一个周期内搞定。这种情况下时钟周期必须设计成能容纳最慢指令所需的时间也就是lw的完整通路延迟所以主频通常只能跑到几十MHz。好处是控制逻辑极其简单非常适合第一次尝试做CPU设计的同学。流水线版本就复杂得多。这里重点说三个冒险问题数据冒险、控制冒险、结构冒险。数据冒险最简单的解决办法是在EX阶段后面加转发逻辑也就是把ALU输出或访存结果直接旁路到EX阶段入口而不是等写回完成后再读寄存器。控制冒险在分支指令时尤其明显解决方案可以是插入气泡flush、延迟分支或者用分支预测。在FPGA上做类MIPS流水线我建议至少实现ALU结果的转发否则像add r1, r2, r3; sub r4, r1, r5这种连续依赖的程序会直接算出错误结果。2.4 存储器的实现用IP核还是用Verilog数组指令存储器和数据存储器有两种常见实现方式。一种是在Verilog里直接声明reg [31:0] mem [0:1023]然后用$readmemh或$readmemb加载初始化文件这种方式仿真非常方便修改程序只需要改.hex或.dat文件。另一种是在Quartus里调用存储器IP核比如altsyncram这种方式的优势是能利用FPGA内部的Block RAM资源综合后占用的逻辑单元更少但仿真时需要额外处理IP核的初始化文件格式。如果是做课程设计我个人强烈建议直接用Verilog数组不要为了节省LE而去折腾IP核。原因有两个第一教学型CPU需要的存储器容量很小只有几百个字的程序用分布式RAM实现完全够用第二Verilog数组的读写行为非常直观方便对照教科书理解数据通路而IP核会多出读写使能、时钟使能、字节使能这些额外信号对初学者来说增加了无谓的理解负担。3. Quartus实操全流程从建工程到上板验证3.1 新建工程的正确姿势与版本选择打开Quartus选择File - New Project Wizard。依次填写工程目录、工程名称和顶层实体名称这里要注意顶层实体名称必须和顶层模块名完全一致否则综合时会报cant elaborate user hierarchy。如果压缩包里自带工程文件建议先尝试直接打开.qpf但要注意版本兼容问题。版本选择上我的经验是Quartus II 13.1 是比较好用的版本它配合ModelSim-Altera 10.1d使用对Cyclone III和Cyclone IV系列FPGA支持很好网上绝大多数教学资料也是基于这个版本写的。如果你的芯片是Cyclone IV E系列用13.1不会遇到任何device兼容问题。如果非要使用高版本Quartus比如Prime 18.1虽然功能更强但新版本对老芯片的支持有限而且仿真工具的配置方式变化很大不建议课程设计阶段折腾。遇到not compatible with the device这类报错时不要急着换软件版本。先打开Assignments - Device确认所选芯片型号和工程设定一致。再检查.qsf文件里有没有类似set_global_assignment -name DEVICE_FILTER_PACKAGE FBGA这样的约束删掉多余的器件筛选条件让Quartus自动匹配90%的情况能解决兼容问题。3.2 顶层连接与引脚分配顶层模块的作用是把CPU核心和外界连起来。教学型CPU一般不需要真正连接外部引脚仿真时只需要给时钟和复位信号即可。但如果要上板就必须做引脚分配。比如使用DE2-115开发板就要把时钟信号连到板载50MHz晶振对应的引脚通常是PIN_Y2和PIN_AB22复位信号连到SW按键指令存储器的只读输出可以连到LED或七段数码管显示。引脚分配有两种方式图形界面方式和Tcl脚本方式。图形界面在Assignments - Pin Planner里操作简单直观Tcl脚本更适合反复修改比如set_location_assignment PIN_AB22 -to clk。我个人建议用Tcl脚本因为如果用错引脚重新编译的时间成本很高脚本版本可以快速修改和追溯。3.3 编写Testbench与ModelSim联调仿真这一步是验证CPU设计正确性的关键也是很多初学者卡壳最多的地方。一个合格的testbench至少要有以下结构声明clk、reset_n等信号初始化CPU寄存器然后产生时钟和复位信号。时钟推荐用always #10 clk ~clk;生成周期20ns对应50MHz频率。复位信号要复位至少一个完整时钟周期确保所有状态机回到初始状态。典型的讲义级验证方式如下先在ins_mem.v里用$readmemh(program.hex, mem)加载测试程序然后在Testbench里观察通用寄存器r[1]、r[2]的值。比如写一段计算1 2 3 ... 10的循环程序跑完仿真后检查最终结果是否等于55。如果结果正确说明数据通路和控制信号的组合基本没有大问题。这里有一个非常实用的调试技巧在ModelSim里把cpu_top.regfile.regs这个数组信号拖到Wave窗口可以实时看到32个寄存器的变化。具体路径取决于顶层例化命名一般为u_cpu/u_regfile/regs。这是定位“哪条指令执行错了”最快的办法比单步调试效率高得多。3.4 利用SignalTap在线调试如果仿真通过但上板后功能不对就需要用到SignalTap II逻辑分析仪。这个工具相当于FPGA内部的示波器它在综合时把探针信号嵌入设计然后通过JTAG把数据实时上传到Quartus界面显示。使用SignalTap抓数的步骤如下File - New - SignalTap II Logic Analyzer File。双击空白处添加信号建议抓取PC值、当前指令、写使能、ALU结果这几个关键信号。然后设置采样时钟为系统时钟采样深度设为1K或2K设置触发条件为PC 某地址或者reg_write上升沿。配置完成后重新编译然后烧录FPGA打开SignalTap并运行采集。如果发现PC跑飞第一步就要检查复位信号是否正常释放如果PC不跳变大概率是时钟没有进来先查时钟引脚配置。4. 常见问题与排查技巧实录4.1 仿真波形全是高阻或不定态这个问题几乎每个初学CPU的人都会遇到。检查顺序应该从下往上首先确认Testbench里有没有正确初始化CPU内部信号比如寄存器堆的初始值。很多寄存器堆实现用integer i; for(i0; i32; ii1) rf[i] 0;来初始化如果忘写这段寄存器在仿真开始时就是不定态x。其次确认复位是否拉低足够时间有些芯片是低电平复位有些是高电平复位搞反了会导致所有状态机卡死。最后检查是否有模块漏掉了时钟信号连接比如存储器的时钟和CPU核心时钟不是同一个信号导致读取出错。4.2 编译时大量警告信号被裁剪或锁存器推断Quartus编译报告里出现Warning (10240): Verilog construct always(negedge clk) requires an event control这类警告通常是因为敏感列表写法不规范。还有一类常见警告是Inferred latch for xx这说明你的case语句没有写default分支或者在某个分支下某些信号没有赋值。组合逻辑的always块里如果没有覆盖所有分支综合器会推断出锁存器这在CPU控制器里是非常严重的错误会直接破坏数据通路。排查锁存器的办法很简单在Quartus里打开Chip Planner如果看到某个信号以LCELL方式实现而不是组合逻辑等效形式就说明推断出了锁存器。修复方法是在always (*)块开头先给所有输出赋默认值再用case语句覆盖特殊条件这样就不会产生锁存器。4.3 CPU能仿真的指令和上板后对不上这种情况最常见的原因是存储器初始化文件没有正确加载到板上。很多课程设计代码在仿真时用$readmemh加载测试程序但上板时$readmemh只能初始化FPGA配置时的RAM初始值如果你的存储器实现用的是异步读、同步写的逻辑综合后RAM的初始内容可能与仿真时完全不同甚至默认全部为0。解决办法有两个一个是在Quartus工程中将存储器模块的初始值设为固定值建议在代码里写initial begin ... end但要注意这种写法只对仿真有效另一个更稳妥的方案是把测试程序硬编码在ROM逻辑里也就是用case语句直接根据地址输出指令。比如always (*) begin case(addr) 5d0: instr 32h3c010000; ... endcase end虽然代码丑一点但综合后就是一块真正的只读逻辑上板效果和仿真完全一致。4.4 上板后时钟和复位问题板级调试遇到死机或PC不跑最值得怀疑的是按键抖动。用板载按键做复位时机械按键弹跳产生的毛刺会让CPU刚复位就进入错误状态建议添加一段简单的消抖逻辑采样到按键变化后持续20ms稳定再确认电平。时钟方面如果使用分频时钟要确认分频逻辑是否放置正确错误的时钟域交叉可能导致亚稳态。另一个实用经验是在调试阶段不要追求高频。把分频器先把50MHz降到5MHz甚至1MHz调试难度会大幅降低因为SignalTap在低频下更容易触发。等CPU验证通过后再逐步升频看它最高能跑到多少这也是一个很有意义的实验数据。4.5 从调试到扩展一个可复用的调试流程根据我的经验验证32位CPU的成功与否不在于仿真波形是否完美而在于能否有一套可复现的流程来快速定位问题。我的固定套路是先用一段只有3条指令的测试程序验证最基本的数据通路比如addi r1, r0, 5、addi r2, r0, 3、sub r3, r1, r2然后跑一段带分支和跳转的循环程序验证控制指令最后跑一段调用式程序或冒泡排序程序验证完整功能。每次都观察全部寄存器的值对比预期结果一旦不对用SignalTap抓PC和执行阶段信号基本半小时内能定位问题。如果CPU本身已经稳定还可以做几个扩展方向添加乘法除法指令在ALU里用移位加的方式实现添加简单的堆栈指令支持函数调用甚至可以把调试好的CPU核心打包成IP核用Platform Designer搭建一个SoC。我个人认为把这套基础CPU吃透后再去做这些扩展比一开始就追求复杂设计要高效得多。毕竟数字逻辑设计最忌讳的是在没搞定数据通路的情况下盲目堆代码。本文还有配套的精品资源点击获取