FPGA实现32位CPU:Quartus工程详解与仿真调试全流程
2026/9/7 12:12:15 网站建设 项目流程

简介:面向计算机组成原理课程设计与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. 项目核心定位:代码背后到底是一颗什么样的CPU

1.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_regid_ex_reg这类命名,基本可以确定是流水线设计。

1.3 为什么网上的工程多是教学型CPU,而不是开源硬核

这个问题很关键。你下载到的工程大概率不会实现完整的MIPS III指令集,而是会裁剪到十几条核心指令,比如lwswaddsubandorsltbeqj这些。原因很好理解:教学目的只需要让学习者掌握数据通路和控制器的设计方法,而不是处理异常、中断、MMU这些工业级CPU才需要的复杂机制。

还有一个原因:这套代码设计出来主要是配合教材使用的。国内很多计算机组成原理教材(比如唐朔飞、袁春风老师的版本)都有配套的Verilog代码,网上的“完整版”很多就是这些代码的整理或改写。这类代码的特点是仿真友好、注释量适中、模块划分清晰。换句话理解,它不是用来跑Linux的,而是用来跑一段排序程序或循环累加程序,验证CPU功能正确性。

2. 核心细节解析与实操要点

2.1 模块划分:拿到代码后先看哪些文件

打开压缩包,不要急着一个个点开看代码,先按文件类型做分级整理。一个规范的32位CPU工程通常包含以下几类文件:

文件/目录作用重要程度
top.vcpu_top.v顶层模块,例化所有子模块并连接最先看
alu.vALU算术逻辑单元,核心运算部件重点看
regfile.v寄存器堆,32个32位寄存器重点看
ctrl.vcontrol.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_writealu_srcmem_writemem_to_regbranch这些控制信号。以一条add指令为例:它的opcode是000000,funct是100000,那么控制器会拉高reg_write,让alu_src = 0(操作数来自寄存器而不是立即数),alu_op设置为加法运算,mem_write拉低,mem_to_reg拉低(写回数据来自ALU输出而不是存储器读取值)。

这里有一个实操中经常踩的坑:有些代码会直接用opcode判断所有指令,忽略了R型指令需要额外看funct字段。如果只判断opcode == 6'b000000,那么addsub的控制信号完全一样,ALU功能选择就会出错。正确的做法是设计一个内部信号alu_control,由控制器根据opcodefunct共同确定,然后传给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。依次填写工程目录、工程名称和顶层实体名称,这里要注意顶层实体名称必须和顶层模块名完全一致,否则综合时会报can't 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至少要有以下结构:声明clkreset_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(i=0; i<32; i=i+1) 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) 5'd0: instr = 32'h3c010000; ... endcase end,虽然代码丑一点,但综合后就是一块真正的只读逻辑,上板效果和仿真完全一致。

4.4 上板后时钟和复位问题

板级调试遇到死机或PC不跑,最值得怀疑的是按键抖动。用板载按键做复位时,机械按键弹跳产生的毛刺会让CPU刚复位就进入错误状态,建议添加一段简单的消抖逻辑:采样到按键变化后持续20ms稳定再确认电平。时钟方面,如果使用分频时钟,要确认分频逻辑是否放置正确,错误的时钟域交叉可能导致亚稳态。

另一个实用经验是在调试阶段不要追求高频。把分频器先把50MHz降到5MHz甚至1MHz,调试难度会大幅降低,因为SignalTap在低频下更容易触发。等CPU验证通过后再逐步升频,看它最高能跑到多少,这也是一个很有意义的实验数据。

4.5 从调试到扩展:一个可复用的调试流程

根据我的经验,验证32位CPU的成功与否,不在于仿真波形是否完美,而在于能否有一套可复现的流程来快速定位问题。我的固定套路是:先用一段只有3条指令的测试程序验证最基本的数据通路,比如addi r1, r0, 5addi r2, r0, 3sub r3, r1, r2;然后跑一段带分支和跳转的循环程序,验证控制指令;最后跑一段调用式程序或冒泡排序程序验证完整功能。每次都观察全部寄存器的值,对比预期结果,一旦不对,用SignalTap抓PC和执行阶段信号,基本半小时内能定位问题。

如果CPU本身已经稳定,还可以做几个扩展方向:添加乘法除法指令,在ALU里用移位加的方式实现;添加简单的堆栈指令支持函数调用;甚至可以把调试好的CPU核心打包成IP核,用Platform Designer搭建一个SoC。我个人认为,把这套基础CPU吃透后再去做这些扩展,比一开始就追求复杂设计要高效得多。毕竟数字逻辑设计最忌讳的,是在没搞定数据通路的情况下盲目堆代码。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询