简介:这份2021年电子科技大学计算机体系结构实验四报告,面向正在学习流水线CPU设计、需要攻克控制冒险难题的高校学生与Verilog自学者。报告围绕JUMP无条件跳转及BNE、BEQ条件跳转引发的控制冒险展开,讲解如何在ID级提前比较寄存器产生id_z信号、用nop部件将IF级预取指令替换为空指令,并给出转移指令序列的仿真验证思路与电路结构图修改说明。资源包共1个PDF文件,大小约1.34MB,内容涵盖实验原理、目的、器材、步骤与代码片段,可直接对照Xilinx ISE 14.7平台复现实验。已有838人学习下载,适合作为流水线控制冒险解决方案的参考范本,帮助读者理解单周期与流水线CPU的差异、掌握前推与暂停结合的硬件实现细节,并完成结构图绘制与仿真排错。
1. 从一条 beq 指令说起:流水线控制冒险到底卡在哪
流水线跑到第五条指令时,一条beq突然跳转,前面已经取进 IF 级的指令瞬间变成“废指令”——这就是控制冒险最直观的表现。很多同学做计算机体系结构课设时,数据冒险靠前推勉强跑通,一到转移指令仿真波形就全乱,PC 值对不上、寄存器结果错位,排查半天找不到问题。这份实验报告解决的正是这个卡点:在五级流水线 CPU 上,用 ID 级提前比较加空指令插入的方式,把 JUMP、BNE、BEQ 三类转移指令的控制冒险一次性处理干净,并给出可仿真的完整指令序列。它适合正在做流水线 CPU 课设、需要交一份能跑通仿真的实验报告的学生,也适合想参考一套完整 Verilog 实现思路的从业者。整套方案基于 Verilog HDL 和 Xilinx ISE 14.7 平台,四个学时的工作量,结构清晰、验证闭环。
2. 需求与总体设计:为什么必须在 ID 级解决转移判断
2.1 控制冒险的成因与延迟周期账
控制冒险也叫转移相关问题。转移指令会改变程序顺序执行的特性,而在流水线里,转移发生之前若干条后续指令已经被取进流水线。等转移真正生效,这些预取的指令就造成了错误结果。报告里给了一个很关键的账:若转移指令从取指到执行完毕需要 n 个周期,就会有 n-1 条后续指令受影响。
本实验的 CPU 把转移条件的计算放在 ID 级,转移地址的偏移计算也在 ID 级完成,所以转移指令只需要 2 个时钟周期,受影响的只有 1 条后续指令。这个数字直接决定了后面方案的复杂度——只需要处理一条废指令,不需要做深度的流水线冲刷逻辑。很多同学一上来就想照搬教材里的分支预测或者多周期停顿方案,其实先把“到底几条指令受影响”算清楚,方案自然就收敛了。
2.2 三种可选路线与选型理由
面对控制冒险,通常有三条路可走。第一条是暂停等待:条件转移的 Z 标志没准备好就停一个周期,等 ALU 算完再判断。第二条是预测:预测转移发生或不发生,猜错了再废弃错误指令。第三条是提前判断:把比较电路挪到 ID 级,提前产生等价于 rsrtequ 的信号。
报告选的是第三条,理由很实在。暂停等待要多花一个周期,性能上有损失;预测机制需要额外的预测逻辑和失败恢复路径,四个学时的实验里实现成本偏高。而在 ID 级增加一个比较器,直接比较从寄存器堆取出的 id_a 和 id_b,产生 id_z 信号,就能省掉等待 Z 标志的那一个周期。代价是需要额外的硬件,但换来的时序收益和实现简洁性在这个场景下更划算。我一般会建议做课设的同学优先考虑这条路线,因为它把“判断”和“取指”放在同一级,后续的废指令处理逻辑非常干净。
2.3 总体数据通路与新增部件职责
整个方案建立在“前推加暂停解决数据冒险”的流水线基础之上,新增的解决控制冒险的电路主要落在 ID 级和 IF 级之间。核心新增部件有三个:ID 级的比较器,负责产生 id_z;控制单元里对 pcsource 的生成逻辑,负责区分无条件转移、条件转移发生、条件转移不发生三种情况;以及一个抽象出来的 nop 部件,负责在检测到转移时把 IF 级指令替换成空指令。
pcsource 的取值是整个方案的枢纽:为 01 或 10 时表示 ID 级指令是转移指令(01 对应条件转移发生,10 对应无条件转移),此时 nop 部件输出空指令;为 00 时表示非转移指令,nop 部件输出原本的下一条指令 if_inst_org。这个二选一的逻辑,就是“废弃后续指令”这个需求最直接的硬件表达。把复杂问题抽象成一个 nop 部件,是这份报告在结构设计上比较聪明的地方,画结构图和写代码时都不容易乱。
3. 关键模块实现:比较器、pcsource 与空指令插入
3.1 ID 级比较器与 id_z 信号生成
条件转移的本质是比较两个寄存器的值。单周期 CPU 里这个比较要等 ALU 算完从 Z 寄存器拿结果,流水线里则可以在 ID 级直接用取出的寄存器值做比较。报告给出的 id_z 表达式有两种等价写法:
// 写法一:直接比较 assign id_z = (id_ra == id_rb) ? 1'b1 : 1'b0; // 写法二:按位异或后归约或 assign id_z = ~|(id_a ^ id_b);第一种写法可读性好,综合工具也能识别成比较器;第二种写法在门级实现上更省资源,异或后只要所有位都为 0,归约或的结果就是 0,取反即为 1。两种写法在功能上完全等价,选哪种看个人习惯和综合报告。id_z 产生后作为 rsrtequ 信号送进控制单元,替代原来从 ALU 的 Z 标志拿到的判断结果。这里要注意,id_a 和 id_b 必须是经过前推修正后的值,否则遇到数据冒险时比较的就是过期数据,这是后面避坑章节要重点说的点。
3.2 控制单元里 pcsource 的生成逻辑
控制单元内部根据指令类型和 id_z 决定 pcsource。条件转移指令 beq、bne 在 id_z 满足条件时输出 01,无条件转移 jump 直接输出 10,其余情况输出 00。下面这段是控制单元里转移相关分支的核心结构:
// Control_Unit 内部转移控制逻辑(示意) always @(*) begin pcsource = 2'b00; // 默认顺序执行 if (inst_jump) begin pcsource = 2'b10; // 无条件转移,直接改 PC end else if (inst_beq && id_z) begin pcsource = 2'b01; // beq 且相等,转移发生 end else if (inst_bne && !id_z) begin pcsource = 2'b01; // bne 且不等,转移发生 end end这段逻辑的关键在于 id_z 直接参与 pcsource 的判定,而不是等 ALU 算完。beq 在 id_z 为 1 时跳,bne 在 id_z 为 0 时跳,两个条件刚好互补。pcsource 一旦确定,既影响下一条 PC 的选择(bpc 或 jpc),也影响 IF 级的指令替换,一信号两用,这是整个方案能省掉额外控制信号的原因。写这段代码时容易犯的错是把 bne 的判断写成id_z,逻辑刚好反了,仿真时表现为该跳的时候不跳,波形上 pcsource 一直是 00,排查时优先看这里。
3.3 IF 级空指令插入与顶层连线
废弃 IF 级那条错误指令,报告的做法是在顶层模块里加一个 if_inst_org 信号作为 IF_STAGE 的原始输出,再用 pcsource 做一次选择:
// 顶层模块:根据 pcsource 决定送入流水线的指令 assign if_inst = (pcsource == 2'b01 || pcsource == 2'b10) ? 32'h0 // 转移发生,插入空指令 : if_inst_org; // 非转移,正常取下一条32'h0 在本 CPU 的指令编码里对应空指令,流进流水线后不改变任何寄存器或存储器状态,等于把这条废指令“无害化”。这里的设计取舍值得说一句:与其做复杂的流水线冲刷、清空各级流水寄存器,不如在入口处就把指令换成空指令,让它自然流过各级,逻辑简单且不会引入新的时序问题。代价是空指令仍然占用一个周期的流水线带宽,但在只有一条后续指令受影响的场景下,这个开销可以接受。顶层还需要把 id_z 作为新信号引出,并修改 ID_STAGE 的输入端口,把比较结果接进控制单元,这几处连线改动虽小,但漏一根就仿真不出正确波形。
3.4 与前推机制结合时的数据来源
控制冒险很少单独出现,条件转移指令的比较寄存器往往正好是前面 ALU 或 LOAD 指令的目的寄存器,这就叠上了数据冒险。报告的处理思路是复用实验三的前推结果:ID 级比较时,id_a 和 id_b 取的是经过前推选择后的值,可能来自 EXE 级 ALU 结果、MEM 级 ALU 结果,也可能来自 MEM 级 LOAD 数据。仿真里专门验证了三种来源——EXE 级 ALU 前推、MEM 级 ALU 前推、MEM 级 LOAD 前推,分别对应不同的冒险距离。
这意味着比较器的输入不能直接接寄存器堆的原始输出,而要接前推多路选择器的输出。如果这一层没接对,表现是:单独测转移指令没问题,一旦转移指令前面跟一条 ALU 指令,比较结果就错。这是控制冒险和数据冒险耦合时最隐蔽的坑,后面会展开。
4. 仿真验证:用一条指令序列把三类冒险一次测全
4.1 测试序列的设计意图
验证不能只测一条孤立的 beq,要设计一条能同时覆盖数据冒险和控制冒险的指令序列。报告给的序列从 add 开始,穿插 ori、load、store,然后接 bne、beq、jump,中间还安排了不执行的指令来验证废弃逻辑。这条序列的设计意图很明确:第 5 条 bne 测条件转移不发生,第 8 条 beq 测条件转移发生并跳到 jump,第 11 条 jump 测无条件转移,第 16 条 bne 再测一次转移发生且比较值来自 MEM 级前推。
序列里还刻意埋了数据冒险:第 1 到 3 条的 r1 分别体现 EXE、MEM 级 rs 端的数据冒险,第 3 加第 4 条的 r5 是 LOAD-STORE 型冒险,第 6 加第 7 条的 r3 是 EXE 级 ALU 到 ALU 的冒险,第 14 加第 16 条的 r5 是 MEM 级 ALU 到 ALU 的冒险。一条序列把前推和控制冒险的交叉场景基本覆盖全了,这种设计思路值得直接借鉴到自己的课设里。
4.2 指令序列与机器码对照
把汇编序列翻译成机器码写进 ROM,是仿真能跑起来的前提。报告给出的对照如下(节选关键几条):
assign rom[6'h01] = 32'h00100443; // add r1,r2,r3 assign rom[6'h02] = 32'h28000824; // ori r4,r1,2 assign rom[6'h03] = 32'h34000825; // load r5,r1(2) assign rom[6'h04] = 32'h38000c45; // store r5,r2(3) assign rom[6'h05] = 32'h40002485; // bne r4,r5,6'h09 assign rom[6'h08] = 32'h3c000888; // beq r4,r8,2 assign rom[6'h0B] = 32'h4800000E; // jump 0x000000E assign rom[6'h10] = 32'h40002485; // bne r4,r5,6'h09每条机器码的字段划分要和自己 CPU 的指令格式严格对应,opcode、rs、rt、立即数偏移各占哪几位,错一位仿真结果就全偏。写 ROM 初始化时建议按地址顺序排列并加注释,后面看波形对指令时能省大量时间。第 9、10 条(addi、and)是故意放在 beq 后面等着被废弃的,仿真时它们的执行结果应该保持不变,这是验证废弃逻辑是否生效的直接证据。
4.3 波形关键节点解读
看波形要抓几个关键时间点。50ns 时第一条 add 进入 IF 级,150ns 进入 ID 级,同时第二条 ori 进入 IF 级,两者之间的数据冒险由前推解决,流水线不停。前三条指令的 ALU 结果依次是 5、7、7,对应 r1=2+3=5、r4=5 or 2=7、m(5+2)=m7=7。
store 进入 ID 级时与 EXE 级的 load 构成 LOAD 数据冒险,需要暂停一个周期,波形上能看到红色框内 ALU 输出无效。暂停后 store 进入 EXE 级,输出 m(2+3)=m5。此时 ID 级是 bne,比较 r4 与 r5 相等,转移不发生,700ns 时 pcsource 变为 00。注意这里 bne 取的 r5 来自 MEM 级数据前推,波形上要顺着前推选择信号去核对。
950ns 时 ID 级是 beq,r4 与 r8 相等,转移发生,pcsource 为 01,下地址 bpc=0000002c,IF 插入空指令。随后空指令进 EXE,ID 级变成 jump,pcsource 为 10,下一条地址 jpc=00000038。等空指令流过 EXE 后,xor 和 and 依次执行,结果 4 和 0 符合预期。最后一条 bne 比较 r4=8 与 r5=4 不相等,转移发生,pcsource 为 01,目标地址 bpc=00000068。整条序列跑完,所有结果与手算一致,方案验证闭环。
5. 避坑与排查:控制冒险实现里最容易翻车的五个点
5.1 现象:单独测转移指令正常,前面加一条 ALU 指令就错
原因:ID 级比较器的输入直接接了寄存器堆原始输出,没有经过前推多路选择器。当转移指令的比较寄存器正好是前一条指令的目的寄存器时,寄存器堆里还是旧值,id_z 判断错误。
解决:把 id_a、id_b 接到前推选择器的输出端,确保比较用的是最新值。验证方法是构造“ALU 指令紧跟 beq 且目的寄存器相同”的最小序列,看 id_z 是否随前推结果变化。
5.2 现象:bne 该跳的时候不跳,pcsource 始终为 00
原因:控制单元里 bne 的判断条件写反了,写成了id_z而不是!id_z。beq 和 bne 的跳转条件互补,很容易复制粘贴时漏改取反。
解决:对照指令语义逐条核对,beq 相等跳、bne 不等跳。仿真时单独跑一条 bne 且两寄存器不等的序列,观察 pcsource 是否变 01。
5.3 现象:转移发生后,后面那条本该废弃的指令仍然改了寄存器
原因:if_inst 的选择逻辑没接对,或者 pcsource 的判定晚了一个周期。空指令插入必须在 ID 级检测到转移的同一周期生效,晚一拍那条错误指令就已经带着有效控制信号进了流水线。
解决:检查顶层 if_inst 的赋值是否用的当前周期 pcsource,而不是打拍后的信号。波形上确认转移指令在 ID 级的那个周期,IF 级输出是否已经是 32'h0。
5.4 现象:jump 指令跳转地址对,但跳转后第一条指令执行异常
原因:jump 的 pcsource 为 10 时同样需要插入空指令,如果只处理了条件转移的 01 分支,漏了 10 分支,jump 后面那条顺序指令就没被废弃。
解决:if_inst 的判断条件写成pcsource == 2'b01 || pcsource == 2'b10,两个转移分支都要覆盖。这是报告代码里明确写出的写法,照抄时别自作主张简化。
5.5 现象:仿真波形里 PC 值跳变时机和预期差一个周期
原因:PC 更新、pcsource 生成、空指令插入三者的时序关系没理清。pcsource 在 ID 级组合产生,PC 在时钟沿更新,空指令在同一沿进入流水线,三者必须对齐到同一个时钟节拍。
解决:画一张简单的时序对照表,标出每个信号在哪个时钟沿变化,仿真时逐拍核对。这类问题靠盯波形硬看效率低,先把时序关系写清楚再对照,能省一半时间。
6. 验收与答辩:怎么证明这套方案真的解决了控制冒险
验收这件事,核心不是把波形截图堆上去,而是让评审看到“判断依据—实现—证据”这条链是闭合的。我一般会按三步走。
第一步,先讲清楚延迟周期账。开口就说:本 CPU 转移条件在 ID 级计算,转移指令占 2 个周期,只有 1 条后续指令受影响,所以方案只需要处理一条废指令。这个前提一立,后面所有设计选择都有了合理性,评审也不会追问“为什么不做分支预测”。
第二步,用一张对照表把三类转移场景和验证证据绑死。表格比口头描述有说服力:
| 场景 | 指令 | 预期 pcsource | 波形证据 |
|---|---|---|---|
| 条件转移不发生 | bne r4,r5 | 00 | 700ns 处 pcsource 保持 00 |
| 条件转移发生 | beq r4,r8 | 01 | 950ns 处 bpc=0000002c,IF 插入空指令 |
| 无条件转移 | jump | 10 | jpc=00000038,后续空指令流过 |
| 转移叠加数据冒险 | bne 取 MEM 前推值 | 01 | 比较值来自 mem_alu_result |
第三步,主动交代边界。报告里那句“由于时间限制只完成了 ID 级处理控制冒险,实际上还可以在 EXE 级完成”,答辩时不要藏,主动说出来反而是加分项——它证明你知道方案的边界在哪。评审问“还有什么改进空间”,这就是现成答案。
答辩时还有一个技巧:把那条覆盖三类冒险的指令序列当成主线,从第 1 条讲到第 16 条,每讲到一条转移指令就停一下,指出波形上 pcsource 的变化和空指令的插入。评审跟着你的节奏走一遍,比你自己罗列十个模块有效得多。
血泪经验是,我第一次做这类课设时,波形截图截了一堆却没标时间点和信号名,答辩时自己都找不到对应位置,被追问得很难看。从那以后我每次做流水线仿真,都强制在关键时间点加标记、在截图旁写一句“此处验证了什么”,验收材料一次过。希望帮到你。
本文还有配套的精品资源,点击获取