单周期CPU设计解析:从Quartus工程到FPGA仿真实现
简介基于Quartus II与VHDL实现的单周期CPU设计完整工程包面向数字逻辑/计算机组成原理课程学习者及FPGA入门开发者。设计按单周期处理器结构划分指令寄存器、数据路径与控制单元等模块工程文件可演示取指、译码、执行、访存和写回全流程。压缩包共99个文件以cdb/hdb/qmsg/qsf/tdf等Quartus工程与编译文件为主含VHDL源文件、mif初始化文件、qsf引脚约束、仿真波形vwf及编译报告rpt整体大小约1.51MB。已有389人浏览学习。包内工程结构完整可直接在Quartus II中打开并重新编译、仿真验证是理解单周期CPU工作原理、熟悉EDA设计流程的实用参考案例。1. 一份带着编译残留的Quartus单周期CPU工程能读出什么打开danzhouqiCPU.rar看到的是project4这个工程的一整套编译残留几十个.cnf.hdb、.map.rpt、.sim.qmsg还有三个.tdf文件和一个project4.v.bak。这不是一个干净整洁的代码包更像是一台真实机器跑完一次完整编译与仿真后的现场。单周期CPU的课程设计大多长这样顶层Verilog、AHDL写的多路选择器、.mif初始化的指令存储器、.vwf波形激励以及大量Quartus II自动生成的中间文件。这套东西适合两类人一类是正在做单周期CPU课程设计、需要参考完整工程结构的学生另一类是工作后用Verilog写SoC、想回头看CPU最基本数据通路和控制信号如何落地的工程师。前者能照着.vwf和.mif复现仿真过程后者能从.qsf和综合报告里看出这个设计在FPGA上的真实开销。2. 单周期CPU的数据通路与控制信号先看电路再谈综合2.1 五段式执行如何压缩在单周期内单周期CPU的核心约束是一个时钟周期内完成取指、译码、执行、访存、写回五个阶段CPI恒为1。代价是时钟周期必须覆盖最坏路径的延迟所以这类设计的主频通常上不去。以project4为例project4.v顶层里各种Mux和寄存器堆被串在一条长链上PC先送指令存储器读出的指令经过控制单元产生信号同时寄存器堆读数送ALUALU结果再送数据存储器或直接回写。理解这条数据通路的关键是分清哪些部件在每个周期都工作、哪些部件的输出在当前周期被忽略。寄存器堆总是执行读操作但写使能由RegWrite控制数据存储器总是输出地址对应的内容但MemRead和MemWrite决定这个输出是否有意义ALU永远在算但ALUOp和ALUSrc决定算的是什么、第二个操作数来自寄存器还是立即数。忽略某个输出和真正关断电源是两个概念这也是单周期设计资源利用率低的根本原因。2.2 控制信号真值表与Mux选择的对应关系控制单元本质上是一个巨大的组合逻辑查表以opcode和funct字段为输入输出一拍内所有控制信号。project4工程里有mux_ioc.tdf、mux_s4d.tdf、mux_3nc.tdf三个AHDL文件从命名看分别对应指令/立即数选择、符号扩展或移位后的数据选择、以及4选1类型的通路选择。控制信号与Mux的对应关系大致如下表控制信号值为0时的行为值为1时的行为影响的MuxRegDst写寄存器号来自rt字段写寄存器号来自rd字段写寄存器地址MuxALUSrcALU第二操作数来自寄存器ALU第二操作数来自立即数ALU输入MuxMemtoReg写回数据来自ALU结果写回数据来自数据存储器写回数据MuxBranchPC PC4跳转到PC4BranchOffsetPC来源MuxMemWrite数据存储器不写入数据存储器写入rt值数据存储器使能RegWrite寄存器堆不写入寄存器堆写入结果寄存器堆使能这些信号在Verilog里通常用case(opcode)实现。关键点是Branch信号必须与ALU的Zero输出做与运算才能决定是否真正跳转。很多初写单周期CPU的同学会在拿Verilog里直接写PC PC 4 offset看起来功能对但不符合MIPS数据通路的控制语义综合出的电路结构也会和教材里的标准通路对不上。比较好的做法是把PC来源拆成清晰的两路用Mux选择。2.2.1 ALUOp的编码陷阱ALUOp是控制信号里最容易被想简单的部分。常见做法是定义ALUOp[1:0]用00表示加法、01表示减法、10表示由funct字段决定。问题出在lw、sw、beq这三类指令的funct字段是无效的如果控制逻辑不加区分地把funct传给ALUlw指令就可能被算成一次错误的运算。解决方式是让ALUOp为2b00或2b01时直接忽略funct只有在ALUOp为2b10时才把funct译码成具体运算。这个分支判断写错了仿真单条指令看不出来连续执行序列时星号点就会出现。2.2.2 立即数扩展的符号位处理MIPS的I型指令立即数是16位进入ALU之前要扩展成32位。lw、sw、addi需要符号扩展ori、andi需要零扩展。project4中mux_s4d.tdf的命名暗示这里做了4位移位和数据选择实际工程里通常用{{16{imm[15]}}, imm}拼出符号扩展结果。这个细节在Verilog仿真里几乎不会出错但下载到FPGA板上跑内存读写测试时符号扩展写错会导致地址高位被置1访问到完全错误的内存区域。2.3 为什么TDF文件也能参与综合project4.v是顶层.tdf文件是Altera的AHDL文本描述格式。TDF的.tdf写在Quartus里可以被综合器直接消费和Verilog模块混合布线。这种做法在学生工程里并不罕见老师给的接口模板是AHDL学生用Verilog写核心逻辑两者通过端口名对接。阅读这类工程时找到TDF文件里的SUBDESIGN段和BEGIN段把端口列表翻译成Verilog的module端口声明就能把整个设计的模块边界画出来。3. 从project4.v到qsf单周期CPU工程的层次化拆解3.1 源文件与编译产物的对应关系Quartus II工程目录是出了名的“脏”大量中间文件让初看的人无从下手。抓重点是理解各类文件后缀的含义文件后缀作用是否可恢复.v / .tdf / .vhd源码设计本体是.qpf / .qsf工程文件与设置是.vwf波形激励仿真输入部分可恢复.qsf器件型号、引脚约束是.sof / .pof配置文件烧录用否.mif存储器初始化文件是.rpt / .smsg / .qmsg编译与仿真报告否.cnf.* 系列综合中间网表否project4.map.kpt、project4.fnsim.cdb这类文件全部可以删除删掉后重新编译会再生成。如果你要把工程发别人只打包.v、.tdf、.qsf、.qpf、.mif、.vwf就够了包体积会缩小到原来的十分之一。.qsf里记录了器件家族和引脚分配例如set_global_assignment -name FAMILY Cyclone IV、set_location_assignment PIN_64 -to clk重新打开工程后这些约束会自动加载。3.2 VWF文件是理解设计意图的最佳入口project4.vwf是Vector Waveform File用于Quartus II内置仿真器。它的价值不在文件本身而在里面定义的时间戳、输入信号翻转时刻和预期输出。打开VWF文件重点关注以下几个信号组的时序-- 激励序列示例对应vwf中clk周期20ns的设计 0 ns 全局复位 rst 1 10 ns rst 0pc值开始变化 30 ns 第一条指令取指完成alu_result出现有效值 50 ns 寄存器堆写回rd_data更新 70 ns 第二条指令进入取指阶段上意思很明显复位释放后第一个指令结果的回写要在下一条指令取指之后才生效。如果仿真波形里reg_write_data的更新时间比pc_next变化晚一个周期还多说明控制信号时序有竞争风险。VWF的优势是不需要写testbench直接拖拽信号就能跑劣势是回归困难、不好在CI里自动化。单周期CPU这种小设计用VWF足够但把它当参考理解数据通路的时序关系比单看代码直观得多。3.3 MIF文件初始化指令存储器的机制工程里出现了project4.ram0_rom_pc_ebcb25e5.hdl.mif这是Quartus自动生成的存储器初始化文件。MIF的本质是把CPU要执行的指令逐条列成地址和数据的映射综合时这些内容会被烧进FPGA的BRAM或查找表。MIF的格式非常直观WIDTH32; DEPTH256; ADDRESS_RADIXHEX; DATA_RADIXHEX; CONTENT BEGIN 00 : 20080000; -- addi $t0, $zero, 0 04 : 21090001; -- addi $t1, $t0, 1 08 : 012A4022; -- sub $t0, $t1, $t2 0C : 08000000; -- j 0x0 END;WIDTH和DEPTH要和工程里实例化的ROM IP核参数一致ADDRESS_RADIXHEX表示左侧地址是十六进制。CPU的取指地址按字对齐所以相邻指令地址间隔是4字节。用十六进制写指令码时建议对照MIPS指令编码表逐字段手动编码或用汇编器生成后导入避免手算出错。3.3.1 MIF生成脚本的实用改进手动写几十条指令的MIF很容易出错常见做法是用一段脚本生成。我通常会写一个Python脚本把汇编指令的助记符和操作数解析成机器码直接输出MIF文件。流程图上的处理是解析文本行 - 匹配指令模板 - 拼接字段 - 写入MIF的CONTENT段。这样做的好处是改一条指令只需改源码不用重新手算十六进制尤其在调试循环跳转时节省大量时间。MIF生成后还要确认BRAM的初始化属性是UNINITIALIZED还是ZERO如果设置成ZERO未初始化地址会全部读0在MIPS里0地址的指令是nop循环跳出后可能陷入空转。4. 仿真、时序分析与ModelSim联调把VWF跑出真实波形4.1 使用Quartus内置仿真器处理VWF在Quartus II中选择Processing菜单下的Simulator Tool通过Mode下拉框选择Timing或Functional。Functional仿真需要先运行Generate Functional Simulation Netlist这个步骤会调用.v和.tdf生成网表速度快但不算门级延迟。Timing仿真则会基于布局布线后的网表信号跳变带上真实延迟适合验证异步复位和时钟边沿附近的建立时间。VWF仿真失败的常见原因是激励文件里时钟定义得不符合设计约束。单周期CPU中时钟周期至少大于关键路径如果VWF里设置10ns周期而时序分析报告显示关键路径是18ns仿真结果可能每拍都是亚稳态。建议设置时钟周期为20ns并让第一个下降沿出现在10ns处确保复位释放和时钟上升沿有足够距离。跑完之后打开Simulation Report检查pc信号是否按预期递增、reg_write_data在下一条指令上升沿之前是否稳定。4.2 时序报告里看哪些行编译后生成的project4.sta.rpt文件里重点看TimeQuest报告的Setup和Hold部分。单周期CPU的主频瓶颈通常在“寄存器堆读 - ALU计算 - 数据存储器访问 - 写回寄存器堆”这条链路上。时序报告里出现红色负slack时优先检查是否有组合逻辑绕了远路比如把ALU的输出又经过一个算术移位才送寄存器堆。可以在.qsf中用set_false_path声明跨时钟域的异步复位但不能随便约束关键路径。路径类型检查项常见超标原因Input Setup外部输入到寄存器引脚到寄存器路径过长Register-to-Register寄存器堆到ALU到存储器ALU级联过多Output Setup寄存器到外部引脚驱动LED或IO的扇出过大单周期CPU的时钟频率上限由关键路径决定而不是由指令条数决定。当slack为负时把.qsf里的FMAX约束调低一个档位再重新跑综合看报告是否收敛。收敛不了的场合要考虑分成支持流水线的多个周期但那是把单周期改成多周期后的另一个课题。4.3 Quartus II调用ModelSim失败的处置搜索热词里出现的“quartus ii cannot launch modelsim”是常见现场。现象是Simulator Tool选择ModelSim-Altera后报错找不到动态库或无法定位modelsim.exe。原因通常是安装顺序或环境变量不对。处理步骤是检查Quartus II Tools - Options - EDA Tool Options里的ModelSim路径是否指向实际安装目录确认用户环境变量PATH里存在$QUARTUS_ROOTDIR\eda\msim\win32aloem如果是64位系统还要保证Quartus和ModelSim版本位数一致混用32/64位会导致启动后马上闪退。# 命令行检查modelsim是否可被quartus找到Windows环境变量 set | findstr QUARTUS set | findstr MODELSIMMODELSIM环境变量指向modelsim.ini文件里面配置了各厂商库的映射关系。启动失败时常见的是modelsim.ini里的Altera库路径写错手工改一下库名和真实路径的映射即可。比较省事的方式是复用Quartus安装目录下自带的modelsim.ini模板而不是自己从零配置。这类问题几乎都是环境问题与设计本身无关排查时优先还原默认路径。5. 单周期CPU的进阶玩法从Lab作业到可综合的SoC雏形5.1 用阻塞赋值与非阻塞赋值重新审视寄存器堆单周期CPU里寄存器堆的写法直接决定综合出的电路行为。写寄存器堆时CLK上升沿判断RegWrite必须用非阻塞赋值always (posedge clk or posedge rst) begin if (rst) regfile[0] 32h0; else if (RegWrite waddr ! 5b0) regfile[waddr] wdata; end参数说明waddr为写寄存器地址wdata为写回数据RegWrite为使能信号寄存器0写操作被排除是因为MIPS约定0号寄存器恒为0。很多Lab代码在这里用阻塞赋值在仿真时可能看不出差别综合后的行为却可能因生成锁存器而异常。加上waddr ! 5b0这个条件是工程中防止0号寄存器被改的一层保险虽然综合器大概率会优化掉但语义更严谨。5.2 加一个中断向量表与MIF热更新单周期CPU做完基本指令集后可以往SoC雏形方向拓展一步在MIF里预留中断向量段把地址0x00000004作为中断服务程序入口主程序从0x00000000开始。这样做的收益是逼自己把取指阶段的PC更新逻辑改成“异常时强制跳转”而不是单纯返回PC4。实现异常跳转只需增加一个控制信号IntReq在写回阶段产生一个EPC寄存器保存当前PC。编译器或汇编器配合链接脚本把.text段放在0x00000000.isr段放在0x00000004。MIF的热更新是另一个值得练手的技巧。Quartus的In-System Memory Content Editor允许在不重新编译工程的情况下改写BRAM内容这比每次改指令都重新综合一条龙快得多。方法是在.qsf文件里给ROM IP核添加set_parameter使能AnyROM的Runtime Modification然后通过JTAG连接FPGA在编辑器里直接改MIF对应的地址区域。CPU跑的指令序列可以现场修改这对调试分支跳转和函数调用非常有价值。断开调试器后FPGA重新上电BRAM会从最初的MIF文件重新加载。这意味着热更新只存在于当前运行时需在最终交付前把验证过的MIF固化回去。若想实现真正的在线程序更新需要在设计里加入一个串口或者SD卡引导模块在CPU复位阶段把新指令写入BRAM整体就从单周期CPU扩展成了具备自举能力的MCU雏形这也是从课程设计迈向工程项目的自然一步。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →