
简介开放式实验CPU设计程序包围绕“CPU设计”实验主题面向计算机体系结构课程的学生或希望深入理解处理器原理的开发者提供从指令集架构到微架构实现的完整实践案例。资源以CPU调试实验6.13为核心涵盖控制单元、ALU、寄存器堆、内存接口等模块并涉及数据通路、流水线、RISC/CISC对比及Verilog/VHDL调试技术等关键知识点。整个压缩包共94个文件以VHDL硬件描述源码、Quartus工程与配置文件、编译报告和仿真测试文件为主另有少量文本说明整体仅903KB便于快速下载和学习。已有272人学习浏览适合作为课程设计、自主实验或备赛参考。压缩包内包含测试程序、被调试CPU各单元源码以及完整编译与调试记录能够帮助读者在动手过程中掌握仿真工具使用、断点设置、单步执行等排错手段从而真正打通CPU设计从理论到实践的闭环。开放式实验CPU设计从指令集选择到上板验证的全过程复盘每年做计算机组成原理实验的时候总有一批人被“开放式实验CPU设计”这个标题卡住。说它难吧其实要求的指令就那么几十条说它简单吧我见过不少同学在仿真阶段跑得挺好一到上板就“黑屏”不知道从哪查起。这个实验真正的难点不在于“写代码”而在于“做决策”——指令集选什么、单周期还是流水线、控制信号怎么定、验证到什么程度算合格每个岔路口都没有标准答案开放式无非是把这些选择权全部交给你。我这次完整走了一遍从零开始的CPU设计流程选型RISC-V工具用Vivado最终在FPGA开发板上把CPU跑了起来。这篇文章不打算重复教材里那种自上而下的理论推导而是按我实际操作时的思考顺序把每个关键节点的决策逻辑、实现细节和踩坑记录写清楚。位宽多少、控制信号怎么编码、testbench怎么组织、上板后怎么看结果这些东西教材里讲得都偏理想化真正动手时你会发现细节远比概念多。不管你的课程实验是要求MIPS还是RISC-V是单周期还是流水线是仿真验收还是上板验收这篇复盘都可以作为一个完整的参考框架。我会把自己当时做的选择、为什么这么选、选了之后遇到什么问题一一讲明白你完全可以根据自己的实验要求替换指令集或微架构但整体的推进路线是通用的。1. 动手之前先想清楚开放式实验的“开放”到底是什么很多同学拿到这个题目后第一反应是打开搜索引擎找一份现成代码。这恰恰是开放式实验最大的陷阱——开放意味着没有标准答案但也意味着你没有模板可抄。与其急着写代码不如先花半天把下面这几个问题想清楚。1.1 开放的四个维度从哪里选、选什么、怎么选开放式实验一般在四个维度上给你选择权指令集体系结构是用教学经典的MIPS32还是当前工业界主流、正在进入教材的RISC-V RV32I甚至自己定义一套极简教学指令集有些实验确实允许。微架构实现单周期、多周期还是五级流水线。同样的指令集三种实现方式的工作量差异很大性能也有明显差别。开发工具链与验证手段Verilog还是VHDLVivado还是Quartus仿真验证还是上板验证还是两者都要。验收深度有的实验只要功能正确有的要求报告指令格式分析和时序图说明有的还会要求扩展中断、异常或I/O外设。这四个维度组合起来可能的方案数量非常多。我当时给自己定的原则是在不影响毕业要求的范围内选择学习价值最高、但风险可控的组合。RISC-V发展很快很多学校近两年也把实验从MIPS迁移到RISC-V加上网上公开的参考资源越来越多所以指令集我选了RV32I——它是RISC-V的基础整数指令集指令数量不到40条非常适合教学工具链GCC、模拟器都有现成的出了问题还能借助SPIKE、Verilator等工具做对照调试。微架构方面考虑到时间周期和实验要求我选的是单周期实现。单周期的好处是控制逻辑直观、每个模块的职责清晰、调试难度低对于第一次完整设计CPU的人来说能先把“指令到底是怎么走通数据通路的”这件事彻底搞明白。流水线可以在单周期基础上慢慢加一步到位反而容易在冒险处理上卡死。1.2 一句话说清CPU设计的本质工作CPU设计的核心其实特别朴素每个时钟周期取出一个指令通过译码产生控制信号指挥数据通路完成一次确定的操作然后更新状态继续取下一条指令。RISC-V之类精简指令集之所以教学友好是因为它的指令编码极其规整比如32位指令中操作码字段固定占低7位译码逻辑可以用简单的case语句实现。而单周期CPU本质是一条固定的物理路径每个模块之间通过总线连接控制单元根据当前指令把这条路径上的“开关”拨到正确的位置。所谓“设计CPU”就是定义这条路径上每一个模块的接口以及拨动开关的控制信号逻辑表。这个理解方式帮我少走了很多弯路。后面所有模块的划分都围绕它展开每个信号都能归类于“数据”或“控制”连接关系自然就清楚了。2. 指令集选型为什么我选了RV32I而不是MIPS或自造指令2.1 三种指令集的现实对比对于课程实验选择指令集本质是在教学难度、资料丰富度和验收风险之间做平衡。我把三类常见选择做了个对比如下选项指令数量工具链成熟度学习资料量课程兼容性自造极简指令集8-16条低需自写汇编器甚至机器码转换少低验收时无法借助通用工具MIPS32子集30条左右高GCC支持、SPIM模拟器很多高经典教材大量篇幅以MIPS为例RISC-V RV32I37条高GCC、SPIKE、Verilator都有现成支持近两年快速增长越来越高多所高校已切换如果只看眼前MIPS反而最稳妥毕竟教材和网络资料数量最多。但RISC-V的优势在于它的指令编码是模块化设计的比例比较均衡对理解现代处理器设计更贴近而且但凡你后续工作、继续读研接触的题目很少有围绕MIPS展开的。MIPS更适合“照教材抄作业”RISC-V更适合“自己综合理解”。2.2 RV32I指令的划分逻辑RV32I共37条基础整数指令按功能可以分成五组这个分组直接对应后续控制单元的设计R型运算指令ADD、SUB、AND、OR、XOR、SLL、SRL、SRA、SLT、SLTU双源寄存器加一个目的寄存器功能由funct3和funct7共同决定。I型运算指令ADDI、ANDI、ORI、XORI、SLLI、SRLI、SRAI、SLTI、SLTIU寄存器加立即数除了移位指令funct3字段选定运算类型。Load/Store指令LB、LH、LW、LBU、LHU、SB、SH、SW访存指令执行阶段ALU计算有效地址再按字节/半字/字访问数据存储器。分支指令BEQ、BNE、BLT、BGE、BLTU、BGEU比较两个寄存器决定是否跳转。比较逻辑可以完全复用ALU的减法电路。跳转与链接指令JAL、JALR、AUIPC、LUI这类指令承担地址计算和高位立即数加载处理时要和普通运算指令分开因为它们的写回来源不是ALU而是PC链路。每组指令的译码逻辑都有共性比如R型和I型运算指令共享ALU的计算通道Load/Store共享地址计算结构。设计控制真值表时按组归纳能大幅降低出错概率。2.3 实验裁剪建议不是指令越多越好实验指导书如果没有硬性指令清单我建议你给自己画一条“最小可用曲线”先把ADD、SUB、ADDI、LW、SW、BEQ、BNE、JAL、LUI这9条指令完整贯通让一条最简单的程序跑起来在核心里增加AND、OR、XOR、SLT、SLL、SRL、JALR等常用指令用于支持稍微复杂一点的验证程序课外加分再考虑AUIPC、分支族的完整实现、结合移位器走通整数除法/开方。这个顺序能保证无论时间多紧张你都有一个可以在仿真里跑通的“活CPU”。千万不要一上来就想把37条指令全部覆盖控制信号组合会瞬间爆炸排错时很容易失去方向感。我当时是先用9条核心指令打通了数据通路确认仿真波形正确后再逐步补齐其余指令每条新指令从添加到验证通过控制在半天内。3. 数据通路的搭建顺序从PC到写回的六段旅程代码组织结构因人而异但无论你采用单文件还是多模块文件数据通路的几个基本部件都是绕不开的。下面按数据实际流经的顺序拆分这样写代码时也方便按图索骥。3.1 取指骨架PC怎么计算指令存储器怎么读程序计数器PC是整个CPU的“心脏起搏器”。单周期设计中PC在每个时钟上升沿更新一次下一周期PC的来源取决于当前指令是否分支、是否跳转这一点我建议统一用一个两输入的多路选择器实现always (posedge clk) begin if (rst) pc 32h0000_0000; else pc pc_next; end assign pc_next branch_taken ? pc_branch // 分支或跳转目标 : pc_plus_4; // 顺序执行指令存储器在仿真阶段用Verilog数组初始化即可常见做法是从hex文件加载机器码这样验证程序不用写在RTL代码里reg [31:0] instr_mem [0:1023]; initial $readmemh(program.hex, instr_mem); assign instruction instr_mem[pc[12:2]];要注意的是PC索引方式RISC-V指令按4字节对齐PC的低2位恒为0所以索引数组时一般用PC的位段也就是高30位对应的地址序号别直接拿32位PC当索引否则仿真时会出现数组越界的高阻态X。3.2 译码与控制一张真值表胜过十行case语句寄存器堆是译码阶段的核心部件RV32I定义32个32位寄存器其中x0被硬件固定为0。寄存器堆接口有三组地址和两组输出端口分别是两个源寄存器读端口和一个目的寄存器写端口写操作发生在时钟上升沿读操作是组合逻辑这个时序在仿真里只要别写反成同步读都不会出大问题。always (posedge clk) begin if (reg_write rd ! 5b0) regfile[rd] wd; end assign rs1_val (rs1 5b0) ? 32h0 : regfile[rs1]; assign rs2_val (rs2 5b0) ? 32h0 : regfile[rs2];控制单元的输入是instruction的opcode、funct3、funct7输出是一组控制信号寄存器写使能、内存写使能、ALUSrc决定ALU第二操作数来自寄存器还是立即数、ALUOp、MemtoReg决定写回数据来自ALU还是内存、Branch、Jump、MemRead等。单周期CPU的成败几乎就看这批控制信号的编码是否正确。我当时画了一张真值表——行是指令名字列是每个控制信号每个格子填0、1或dc填完之后再写Verilog里的case语句排错效率提升非常明显。3.3 执行与访存ALU和内存访问共享一套数据通路ALU的运算类型由控制单元的ALUOp、funct3和funct7共同决定。RV32I中R型指令的funct7位段大都有“0”和“32”两种取值要判断是否需要把它纳入译码条件。简单起见可以先把运算类型定义为信号再在ALU里用case匹配always (*) begin case (alu_ctrl) 4b0000: alu_result src_a src_b; 4b0001: alu_result src_a - src_b; 4b0010: alu_result src_a src_b; 4b0011: alu_result src_a | src_b; 4b0100: alu_result src_a ^ src_b; 4b0101: alu_result src_a src_b[4:0]; 4b0110: alu_result src_a src_b[4:0]; 4b0111: alu_result $signed(src_a) src_b[4:0]; endcase end访存阶段的数据存储器跟指令存储器类似但要注意S型指令写入时是字节使能硬件上真正实现时还要处理非对齐访问。课程实验里可以定一个规则只支持地址按4字节对齐的访问遇到非对齐直接按错误处理。这样能大幅降低访存逻辑复杂度报告里说明即可不能算偷工减料现代CPU对非对齐访问的处理本来就是很复杂的硬件工程。3.4 立即数扩展RV32I最容易被忽略的编码细节我在这块吃过亏。RISC-V的立即数扩展不像MIPS那样按类别复制符号位那么直观它把立即数位段拆散分布在指令字的不同位置Load指令用I型立即数——inst[31:20]、分支指令用B型立即数——来自inst[31]、inst[7]、inst[30:25]、inst[11:8]、JAL用J型立即数——分散在inst[31]、inst[19:12]、inst[20]、inst[30:21]。每类的拼接顺序都不一样写之前最好先从RISC-V手册把位段图抄一遍按位段重新拼接别凭印象写。我当时用了一段专门函数处理还随手写了几个指令码做单元测试确保每类立即数在极端值下符号扩展正确。之所以专门强调这节是因为立即数错误在仿真里表现得很隐蔽可能只差在一个数据的前几位不对波形看起来完全正常但是写回寄存器后整个程序全错排查困难。所以一定要针对I/B/S/U/J每类立即数分别写断言验证。4. Vivado工程搭建与调试把“写到哪算哪”换成分步验证4.1 工程结构与模块划分Vivado新建RTL工程时我建议按模块文件方式组织比单文件更利于排查定位也符合工程开发的习惯。我的模块清单如下pc.v: 程序计数器instr_mem.v: 指令存储器regfile.v: 寄存器堆alu.v: ALU运算单元imm_gen.v: 立即数扩展模块control.v: 主控制单元alu_ctrl.v: ALU控制子单元data_mem.v: 数据存储器cpu_top.v: 顶层模块负责例化与连线顶层模块最重要所有模块的输入输出接口都要汇总到这里。写顶层时最容易出错的是信号名拼写不一致Vivado的elaboration检查不一定在所有情况下都能快速给出清晰提示有时候一个信号在子模块里叫reg_write在顶层写成regwrite系统会当成正则表达式的两个不同信号直接导致连接悬空。建议出仿真结果前先把“connection”相关的warning全部过一遍。4.2 testbench编写策略先造仿真指令再写波形检查testbench的核心不只是加时钟和复位更重要的是要有“观察点”。很多人写好testbench后只等着看波形变化发现波形乱了才开始改代码这是低效的。我建议在testbench里加一个自动比对逻辑每一周期把模拟器比如用SPIKE或用Python写的简易模拟器算出的期望值和CPU输出的寄存器值做对比不一致就打印错误信息仿真结束统计错误数。initial begin // 加载程序 // 时钟复位 repeat (50) begin (posedge clk); if (sim_regfile[reg_addr] ! expect_regfile[reg_addr]) $display(Error at cycle %0d: reg[%0d] %0h, expect %0h, cycle, reg_addr, sim_regfile[reg_addr], expect_regfile[reg_addr]); end $finish; end这里的期望值来自GCC编出来的RV32I汇编再用SPIKE或自己手写一个小解析器得到。如果嫌麻烦退而求其次的方式是用一组手工精心设计的汇编测试程序每条指令前面安排一条不该修改某寄存器值的“哨兵指令”执行完后检查哨兵是否被意外改动能捕捉相当一批写错写回地址的问题。4.3 常见仿真隐坑时钟、复位与X态传播仿真是个大坑聚集地我挑三个最常见的排错经验说说。第一个是复位信号必须同步释放或至少是在时钟沿前建立好如果复位信号在时钟边沿附近变化可能导致部分寄存器复位、部分没复位波形上会看到一堆X态整条链路全部变红。第二个是X态传播问题你的指令存储器初始值没写全或某个RAM单元没有正确初始化读出来的指令是X接着整个数据通路从取指开始全部变X。要判断问题根因直接看第一个周期的PC和instruction信号如果PC正常但instruction是X先查hex加载路径。第三个是无意的latch组合逻辑里的case没写default或者if没有else综合时Vivado会提示latch inferred仿真阶段这类问题往往不报错但上板时序就完全不对。5. 单一周期改流水线的岔路口值得做吗怎么做如果你的实验要求是流水线实现或者你想拿它冲刺高分那就要明白流水线改造不是简单地在模块上加寄存器这么简单。5.1 流水线改造的真实工作量单周期到五级流水线每个阶段之间需要加流水线寄存器IF/ID、ID/EX、EX/MEM、MEM/WB目的是把每个阶段的中间结果锁存到下一拍。改造本身的工作量其实还好真正的复杂度在冒险处理上。数据冒险需要前递forwarding加气泡stall控制冒险需要分支预测或者至少是简单预测。如果你直接写一个带前递和不带预测的流水线波形调试的时间可能数倍于单周期。5.2 实验语境下的合理范围如果只是课程实验一个比较合理的方案是在单周期基础上增加IF/ID和ID/EX两级流水线寄存器做一个“准两级流水线”只处理数据冒险中的RAW冒险对控制冒险采用flush掉错误分支指令的方式。这样你掌握了流水线寄存器的组织方法和冒险处理的基本思路却又不会陷入分支预测器的深水区。我在后来的扩展里就是这么干的代码量只增加了约20%但报告里讲清为何采用flush不采用预测后答辩老师普遍认可。5.3 哪个扩展方向性价比最高对于想在实验里出彩的同学我建议重点考虑三件事第一是增加中断或异常支持对外部中断请求做响应、保存返回地址到特定CSR这比加乘法指令更能体现系统级理解第二是增加总线接口让CPU通过简单的总线协议访问外部串口或GPIO上板能打印字符串或点亮数码管展示效果非常直观第三是增加Cache或者至少是指令/数据分离存储不过这个对课程实验来说偏重酌情考虑。我个人认为上板展示的优先级最高看得见摸得着评委交流起来也容易。6. 上板验证与实战排错仿真过了为什么板子是黑的6.1 仿真与上板的本质差异仿真环境是理想的时钟稳定、信号无偏差、无引脚约束时序要求。上板就完全不同了综合后要满足时序约束不满足约束就会出现亚稳态、毛刺甚至功能错误。最大的差异在三块真实时钟信号可能有抖动或相位问题内部信号延时导致控制信号不同时到达部分初始值在上电前不确定没有可靠的复位就不可能进入确定状态。6.2 我在上板时排过的五个实际问题问题一LED灯不亮整体无反应。排查下来是复位信号的引脚约束没有写在XDC文件里导致复位端口悬空上电状态不定。解法是把复位引脚绑定到板载按键或直接使用芯片的POR复位逻辑。问题二程序跑飞数码管乱跳。原因是时钟未经BUFG走全局时钟网络Vivado会报clock not routed as global clock表面上看综合能过但运行时延太大时序已崩。解法是在时钟原语或端口约束里让工具自动缓冲分配。问题三部分指令执行正确、部分错误集中在指令存储器初始化部分。因为hex文件路径在仿真时用相对路径上板后不可用Vivado综合时没有把RAM初始化数据打进去。解法是使用$readmemh的绝对路径或者直接在RTL里用parameter数组初始化。问题四分支跳转偶尔出错波形很难复现。单周期内有两个沿——PC更新沿和寄存器写沿如果分⽀判断逻辑在写沿之后才稳定下一拍PC就取错了。解法是看清时序PC更新的组合逻辑必须在PC时钟沿前稳定建立。问题五拨码开关拨动没反应实测是XDC文件里引脚约束没有配置内部上拉/下拉所以拨码悬空时读到的是高阻状态。解法是外加约束IO标准为LVCMOS33并启用内部上拉或下拉。6.3 一套好用的演示验证程序演示程序不需要太复杂但最好能直观展示CPU的通用计算能力。我当时的演示程序功能是读取拨码开关数值累加模7后送数码管显示同时每隔一定循环次数把累加值通过UART发送到PC串口终端。这个程序既包含读取输入、读写内存、数学运算、条件分支、循环控制也包含外围总线交互工作量不大验收时的感觉比单纯跑个流水灯强非常多。你如果时间紧一个流水灯程序也能证明CPU能执行指令、能写内存和输出接口但它不太能证明你的CPU是从取指到访存到写回的完整通路都对。至少留一个内存操作的演示项比如把输入采样到内存再读回来比较再输出这样才能覆盖访存路径。7. 交给下一个做CPU设计实验的人整个项目做完我最大的体验是CPU设计没有想象中那么高不可攀但每一个“看起来很简单”的模块都在测试你对整体数据通路的把握程度。仿真阶段多花一天写自动测试胜过上板后花三天乱试控制信号真值表画得越细后面排错就越轻松上板阶段老老实实检查XDC引脚约束能帮你省掉大半“黑屏”噩梦。如果你马上要开始做这个实验我给你的行动顺序是先拿9条核心指令把数据通路贯通并且仿真通过再补全指令集然后做上板最小演示最后再根据个人余力决定是否做流水线、中断或总线扩展。反过来如果想一步到位做复杂的流水线大概率会在冒险处理上卡到期末。开放式的题核心考的就是在限定时间内做出合理取舍这个能力比CPU本身的电路设计更值钱。本文还有配套的精品资源点击获取