CPU流水线设计实战:Verilog实现插入气泡、重定向与多级嵌套中断
2026/9/9 0:44:32 网站建设 项目流程

简介:华中科技大学计算机组成原理课程设计项目,完整实现基于Verilog的CPU流水线,涵盖流水线分段、插入气泡、重定向及多级嵌套中断等核心机制,体现出对处理器数据通路与控制逻辑的深入理解,适合计算机专业学生、硬件设计初学者及课程设计参考。资源共345个文件,压缩包31.39MB,包含Verilog源码、约束文件、比特流、仿真脚本、工程报告与波形配置文件等,涵盖从设计输入、综合实现到仿真验证的完整流程,目录结构清晰便于按模块学习。已有1939人学习,包内含可执行的批处理脚本与已综合布局布线后的工程中间文件,便于快速复现实验环境。这套资料不仅给出完整的Verilog实现,还包含工程中间文件与详细的报告文档,有助于理解流水线数据冲突、气泡插入和中断恢复的具体设计方法,随包附带的中间文件便于对照寄存器传输级代码进行逐段调试,适合课程设计、毕业设计及硬件竞赛备战使用。 华中科技大学计算机组成原理课程设计,CPU流水线,Verilog源码,带插入气泡、重定向、多级嵌套中断功能——看到这套组合拳,基本就是把计算机组成原理里最难的几个考点全揉在一个工程里了。如果你正在做这个课设,或者准备嵌入式、FPGA、数字IC方向的笔试面试,这篇内容应该能帮你在动手之前先把整体思路捋清楚。我按自己当时做类似项目的顺序,从架构、冒险处理、中断设计到仿真调试,把关键环节和踩过的坑一起拆开讲。

1. 项目整体设计与核心需求拆解

1.1 课程设计到底在考什么

先看需求清单:流水、插入气泡、重定向、多级嵌套中断,这四件事不是孤立的,它们刚好对应了CPU设计里最经典的几类问题。流水线解决的是吞吐率问题;插入气泡和重定向解决的是数据冒险和控制冒险;多级嵌套中断解决的是处理器对异步事件的处理能力。把这四件事全部用Verilog跑通,意味着你不只是会写代码,而是真的把指令生命周期、时序冲突、状态保存恢复这套机制想明白了。

从题目提供的信息看,这应该是华科计算机学院或相关专业的课程设计任务,通常基于MIPS指令集或MIPS的子集(比如整数指令、乘除法、访存指令等)做一个五级流水的CPU核心,再用FPGA开发板或仿真工具验证。标题里“Verilog源码”说明交付物是RTL代码,大概率还要配套testbench、仿真波形和设计报告。

1.2 方案选型前后的考虑

我当时做类似项目时,最纠结的其实是两个选择题。第一是架构选型:用经典五级流水(取指IF、译码ID、执行EX、访存MEM、写回WB)还是四级流水。我建议直接用五级标准结构,理由很简单:资料多、思路成熟、后续扩展中断时更容易划分时序。第二是控制器实现方式:用微程序控制还是硬布线控制。课设场景下硬布线是绝对的主流,因为代码直观、仿真容易排查,而微程序控制器写起来绕,调试成本也高。

另外一个关键决策是指令集子集的取舍。不用一上来就把所有MIPS指令都实现,先把基础指令跑通:add/sub/and/or等ALU运算、lw/sw访存、beq/bne分支、j跳转、jr跳转寄存器,再加syscall或自定义中断指令用于触发异常,基本就覆盖了课程设计的所有考点。指令集太大反而会让冒险检测和中断现场保存的复杂度成倍上升。

提示:如果你的时间只有两周,千万不要追求全指令集。先把无冲突路径跑通,再逐步加入冒险处理和中断,每一步都保留一个能通过的仿真版本,这样出问题时你能快速定位到是哪次改动引入的Bug。

2. 五级流水线架构与Verilog核心模块

2.1 五级流水线的数据通路设计

五级流水线的本质是“把一条指令的执行过程切成五段,每段用寄存器隔开,让多条指令像流水线上不同工位一样同时处理”。Verilog实现时,这个“隔开”靠的就是流水线寄存器:IF/ID寄存器、ID/EX寄存器、EX/MEM寄存器、MEM/WB寄存器。

每一级流水线寄存器的位宽和内容要提前设计好。IF/ID寄存器存放取到的指令字、PC+4;ID/EX寄存器存放译码后的控制信号、寄存器堆读出的两个操作数和立即数扩展结果;EX/MEM存放ALU计算结果、访存地址、写入数据;MEM/WB存放访存读出的数据或ALU结果、目标寄存器地址。这些寄存器的位宽建议用parameter或宏定义统一管理,不要散写数字。

2.2 核心模块划分与接口设计

我推荐的模块划分方式如下,直接对应Verilog文件组织:

  • pc_module:程序计数器,支持正常递增、分支跳转、中断向量跳转三种更新模式
  • instruction_memory:指令存储器,用只读ROM实现,存放测试程序
  • register_file:寄存器堆,2读1写,支持写数据前递冲突处理
  • alu_module:算术逻辑单元,支持加减、位运算、比较、移位等
  • data_memory:数据存储器,支持字节/字读写,用于lw/sw指令
  • controller:主控制器,根据opcode输出所有控制信号(RegDst、ALUSrc、MemRead、MemWrite等)
  • hazard_unit:冒险检测单元,负责插入气泡
  • forwarding_unit:转发单元,负责数据重定向
  • interrupt_controller:中断控制器,负责多级嵌套中断的管理

模块间的连接要注意一个细节:数据通路是“五级串行”,但控制信号往往需要“跨越流水线寄存器传递”,比如MemToReg信号必须跟着指令一起从ID级走到MEM级再到WB级,这时要在每一级流水线寄存器的输出侧重新寄存控制信号。很多同学第一次写时只在ID级给了控制信号,结果到了WB级发现信号已经丢了,这种错误极难排查。

// 流水线寄存器示例:ID/EX段控制信号寄存 always @(posedge clk or posedge rst) begin if (rst) begin id_ex_memread <= 1'b0; id_ex_memwrite <= 1'b0; id_ex_regwrite <= 1'b0; // ... end else if (stall) begin // 气泡插入时控制信号清零,但数据保留与否视策略而定 id_ex_memread <= 1'b0; id_ex_memwrite <= 1'b0; id_ex_regwrite <= 1'b0; end else begin id_ex_memread <= memread_id; id_ex_memwrite <= memwrite_id; id_ex_regwrite <= regwrite_id; end end

2.3 数据存储器与指令存储器的设计要点

在FPGA上实现时,指令存储器和数据存储器要注意初始化方式。Verilog里可以用$readmemh或$readmemb读取文本文件,把测试程序的机器码装载到存储器中,这样可以避免把指令硬编码在寄存器里,测试起来也更灵活。指令存储器我建议用同步读,数据存储器用异步读或同步读都可以,但要统一,否则仿真时会出现读数据到写回阶段时序不匹配的问题。

我踩过的坑:一开始指令存储器写的异步读,数据存储器写的同步读,导致仿真波形里数据莫名其妙的“慢一拍”,排查了很久才发现是存储器端口类型不一致造成的。如果看到类似时序错位,优先检查所有存储器的读端口是同步还是异步。

3. 数据冒险处理:插入气泡和数据重定向

3.1 什么是数据冒险,为什么要“插气泡”

数据冒险的本质是,后面指令需要的数据,前面指令还没写回。比如:

add r1, r2, r3 sub r4, r1, r5

第二条指令在ID级读寄存器堆时,第一条指令的add还在EX级,r1的最终结果根本还没写回寄存器堆,后一条指令就直接读到了旧值。流水线里可以用两种办法解决:一是让硬件检测到这种冲突后,把后续指令卡住一拍,这就是插入气泡;二是把还在执行段里的结果通过旁路直接送给需要的指令,这就是数据重定向(forwarding)。

插入气泡在Verilog里的实现思路是:当hazard_unit检测到ID/EX段正在执行的指令要写某个寄存器,而IF/ID段里的下一条指令要读同一个寄存器,并且写入源是ALU结果时,就暂停当前取指和译码,向流水线中插入一条空指令(NOP),也就是把控制信号全部清零。

3.2 冒险检测单元的触发条件

冒险检测要比你想象的更细致,不能只看寄存器号是否相等。我总结触发气泡的条件有这几项,缺一不可:

  • ID/EX段的控制信号里RegWrite为1(当前指令会写寄存器)
  • ID/EX段的rd或rt等于IF/ID段的rs或rt(读写同一寄存器)
  • 数据确实来自ALU或访存阶段,而不是来自已经前递的路径
  • 如果涉及lw指令的load-use冲突,则必须无条件插入一个气泡,因为load数据要等MEM级才有,转发根本来不及
// load-use冒险检测:必须插入气泡 wire load_use_hazard = id_ex_memread && ((id_ex_rd == if_id_rs) || (id_ex_rd == if_id_rt));

这里有个重点:寄存器号为0(r0)的情况要单独排除,因为MIPS里r0恒为0,写r0不会有任何影响,读r0也不需要等待。代码里如果忘了排除,轻则多插几个气泡降低性能,重则死锁。

3.3 数据重定向(转发)的实现路径

数据重定向的价值在于,很多冲突根本不需要暂停流水线。当上一条指令的结果已经算出来(在EX/MEM段或MEM/WB段),但还没写回寄存器堆时,可以通过多路选择器直接把结果旁路到ALU的输入端。

Verilog里典型的实现是forwarding_unit输出两个选择信号:

  • forwardA:决定ALU第一个操作数的来源,是从寄存器堆的rs_out取,还是从EX/MEM的ALU结果取,还是从MEM/WB的访存结果取
  • forwardB:同理决定ALU第二个操作数的来源

选择逻辑要分优先级:EX/MEM段的前递优先级要高过MEM/WB段,因为更新的数据才是真正需要的。同时要检查EX/MEM段的RegWrite和寄存器号是否匹配,匹配才前递。

// forwarding单元核心逻辑(节选) always @(*) begin if (ex_mem_regwrite && (ex_mem_rd != 0) && (ex_mem_rd == id_ex_rs)) forwardA = 2'b10; // 从EX/MEM前递 else if (mem_wb_regwrite && (mem_wb_rd != 0) && (mem_wb_rd == id_ex_rs)) forwardA = 2'b01; // 从MEM/WB前递 else forwardA = 2'b00; // 无前递,用寄存器堆原值 end

3.4 气泡与转发的配合优先级

转发和气泡不是互斥方案,它们要组合使用。我当时实现时的优先级逻辑是:先判断是否load-use冲突,如果是,必须无条件暂停一拍,此时转发单元即使检测到数据可用也要等到下一个周期;如果不是load-use冲突,则优先考虑转发,转发解决不了的再插气泡。换句话说,load-use是“硬暂停”,普通冲突是“软转发”。这个组合方案在性能和实现难度之间取了一个最佳平衡点。

注意:插入气泡时,不仅要把IF/ID和控制信号清零,还要确保IF阶段的PC不更新,否则你“空出一拍”就变成了“取出一条新指令”,流水线会乱套。PC的stall使能要在hazard单元里一并控制。

4. 多级嵌套中断的设计与实现

4.1 中断响应的基本流程

多级嵌套中断是这套课设里最难的部分,很多人写到这里才开始头疼。先明确中断处理的一般步骤:处理器收到中断请求后,先判断优先级和中断使能状态;若允许响应,就暂停当前流水线,保存当前PC和关键状态;然后跳转到中断服务程序入口;执行完中断服务程序后,恢复现场,返回到被中断的指令继续执行。

在五级流水CPU里实现这个流程,最关键的是“打断流水线但不丢失指令状态”。你在中断到来的那个周期,流水线里可能同时存在多条不同阶段的指令,如果直接清空所有流水线寄存器,这几条指令就丢了。

4.2 多级嵌套的现场保存设计

多级嵌套意味着中断可以被更高优先级的中断再次打断。这种情况下不能只保存一份现场,必须设计成“栈式保存”结构。在Verilog里我用的是一个中断栈数组,深度设为4或8(具体看你实际需要支持几级嵌套),每一级保存PC、状态寄存器、通用寄存器的快照和其他必要上下文。

// 中断现场栈示例 reg [31:0] pc_stack [0:3]; reg [31:0] status_stack [0:3]; reg [2:0] stack_pointer; // 当前栈指针,指向栈顶

响应中断时,stack_pointer加1,把当前PC压栈;返回时,把栈顶PC弹回pc_module,stack_pointer减1。如果嵌套深度不够,高级别中断再来时就会覆盖栈顶,这是设计里需要特别防御的——要么增加栈深度,要么在高优先级中断响应时屏蔽同级和低级中断。

4.3 中断控制器的仲裁逻辑

中断优先级仲裁可以参考中断请求信号的设计方式。我用了3个外部中断源,分别为INT_A(最高)、INT_B、INT_C(最低),每个中断源都有独立的使能位和屏蔽位。仲裁逻辑在clk的上升沿采样所有请求信号,若多个同时拉高,取优先级最高者响应。同时,正在处理低优先级中断时,如果来了更高优先级请求,当前低优先级中断被打断,进入高优先级处理流程。

这里有个很细节的坑:中断请求信号是异步的,直接用组合逻辑采样容易产生亚稳态。正规做法是先将每个中断请求用两级触发器做同步处理,再进行仲裁。虽然课设仿真环境里很难复现亚稳态,但代码风格上还是建议加上同步器,这也是面试时经常被问到的点。

4.4 与流水线结合的时序难点

中断打断流水线时,要区分“在流水线边界打断”还是“任意位置打断”。最稳妥的做法是:检测到需要响应中断时,在当前周期先暂停取指(PC不更新),让流水线中已有的指令继续往前流动,直到流水线空转或只剩无副作用的指令,再开始执行中断服务程序。这种设计叫“精确中断”,可以保证中断返回后,被中断指令之前的指令都完整执行了,之后的指令都没执行。

为什么要这么设计?因为如果一条访存指令正在MEM级执行,你强行把它截断,数据存储器里可能已经写入了一部分数据,但这条指令又没有真正完成,现场和内存状态就彻底对不上了。精确中断在实现上要增加一个“流水线完成检测”的逻辑,即判断IF/ID到MEM/WB各段是否还有有效指令,全部为空才跳转。

5. 仿真验证与常见问题排查

5.1 测试程序的构造思路

功能测试要分三阶段来写testbench。第一阶段是无冲突指令序列,比如连续执行add、sub、and等,验证基础数据通路;第二阶段是精心构造的冒险序列,比如紧跟一个add后马上用r1做运算的代码,验证load-use气泡是否插入正确、数据前递是否有效;第三阶段是中断测试,在指令流中间随机插入中断触发信号,并在中断服务程序里又触发更高优先级中断,验证多级嵌套是否正常工作。

测试程序用汇编写好,再手动或借助工具转成机器码,放入指令存储器。如果不想手动编码,可以用一个简单的Python脚本把汇编指令转成二进制机器码,然后生成.coe或.mem文件给仿真工具加载。

5.2 常见Bug:气泡错位、转发竞争、现场丢失

气泡错位的典型现象是:波形上看起来确实插入了NOP,但控制信号的时序整整慢了一拍。原因是stall信号发出后,IF/ID寄存器和ID/EX寄存器的“清零”时机没有对齐。正确的做法是,stall信号在ID段生效时,ID/EX寄存器在下一拍输出气泡信号,IF/ID寄存器同时保持原值不变。

转发竞争的典型现象是:同一周期里,EX/MEM和MEM/WB两个阶段都检测到要写同一个目标寄存器,结果数据跳来跳去不稳定。解决办法是加优先级判断,确保EX/MEM段的结果优先。

现场丢失的典型现象是:高级别中断返回后,低级别中断的现场找不到了。排查时先确认中断栈深度是否设得足够,再确认返回时stack_pointer是否被正确恢复。另外,中断返回指令本身不能被中断,否则返回到一半又被打断,现场栈就全乱了。实现上可以在中断返回指令执行期间暂时屏蔽所有更低优先级中断。

5.3 用ModelSim调试的有效技巧

关于仿真工具,ModelSim或QuestaSim都可以,Vivado/Xilinx的仿真器也行。我建议坚持看波形而不是只看打印信息,因为流水线的时序错位用文本很难看出来,用波形一眼就能找到是哪一级出了问题。

调试时我会把每一级的有效指令、目标寄存器、写使能信号全部导出到波形窗口,并把PC值也一起拉出来。如果发现某个周期内两条指令的PC没有按顺序递增,那说明气泡插入时机有问题;如果某条指令的值来源与预期前递路径不一致,那说明forwarding单元冲突判断写错了。

技巧:在testbench里加一个“伪完成信号”很有用。写一个断言模块,专门检查流水线提交阶段的指令是否满足预期结果。一旦断言失败就stop仿真,立刻定位错误周期。这样比盯着波形一条一条找高效得多。

6. 从课设到实际工程的几点体会

这套CPU流水线设计做完之后,不只是提交一个课设那么简单。它最关键的价值在于:你亲手把“指令到底如何在CPU里流动”这件事从抽象变成了可观察、可控、可调试的代码。以前在课本上看到的冒险、转发、中断上下文切换,在写Verilog调试波形的过程中会变得特别具体。

我个人的体会是,最重要的不是把功能跑通就结束,而是要把每一处设计取舍的原因记下来。比如为什么load-use要插气泡而普通ALU冲突可以转发,为什么中断现场要保存到栈式结构而不是一个固定寄存器,为什么中断返回指令需要额外的保护逻辑。这些就是你在面试或后续研究里能说出来的深度。

如果你做完基础功能还有精力,可以试着扩展一些内容:添加乘法除法指令并处理其多周期特性、加入分支预测器、将访存改为字节使能、甚至把中断控制器改成支持定时器中断的版本。这些扩展虽然在课程设计阶段不是硬性要求,但在后续的项目或IC相关岗位面试中是很大的加分项。

最后建议写文档时整理三样东西:整体架构图、冒险检测的完整状态表、中断处理的时序图。这些素材在你未来做更复杂的SoC设计、RISC-V处理器、或者需要向别人解释你的处理器设计时,都是最直接的参考材料。

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

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

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

立即咨询