五级流水CPU设计:数据冒险重定向与多级嵌套中断实现

发布时间:2026/10/30 5:23:25
五级流水CPU设计:数据冒险重定向与多级嵌套中断实现

简介:面向计算机组成原理课程设计与硬件验证学习者,这份华中科技大学课程设计资料围绕 Verilog CPU 流水线展开,覆盖取指、译码、执行、访存、写回等阶段,并实现插入气泡、数据重定向与多级嵌套中断等关键机制,适合用来理解流水线冲突处理、中断响应流程以及从仿真到上板的完整链路。压缩包共 345 个文件,约 31.39MB,包含 25 个 v 源码、166 个 xml 工程与约束配置、30 个 rpt 综合实现报告,以及 dcp、bit、tcl、jou、log 等 Vivado 工程产物,可对照源码、报告和脚本复现设计流程。已有 1949 人学习下载,读者可从中获得可运行的流水线 CPU 源码、测试与上板验证素材、综合实现结果及排错参考,便于按模块拆解学习并用于同类课程设计。

1. 需求先摆清楚:一条 load 指令后紧跟 ADD,流水线到底该停几个周期

课设任务书上写着四件事:五级流水、插入气泡、数据重定向、多级嵌套中断。真正动笔写代码时才发现,这四件事并不是四个独立的模块——load-use 冒险要停一拍,气泡插进 IF/ID 的同时 PC 要不要冻结、分支在 ID 段判定又撞上中断请求,这几个信号会在同一个时钟沿打架。我见过不少同学的 CPU 单跑指令流没问题,一开中断就飞,或者 SUB 紧跟在 LW 后面结果偶尔错。

这套方案就围绕这个场景展开:用 Verilog 搭一颗五级流水 CPU,把冒险处理(气泡 + 重定向)和可嵌套的多级中断做进同一套控制逻辑里,配套仿真用例和波形验收点。适合正在做计算机组成原理课程设计、需要交一份能跑通、能讲明白的 CPU 源码的同学,也适合想把流水线控制单元重写一遍的从业者参考结构。

2. 总体架构设计:五级流水数据通路与八个模块的职责划分

2.1 从单周期到五级流水:拆段依据与四组流水寄存器

单周期 CPU 的临界路径太长,取指、译码、执行、访存、写回串成一条组合逻辑链,时钟周期被最慢的那条路径(通常是 ALU + 数据存储器)锁死。拆成五级之后,每段只做一件事,时钟周期由最长的一段决定,理想情况下吞吐量提升接近五倍。代价是引入流水寄存器,以及随之而来的一切冒险问题。

拆段的粒度按功能而非按延迟来切,是课程设计里比较稳妥的做法:IF 段只管把 PC 送进指令存储器并锁存指令;ID 段译码、读通用寄存器、生成本条指令的全部控制信号;EX 段走 ALU 和分支判定;MEM 段访问数据存储器;WB 段把结果写回寄存器堆。

关键决策是控制信号的传递方式。有两种写法:一种在 EX 段重新译码,另一种在 ID 段一次性生成全部控制信号,让它们跟着流水寄存器往下走。我一般选后者,因为控制信号一旦随指令流动,后面每段只看自己那组信号,逻辑清晰,中断时冲刷流水也不会出现信号与指令错位。四组流水寄存器分别是if_id_reg、id_ex_reg、ex_mem_reg、mem_wb_reg,每组内部既有数据通路(指令、操作数、ALU 结果),也有控制通路(RegWrite、MemRead、MemWrite、MemToReg、ALUOp 等)。

提示:流水寄存器里的控制信号一定要跟着数据一起被冲刷。只要漏掉一个控制位,就会出现“指令被清掉了,但写使能还在”的诡异现象,寄存器堆被莫名其妙地改写。

2.2 模块划分与顶层例化关系

整个 CPU 拆成八个模块,职责边界划清楚之后,调试可以一个模块一个模块地打波形。

模块职责关键输入输出
pc_reg程序计数器,支持正常 +4、分支跳转、中断跳转、冻结pc_next、pc_stall
imem指令存储器,256×32 位,组合读addr→instr
regfile32 个 32 位通用寄存器,两个读口一个写口rs/rt/rd、we
id_ctrl主控单元,按 opcode/funct 产生全部控制信号instr→ 控制总线
alu算术逻辑单元,支持 8 种运算a、b、alu_op→y
dmem数据存储器,256×32 位addr、we
hazard_unit冒险检测,产出冻结与冲刷信号、重定向前递选择见第 3 章
intc中断控制器,优先级仲裁、嵌套栈管理见第 4 章

顶层只做例化和连线,不放任何逻辑判断。这条规矩看起来是形式主义,实际调试时价值极大:当你在波形里看到 PC 冻结了但 IF/ID 没清空,可以立刻定位到是hazard_unit的输出接错了,而不是在几千行的顶层里翻。

// 顶层:只做例化与连线,不写判断逻辑 cpu_top u_cpu ( .clk (clk), .rst_n (rst_n), .irq (irq), // 4 路外部中断请求 .pc (pc_dbg), // 引到顶层便于波形观察 .instr (instr_dbg), .alu_result (alu_dbg) ); // 流水寄存器的更新统一由一个 always 块驱动,避免多驱动 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin if_id_reg <= 64'd0; id_ex_reg <= 128'd0; ex_mem_reg <= 128'd0; mem_wb_reg <= 128'd0; end else begin // stall 时保持,flush 时清空,否则正常推进 if_id_reg <= if_id_flush ? 64'd0 : (if_id_stall ? if_id_reg : if_id_next); id_ex_reg <= id_ex_flush ? 128'd0 : (id_ex_stall ? id_ex_reg : id_ex_next); ex_mem_reg <= ex_mem_flush ? 128'd0 : ex_mem_next; mem_wb_reg <= mem_wb_next; end end

这段代码里stall和flush是两组语义完全不同的信号,混淆它们是新手最常犯的错。stall表示“保持当前值不变”,用于 load-use 冒险时冻结 PC 和 IF/ID;flush表示“清成 NOP”,用于分支跳转和中断响应时把取错路的指令冲掉。同一个流水寄存器可能同时需要这两种行为,优先级必须先 flush 后 stall——因为跳转发生时,冲掉错误指令比保持更重要。

2.3 关键参数与控制信号定义

参数定死之后,后面所有模块位宽对齐,不会出现“EX 段 32 位、MEM 段 31 位”的低级错误。

参数取值说明
DATA_WIDTH32数据与地址位宽
REG_NUM32通用寄存器个数,5 位地址
IMEM_DEPTH256指令存储器字数
DMEM_DEPTH256数据存储器字数
IRQ_NUM4中断源数量
NEST_DEPTH3中断嵌套最大深度

控制信号一共 9 位,在 ID 段一次性生成:RegDst(写回目标选择 rd/rt)、ALUSrc(第二操作数来自寄存器还是立即数)、MemToReg(写回数据来自 ALU 还是存储器)、RegWrite、MemRead、MemWrite、Branch、Jump、ALUOp[2:0]。这 9 位被打包进id_ex_reg、ex_mem_reg、mem_wb_reg一路下传,任何一段需要使用时直接从本段寄存器取,不再回头查指令。

3. 流水线冒险处理:插入气泡与重定向的判定逻辑

3.1 三类冒险的判定条件与处理策略选择

结构冒险在这套设计里被哈佛结构规避掉了:指令存储器和数据存储器物理分离,IF 段取指和 MEM 段访存不会争同一个端口的读写口。如果强行用统一存储器,就得在 MEM 段插入气泡让路,性能损失明显,课设里没有必要给自己增加难度。

数据冒险是重头戏。RAW(写后读)分两种情况:一种是运算结果在 EX 段末尾已经算出来,下一条指令在 EX 段开头就要用,这种靠重定向就能解决,不用停;另一种是 load 指令,数据要到 MEM 段结束才从存储器里出来,下一条指令在 EX 段开头无论如何拿不到,必须停一拍。这就是 load-use 冒险,也是“插入气泡”这个功能存在的唯一理由。

控制冒险来自分支和跳转。这套设计把分支判定提前到 ID 段:ID 段有译码后的寄存器值和符号扩展后的立即数,用一个小加法器算目标地址,比在 EX 段判定少损失一个周期。代价是 ID 段多一个比较器和加法器,面积上完全可接受。分支成立时冲刷 IF/ID,只损失一条已经取进来的指令。

三类冒险的判定条件汇总如下,这是写hazard_unit之前必须先想清楚的表:

冒险类型判定条件处理动作
EX/MEM 转发ex_mem_reg_write && ex_mem_rd != 0 && ex_mem_rd == id_ex_rsforward_a = 2'b10
MEM/WB 转发mem_wb_reg_write && mem_wb_rd != 0 && mem_wb_rd == id_ex_rsforward_a = 2'b01
load-useid_ex_mem_read && (id_ex_rt == if_id_rs || id_ex_rt == if_id_rt)冻结 PC、清空 ID/EX
分支冲刷ID 段branch_taken || jump清空 IF/ID

注意rd != 0这个条件。寄存器 0 是硬连线到 0 的,任何指令写$0都被丢弃,但如果你不判断,转发逻辑会把$0的“写入值”转给后面的指令,结果读到的是垃圾。这个坑我在第一次写转发单元时就踩过,仿真跑出来ADD $t0, $zero, $zero之后所有结果都变成 X。

3.2 重定向单元的优先级编码

转发本质上是一个三选一的多路选择器:正常读寄存器堆、从 EX/MEM 段转发、从 MEM/WB 段转发。优先级必须 EX/MEM 高于 MEM/WB,因为 EX/MEM 是更新的结果。如果两条指令连着写同一个寄存器,MEM/WB 里的是旧值,转发它会拿到过期数据。

// 重定向前递单元:EX/MEM 优先级高于 MEM/WB module forwarding_unit ( input [4:0] id_ex_rs, id_ex_rt, // EX 段当前指令的源寄存器 input [4:0] ex_mem_rd, mem_wb_rd, // 前两级的目的寄存器 input ex_mem_reg_write, // EX/MEM 段是否要写寄存器 input mem_wb_reg_write, // MEM/WB 段是否要写寄存器 output reg [1:0] forward_a, forward_b ); always @(*) begin // A 口:优先 EX/MEM,其次 MEM/WB,否则取寄存器堆 if (ex_mem_reg_write && (ex_mem_rd != 5'd0) && (ex_mem_rd == id_ex_rs)) forward_a = 2'b10; else if (mem_wb_reg_write && (mem_wb_rd != 5'd0) && (mem_wb_rd == id_ex_rs)) forward_a = 2'b01; else forward_a = 2'b00; // B 口逻辑相同,比较对象换成 id_ex_rt if (ex_mem_reg_write && (ex_mem_rd != 5'd0) && (ex_mem_rd == id_ex_rt)) forward_b = 2'b10; else if (mem_wb_reg_write && (mem_wb_rd != 5'd0) && (mem_wb_rd == id_ex_rt)) forward_b = 2'b01; else forward_b = 2'b00; end endmodule

forward_a的编码约定:2'b00取寄存器堆输出,2'b01取mem_wb_reg里的写回值,2'b10取ex_mem_reg里的 ALU 结果。这三个值送到 EX 段 ALU 输入端的两个多路选择器上。注意2'b01那条路径取的应该是mem_wb段的写回数据(已经过了 MemToReg 选择),而不是 ALU 结果——如果 load 指令后面紧跟一条用该数据的指令,MEM/WB 里装的是存储器读出的值,取 ALU 结果就错了。

这段逻辑还有一个容易忽略的细节:id_ex_rt在lw指令里是目的寄存器,但在sw指令里是源寄存器(要存的数据)。转发判断必须知道当前指令的 rt 到底是源还是目的,否则sw会拿到错误的数据。通常的做法是在 ID 段产生一个id_ex_rt_is_src标志,跟着流水寄存器下传,判断 B 口转发时把它加上。

3.3 插入气泡:Hazard Unit 与 IF/ID 冲刷的联动

load-use 冒险的处理不是“停一个周期”这么简单,它要同时做三件事:冻结 PC(否则取下一条指令)、冻结 IF/ID(保持当前指令继续译码)、清空 ID/EX(把已经进到 EX 段的指令变成 NOP)。少做任何一件,结果都不对。

// 冒险检测:load-use 只停一拍 module hazard_unit ( input [4:0] if_id_rs, if_id_rt, // ID 段指令的源寄存器 input [4:0] id_ex_rt, // EX 段指令的目的寄存器 input id_ex_mem_read, // EX 段是 load 指令 input branch_taken, jump, // 分支/跳转成立 output reg pc_stall, if_id_stall, output reg id_ex_flush, if_id_flush ); wire load_use = id_ex_mem_read && ((id_ex_rt == if_id_rs) || (id_ex_rt == if_id_rt)); always @(*) begin // 默认:全部放开 pc_stall = 1'b0; if_id_stall = 1'b0; id_ex_flush = 1'b0; if_id_flush = 1'b0; if (load_use) begin pc_stall = 1'b1; // PC 保持,不取下一条 if_id_stall = 1'b1; // ID 段指令原地等待 id_ex_flush = 1'b1; // EX 段插入气泡(NOP) end else if (branch_taken || jump) begin if_id_flush = 1'b1; // 分支成立,冲掉错误路径指令 end end endmodule

这里if_id_flush和if_id_stall是互斥的,if/else if的写法保证了这一点。如果把两者写成并列的if,分支和 load-use 同时发生时会同时置位,而流水寄存器里 flush 的优先级高于 stall,结果就是本该冻结的指令被清掉了,程序直接跑飞。

pc_stall拉高的那一个周期,PC 不更新,IF/ID 保持,ID/EX 被清成 NOP。下一个周期load_use条件自然消失(因为 EX 段已经变成 NOP 了),流水线恢复推进。所以 load-use 永远只停一拍,不需要计数器。如果出现“停两拍”的现象,说明清空 ID/EX 的那一拍没生效,id_ex_mem_read还在,条件一直成立——这时候去波形里看id_ex_flush是不是真的拉高了一个周期。

3.4 手算验证:用一段六条指令的序列核对每拍结果

光看代码看不出对错,我习惯在写测试激励之前先手算一遍关键寄存器在每一拍的期望值,仿真出来逐拍对照。

测试序列:

LW $t0, 0($s0) # I1:load,结果在 MEM 段末尾可用 ADD $t1, $t0, $s1 # I2:依赖 I1,必须停一拍 SUB $t2, $t1, $s2 # I3:依赖 I2,靠 EX/MEM 转发 AND $t3, $t2, $s3 # I4:依赖 I3,靠 MEM/WB 转发 SW $t3, 4($s0) # I5:依赖 I4,rt 是源寄存器 BEQ $t1, $t2, L1 # I6:分支,ID 段判定

期望的流水行为:I1 在 EX 段时检测到与 I2 的 load-use 关系,停一拍;I2 的 EX 段拿到的是 MEM 段转发来的 load 数据;I3 依赖 I2 的 ALU 结果,走 EX/MEM 转发;I4 依赖 I3,走 MEM/WB 转发,这里正好检验优先级——如果转发单元把 EX/MEM 的优先级写反了,I4 会拿到 I2 的结果而不是 I3 的。

把这段序列对应的十六进制机器码烧进imem,跑 20 个周期,逐拍检查regfile里$t1、$t2、$t3的值。三个值全对,说明气泡和重定向的主路径是通的。再故意把转发优先级改错,看仿真结果是否如预期出错——这个反向验证能确认你的测试用例真的有区分度,而不是碰巧对了。

4. 多级嵌套中断:中断控制器与现场保护机制

4.1 中断源优先级与嵌套深度设计

四个中断源按固定优先级仲裁,编号 0 最高、3 最低。每个中断源有一位的屏蔽寄存器,由软件写irq_mask控制。嵌套深度定成 3 级,实现上用一个深度为 3 的硬件栈保存现场,不占用数据存储器——中断响应必须足够快,走存储器压栈会把响应延迟拉长到十几个周期。

中断源优先级典型用途是否可被嵌套
irq[0]最高定时器溢出不可被低优先级打断
irq[1]高外设就绪可被 irq[0] 嵌套
irq[2]中串口收发可被 irq[0..1] 嵌套
irq[3]低按键输入可被任意更高优先级嵌套

仲裁逻辑在intc模块里只做一件事:从当前未屏蔽且比当前运行优先级更高的请求中挑出编号最小的那个,产生irq_taken和irq_id。屏蔽分两层,一层是软件写的irq_mask,另一层是响应中断时自动置位的“当前优先级阈值”——只有优先级高于阈值的新请求才能打断当前中断,这是实现嵌套的关键。

4.2 中断响应流程:六步走完,一步都不能省

中断响应发生在一条指令“提交”的时刻,也就是 MEM 段结束、WB 段开始之前。选这个时间点是为了保证精确中断:中断前的所有指令全部执行完,中断后的指令一条都没执行,返回后从断点继续,语义干净。

响应流程按顺序是:

  1. 在 MEM 段检测irq_taken,若成立则冻结 PC 与 IF/ID;
  2. 把当前指令的 PC 值写入硬件栈顶,作为 EPC;
  3. 把当前优先级阈值压栈,同时把阈值更新为本次中断的优先级;
  4. 清空 ID/EX、EX/MEM 两级流水寄存器,冲掉未提交的指令;
  5. 把 PC 置为中断向量地址0x0000_0100 + irq_id * 4;
  6. 置位全局中断使能,允许更高优先级嵌套。

中断返回指令ERET走反向流程:从栈顶弹出 EPC 和旧阈值,恢复 PC 和优先级,同时清空流水线。这里有个顺序问题必须注意——EPC 的弹出和 PC 的更新必须在同一个周期完成,如果分两拍,中间那一拍会取到错误的指令。

注意:ERET本身也是一条要走完流水的指令。如果它在 MEM 段弹出 EPC 并更新 PC,那么它后面已经取进来的两条指令必须被冲掉,if_id_flush和id_ex_flush要同时置位。

4.3 中断控制器的 Verilog 实现

// 中断控制器:固定优先级仲裁 + 3 级硬件栈 module intc ( input clk, input rst_n, input [3:0] irq, // 外部中断请求,电平有效 input [3:0] irq_mask, // 软件屏蔽位,1 表示屏蔽 input irq_ack, // 来自 CPU:本拍响应中断 input eret, // 来自 CPU:中断返回 input [31:0] epc_in, // 来自 CPU:断点 PC output reg irq_taken, output reg [1:0] irq_id, output reg [31:0] epc_out, output reg [1:0] cur_prio, // 当前运行优先级阈值 output stack_overflow ); reg [31:0] epc_stack [0:2]; // 深度 3 的 EPC 栈 reg [1:0] prio_stack[0:2]; reg [1:0] sp; // 栈指针,0 表示空 // 仲裁:找出未屏蔽且优先级高于当前阈值的最小号请求 integer i; reg found; always @(*) begin irq_taken = 1'b0; irq_id = 2'd0; found = 1'b0; for (i = 3; i >= 0; i = i - 1) begin // 从低号往高号扫,后写覆盖前写,最终留下编号最小的 if (irq[i] && !irq_mask[i] && ({1'b0, i[1:0]} < cur_prio || cur_prio == 2'd3)) begin irq_taken = 1'b1; irq_id = i[1:0]; found = 1'b1; end end end // 栈操作:响应时压栈,ERET 时弹栈 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sp <= 2'd0; cur_prio <= 2'd3; // 复位后优先级最低,任何中断都能进 epc_out <= 32'd0; end else if (irq_taken && irq_ack && sp < 2'd3) begin epc_stack[sp] <= epc_in; // 保存断点 prio_stack[sp] <= cur_prio; // 保存旧阈值 cur_prio <= irq_id; // 抬高阈值,屏蔽同级与低级 sp <= sp + 2'd1; end else if (eret && sp > 2'd0) begin sp <= sp - 2'd1; epc_out <= epc_stack[sp - 2'd1]; // 弹出断点 cur_prio<= prio_stack[sp - 2'd1]; // 恢复旧阈值 end end assign stack_overflow = (sp == 2'd3) && irq_taken && irq_ack; endmodule

仲裁那段for循环的写法值得多说一句。从 3 扫到 0、用后写覆盖前写,最后留下的是编号最小(优先级最高)的那个,这是 Verilog 里实现固定优先级仲裁的常见技巧,综合出来是一串级联的与或逻辑。条件里的cur_prio == 2'd3是复位后的特例:初始阈值设成 3(最低),此时任何中断都该能进,而i < cur_prio在i == 3时不成立,所以单独放行。

epc_out只在eret那一拍有效,CPU 侧必须用组合逻辑直接把它接到 PC 的下一值选择器上,不能等到下一个时钟沿——中间隔一拍,PC 就已经跑到别处去了。stack_overflow用于指示嵌套深度超限,正常调试时用它触发一个断言,比事后翻波形高效得多。

4.4 中断与流水线的交互:为什么响应点必须放在 MEM 段

中断响应点的选择直接决定“精确中断”能不能成立。放在 IF 段,此时流水线里还有四条指令没执行完,中断返回后这四条要重新取,程序语义上没问题,但如果是存储器写指令,可能已经写了一半,无法回滚。放在 WB 段,所有指令都执行完了,但响应延迟变成四个周期,定时器中断的实时性会受影响。

折中方案是放在 MEM 段末尾:此时前面的指令都已提交(写寄存器、写存储器都已完成),后面的指令还在 EX/ID/IF 里没产生副作用,冲掉即可,不需要回滚任何状态。代价是响应延迟两个周期,对课设的定时器中断完全够用。

提示:中断响应时必须同时冻结 PC 和冲刷流水。只冲刷不冻结,PC 会在中断处理的第一个周期继续 +4 取指,把中断服务程序的第一条指令跳过去。

5. 避坑清单:五类能在答辩现场让 CPU 突然失效的问题

5.1 流水线仿真结果对不上的三类典型现象

现象一:ADD紧跟在LW后面,结果偶尔正确偶尔错误。原因通常是气泡插了,但id_ex_flush只清数据通路没清控制信号,或者清的控制信号里漏了MemToReg,导致下一拍写回的是 ALU 结果而不是存储器数据。解决方法很直接:把id_ex_reg里所有控制位列一张清单,逐位核对是否被 flush 覆盖,这类漏项用眼扫一遍就能发现,比看波形快。

现象二:转发开启后,连续三条写同一寄存器的指令结果全乱。原因是转发优先级写反,MEM/WB 反而压过了 EX/MEM。判定方法是看波形里forward_a在连续依赖时的取值,正确的应该是2'b10,如果出现2'b01,就是if / else if的顺序反了。还有一种可能是比较时用了id_ex_rs去比mem_wb_rd,却忘了mem_wb段的目的寄存器可能是rt(lw指令),需要先经过RegDst选择再比较。

现象三:分支指令后面的那条指令被执行了。这是延迟槽语义和冲刷逻辑冲突。如果你的设计不采用延迟槽,分支成立时必须把 IF/ID 清掉;如果采用了延迟槽,就不能清,要让紧跟的那条指令正常走完。两种设计都对,混着来就错。我一般选不采用延迟槽,逻辑简单,代价是损失一个周期,课设评分里不影响。

5.2 中断调试与综合上板的四类现象

现象四:开中断后第一次能返回,第二次返回地址就偏了。原因多半是 EPC 保存的是PC + 4而不是当前指令 PC。响应发生在 MEM 段,此时 PC 已经指向后面好几条指令了,如果直接拿pc信号当 EPC,返回到的是未来的地址。正确做法是在流水寄存器里一路把当前指令的 PC 带下来,MEM 段直接用ex_mem_pc压栈。

现象五:嵌套中断进来后,外层中断的现场丢失。说明只有一组 EPC 和阈值寄存器,没有栈。单层中断用一组寄存器够用,一旦嵌套,第二次响应会覆盖第一次的 EPC,返回时跳到内层断点,外层永远回不去。改成深度为 3 的硬件栈,栈指针跟着响应和返回增减,问题立刻消失。

现象六:仿真全对,综合后上板跑飞。常见原因是寄存器堆没有复位。仿真时寄存器初值是 X,你的测试激励恰好先写后读,看不出问题;上板后 X 态在真实电路里会传播,读到的值是随机的。解决方法是在regfile的复位分支里加一个循环清零,虽然综合工具会警告“复位扇出过大”,但课设规模下完全可接受。另一个原因是组合逻辑里的case没写default,综合出锁存器,时序分析通不过。

6. 验收与答辩:用三张表把方案讲成一条完整的线

验收的核心不是“我的 CPU 能跑”,而是“我能证明它在什么情况下一定对”。我一般准备三张表,答辩时按顺序讲,评委的追问基本都落在表里。

第一张是功能覆盖表,把任务书要求的四个点逐一对应到模块和测试用例:

功能点实现模块验证用例通过判据
五级流水cpu_top+ 四组流水寄存器20 条无相关指令连续执行20 拍后 20 条全部写回,IPC = 1
插入气泡hazard_unitLW+ADD相邻pc_stall高电平 1 拍,$t1值正确
数据重定向forwarding_unitADD→SUB→AND三级依赖forward_a依次为 10、01
多级嵌套中断intc低优先级中断执行中触发高优先级两次压栈、两次正确返回

第二张是时序对照表,把第 3.4 节手算的期望值录进去,仿真波形截图逐拍标号。答辩时最容易被问“你怎么知道这里是对的”,答案就是“我提前算过,波形第 12 拍的$t2应该是 0x0000_0015,截图里就是”。这张表能把“我看着像对的”变成“我验证过是对的”。

第三张是失败注入表,主动说明你测过哪些错误场景:把转发优先级改反会错在哪、把id_ex_flush去掉会错在哪、把 EPC 换成当前 PC 会错在哪。评委问“如果这里改了会怎样”,你能立刻答出来,说明你是真懂而不是背下来的。

提示:答辩现场不要一上来就放代码。先放一张流水线时空图,用不同颜色标出五级,指着说“这条 LW 在这里停了一拍,因为下一条要用它的数据”,比讲十分钟信号名有效得多。

最后给一个我自己的习惯:每次提交之前,把rst_n拉低跑满 10 个周期再放开,观察复位释放后第一个取指周期 PC 是不是 0、if_id_reg是不是全 0。这个动作花不了两分钟,但救过我两次——有一次pc_reg的复位值写成了 32'hFFFF_FFFF,仿真里因为存储器越界读返回 0 没暴露,上板直接跑飞。从那以后我每次改完流水寄存器都强制走一遍这个复位自检,希望帮到你。

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