流水线CPU控制冒险全解析:从成因、解决方案到仿真验证
简介:这份PDF是电子科技大学2021年计算机体系结构课程的实验四报告,面向正在学习流水线CPU设计、需要理解控制冒险解决思路的高校学生与Verilog初学者。报告围绕JUMP、BNE、BEQ三类跳转指令展开,讲解如何在ID级提前计算转移地址与id_z信号、通过nop部件将IF级预取指令替换为空指令,并给出解决控制冒险的电路结构图与仿真验证方法,同时涉及条件转移与数据冒险叠加时的处理。资源包共1个PDF文件,大小约1.34MB,内容为完整实验报告,含实验原理、目的、器材、步骤与代码片段,可直接对照Xilinx ISE 14.7平台复现实验流程。目前已有838人学习下载,适合作为课程实验参考与流水线CPU设计复习材料。
1. 控制冒险:一张流水线 CPU 的知识地图
流水线 CPU 的复习里,控制冒险是最容易被当成“背概念”糊弄过去的一块。很多人能背出“转移指令改变 PC,导致预取指令错误”,但一落到 Verilog 代码和仿真波形上就翻车。这份实验报告的价值在于,它把控制冒险从定义、成因、解决方案到仿真验证串成了一条完整链路,而且是在前推加暂停解决数据冒险的基础上继续叠加,属于典型的“体系结构实验第四关”。梳理下来会发现,这个方向的知识地图其实只有三层:第一层是冒险从哪来,也就是转移指令在流水线里到底影响几条后续指令;第二层是怎么消,包括暂停、预测、提前判断加废弃;第三层是消完之后怎么验证,靠指令序列和波形逐拍对。适合已经学过单周期 CPU、正在做流水线实验或准备体系结构考试的人,拿来当复习主线。
2. 控制冒险的成因:转移指令到底拖了几拍
2.1 从定义到延迟周期数
控制冒险也叫转移相关问题。程序本来是顺序执行的,但转移指令会改变这个顺序。流水线的问题在于,转移还没真正发生之前,后面若干条指令已经被取进流水线了。一旦转移成立,这些预取的指令就是错的,必须处理掉。
报告里给了一个很关键的量化关系:若转移指令从取指令到执行完毕需要 n 个周期,则会有 n-1 条后续指令受到影响。这个 n 取决于转移条件和转移地址在哪一级算出来。在单周期 CPU 里,条件转移的条件来自 ALU 计算后的 Z 寄存器,再由控制部件产生 rsrtequ 信号。到了流水线里,如果还等 Z 标志准备好,就要多停一拍。
本实验的设定是:转移条件在 ID 级计算得到,转移地址也在 ID 级完成偏移计算。因此转移指令只需要 2 个时钟周期,只有 1 条后续指令受影响。这个“2 周期、1 条受影响”是整篇报告的基准数字,后面所有电路改动都是围绕它展开的。复习时如果记不住具体电路,至少要记住这个前提,因为一旦条件判断挪到 EXE 级,受影响指令数和处理方式都会变。
提示:n-1 这个关系不是死记的,它由“条件在哪一级算出”决定。做题时先问一句:转移条件在哪一级可用?答案不同,延迟周期数就不同。
2.2 三类转移指令的差异
报告把 CPU 支持的转移指令分成三类,处理方式并不一样,这是很容易被合并记忆而搞混的地方。
| 指令类型 | 代表指令 | 是否条件转移 | 关键依赖 | 处理要点 |
|---|---|---|---|---|
| 条件转移 | beq、bne | 是 | 比较 rs 与 rt 是否相等 | 需提前得到 id_z,条件成立才废弃后续指令 |
| 无条件转移 | jump | 否 | 转移地址 | ID 级算出地址即废弃 IF 级指令 |
| 普通指令 | add、load 等 | 否 | 无 | 不触发控制冒险处理 |
条件转移的难点在于“条件”本身可能还没准备好。单周期 CPU 里条件从 ALU 的 Z 寄存器拿,流水线里如果 Z 没好就要暂停一个周期等它。但报告指出,转移指令只可能改变 PC,不改变其它状态,所以不需要封锁 WMEM、WREG 信号,因为对转移指令来说这些信号原本就是 0。这个细节经常被忽略:暂停不是把所有控制信号都冻住,而是只冻该冻的。
无条件转移没有条件判断,难点纯粹在地址计算和废弃时机。jump 在 ID 级才能算出转移地址,此时 IF 级已经进了一条新指令,所以必须用硬件把这条指令废掉。它也不存在数据冒险,因为不读通用寄存器做比较。
2.3 与前推暂停的叠加关系
这份实验不是孤立做控制冒险,而是在实验三“前推加暂停解决数据冒险”的代码上继续改。报告明确提到,条件转移指令 beq、bne 的比较寄存器如果和 ALU 指令或 LOAD 指令的目的寄存器相同,会存在数据冒险。由于实验三已经把计算结果前推到 ID 级,ID 级的比较可以直接使用前推结果。
这句话的信息量很大:控制冒险的解决不能脱离数据冒险单独看。比较器读到的 id_a、id_b 可能不是寄存器堆里的旧值,而是前推过来的新值。仿真部分也反复强调,bne 取的 r5 来自 mem_mo 前推,beq 取的 r4 来自 exe_alu_result 前推,后面还有来自 mem_alu_result 的前推。复习时如果只盯着 pcsource 和 nop,忽略了前推来源,波形一定对不上。
3. 解决方案的核心:ID 级提前判断加空指令替换
3.1 提前比较:id_z 的两种写法
报告给出的核心思路是:在 ID 级增加比较电路,提前判断两个寄存器内容是否相等,产生 id_z 信号,作为转移控制信号,也就是提前产生 rsrtequ。这样就不需要停顿等待 Z 标志准备好,节省了时间,代价是额外硬件。
id_z 有两种等价写法,报告都给了:
// 写法一:直接比较 assign id_z = (id_ra == id_rb) ? 1'b1 : 1'b0; // 写法二:按位异或后归约或 assign id_z = ~|(id_a ^ id_b);第一种写法直观,适合理解语义:两个操作数相等则 id_z 为 1。第二种写法更贴近硬件实现:先按位异或,相等时每一位异或结果为 0,再按位或归约得到 0,取反就是 1。两种写法功能一致,复习时能互相推导即可,不必纠结用哪个。参数上要注意 id_a、id_b 是 ID 级从寄存器堆取出的两个值,但它们可能已经被前推线路覆盖,这一点在 2.3 节已经说明。
注意:id_z 是“提前产生”的 rsrtequ,不是新造一个无关信号。理解它和单周期 CPU 里 rsrtequ 的对应关系,比记住变量名更重要。
3.2 pcsource 与 nop 部件:废弃指令的落地方式
提前判断只是第一步,判断出转移之后还要把 IF 级已经取进来的错误指令处理掉。报告的做法是让 id_z 在影响 pcsource 的同时影响 IF_STAGE,一旦 ID 级检测到条件转移发生或无条件转移,就及时把 IF 级指令更换为空指令。
具体到代码,新增 if_inst_org 信号作为 IF_STAGE 的输出,然后:
// pcsource 为 01 或 10 表示 ID 级是转移指令,插入空指令 // pcsource 为 00 表示非转移指令,输出原本取到的指令 assign if_inst = (pcsource == 2'b01 || pcsource == 2'b10) ? 32'h0 : if_inst_org;这里的 32'h0 就是空指令,效果相当于 nop。报告在电路结构图里把插入空指令的操作抽象成一个 nop 部件:pcsource 为 01 或 10 时输出空指令,为 00 时输出下一条指令 if_inst_org。pcsource 的取值含义要记牢:01 和 10 都代表 ID 级指令是转移指令,00 代表非转移指令。至于 01 和 10 分别对应条件转移成立和无条件转移,报告在仿真里体现为 beq 成立时 pcsource 为 01、jump 时 pcsource 为 10,复习时按这个对应关系去读波形。
Control_Unit 内部的改动是产生转移指令的 pcsource,rsrtequ 即为输入的 id_z。也就是说,控制单元不再等外部给一个迟到的条件,而是直接用 ID 级算好的 id_z 来决定下地址控制信号。这一步把“判断”和“控制”压到了同一级,是省掉等待周期的关键。
3.3 电路结构上新增了什么
报告用一张结构图说明改动,紫色线是新增的解决控制冒险的电路。梳理下来,新增部件和信号主要有四类:
- ID 级比较器:比较从寄存器堆取出的两个值,输出 id_z。
- id_z 信号:接入 Control_Unit,替代原来迟到的条件信号。
- pcsource 的新增控制作用:既决定下地址,又驱动 nop 部件。
- nop 部件与 if_inst_org:在 IF_STAGE 输出端做二选一,转移时给空指令。
这四类要能对着结构图说清楚“谁产生、谁使用”。比较器产生 id_z,Control_Unit 用 id_z 产生 pcsource,nop 部件用 pcsource 决定输出空指令还是原指令。报告要求“说明所增加功能部件及相应控制信号是如何被使用”,复习时可以用这条链自测:id_z 从哪来、到哪去、最终改变了哪条指令。
4. 仿真验证:指令序列怎么设计、波形怎么读
4.1 指令序列的设计意图
报告给了一段 16 条指令的测试序列,设计得很讲究,不是随便凑的。它同时覆盖数据冒险和控制冒险,而且把条件转移发生、不发生、无条件转移都排进去了。逐条看:
add r1,r2,r3 // r1 = 2 + 3 = 5 ori r4,r1,2 // r4 = 5 or 2 = 7 load r5,r1(2) // r5 = m(5+2) = m7 = 7 store r5,r2(3) // m(2+3) = m5 = r5 = 7 bne r4,r5,6'h09 // r4=7, r5=7,不转移 srl r3,r2,1 // r3 = 1 sll r4,r3,3 // r4 = 8 beq r4,r8,2 // 转移至 jump addi r9,r4,7 // 不执行 and r8,r9,r5 // 不执行 jump 0x0000001 // 无条件转移 nop nop xor r5,r1,r3 // r5 = 5 xor 1 = 4 and r6,r1,r2 // r6 = 5 and 2 = 0 bne r4,r5,6'h09 // r4=8, r5=4,转移报告对每条指令的冒险类型做了标注,这是复习时最该抄下来的部分:①到③的 r1 分别体现 EXE、MEM 级 rs 端的数据冒险;③加④的 r5 体现 LOAD-STORE 型数据冒险;⑥加⑦的 r3 体现 EXE 级 ALU(imme)-ALU(imme) 型数据冒险;⑭加⑯的 r5 体现 MEM 级 ALU-ALU 型数据冒险。控制冒险方面,⑤是条件转移不发生,⑧和⑯是条件转移发生,⑪是无条件转移。
这种“一条序列覆盖多类冒险”的设计思路值得学。自己设计验证序列时,先列一张表:要验证哪几类数据冒险、哪几种转移结果,然后往序列里填指令,保证每类至少出现一次,且结果可手算。
4.2 波形逐拍对照
仿真结果按时间点展开,报告给了三个局部图。把关键时间点和事件对上,是读波形的正确姿势:
| 时间 | ID 级指令 | 关键事件 | pcsource |
|---|---|---|---|
| 50ns | add 进入 IF | 流水线启动 | — |
| 150ns | add 进入 ID,ori 进入 IF | 数据冒险由前推解决 | — |
| store 进入 ID | 与 EXE 级 load 存在 LOAD 冒险 | 暂停一个周期,ALU 输出无效 | — |
| 700ns | bne r4,r5 | r4 与 r5 相等,不转移 | 00 |
| 950ns | beq r4,r8 | 相等,转移发生,下地址 bpc=0000002c | 01 |
| 后续 | jump 进入 ID | 无条件转移,下地址 jpc=00000038 | 10 |
| 末段 | bne r4,r5 | r4=8、r5=4 不等,转移发生,bpc=00000068 | 01 |
几个容易读错的地方:700ns 时 bne 不转移,pcsource 变 00,说明 nop 部件输出原指令,流水线继续顺序走;950ns 时 beq 转移,pcsource 为 01,IF 插入空指令,避免顺序下一条进入流水线;jump 时 pcsource 为 10,同样插空指令。报告特别标注了每次比较所用操作数的前推来源:bne 的 r5 来自 mem_mo,beq 的 r4 来自 exe_alu_result,末段 bne 的 r5 来自 mem_alu_result。这三条前推路径如果对不上,比较结果就会错,波形自然对不上。
提示:读波形先找 pcsource 的跳变沿,再倒推 ID 级是哪条指令、比较的两个值从哪条前推线来。顺序反了容易在几十个信号里迷路。
4.3 结果分析与结论边界
报告结论是:实验成功解决了控制冒险,并与前推解决数据冒险的方式结合,能正确运行所有 CPU 支持的指令序列。仿真把条件转移出现数据冒险的情况分成三大类验证:EXE 级 ALU 前推、MEM 级 ALU 前推、MEM 级 LOAD 前推,结果均正确。
这里要看清结论的边界。报告在改进建议里自己说了:由于时间限制只完成了 ID 级处理控制冒险,实际上还可以在 EXE 级完成。这句话说明本实验的方案是“ID 级提前判断”这一种,不是唯一解。复习时要把“本实验怎么做”和“这个问题有几种解法”分开记,否则容易把一种实现当成标准答案。
5. 易错点:这五处最容易翻车
5.1 易错点一:把暂停当成封锁所有控制信号
为什么漏:直觉上“暂停流水线”就是把所有信号冻住,但报告明确说转移指令只改变 PC,不改变其它状态,不需要封锁 WMEM、WREG,因为这些信号对转移指令原本就是 0。
怎么补:暂停前先问“这条指令会写什么”。转移指令不写寄存器、不写内存,所以只冻 PC 相关和取指相关即可。复习时把“哪些信号该冻”列成一张小表,比背整段文字牢。
5.2 易错点二:忽略 id_z 的输入可能来自前推
为什么漏:看代码只看到比较器比较 id_a 和 id_b,容易以为比的是寄存器堆原值。但报告在仿真里反复标注前推来源,说明比较值可能已被前推覆盖。
怎么补:把前推路径和比较器连起来看。EXE 级 ALU 结果、MEM 级 ALU 结果、MEM 级 LOAD 结果都可能前推到 ID 级参与比较。做波形对照时,先确认当前拍哪条前推线有效。
5.3 易错点三:pcsource 的 01 和 10 记混
为什么漏:两个值都表示“是转移指令”,都触发插空指令,功能上看起来一样,容易合并记忆。但报告在仿真里区分了 beq 成立对应 01、jump 对应 10。
怎么补:按“条件转移成立”和“无条件转移”分别记,并回到 Control_Unit 代码看 pcsource 是怎么被赋值的。功能相同不代表编码相同,波形题常在这里设坑。
5.4 易错点四:以为控制冒险可以完全脱离数据冒险
为什么漏:章节标题把控制冒险单独拎出来,容易让人以为它是独立问题。但报告开篇就建立在实验三前推加暂停的基础上,条件转移的比较还要用前推结果。
怎么补:复习时把“数据冒险解决到什么程度”作为控制冒险的前置条件。前推没做对,ID 级比较拿到的就是旧值,转移判断直接错。
5.5 易错点五:只验证转移发生,不验证转移不发生
为什么漏:转移发生时有插空指令、pcsource 跳变,现象明显,容易只测这一种。但报告序列里专门放了⑤这种“条件转移不发生”的情况,pcsource 保持 00。
怎么补:验证序列至少覆盖三种结果:条件成立转移、条件不成立不转移、无条件转移。每种都手算预期结果,再对波形。少一种,就等于少测一条路径。
6. 复习清单:从知识地图到查漏补缺的落地方法
复习到这个阶段,最有效的做法不是再读一遍报告,而是拿一张白纸做“反向复现”。具体分三步走,每步都对应前面章节的检查点。
第一步,画因果链。从“转移指令进入流水线”开始,写出它影响几条后续指令、为什么是 n-1、本实验为什么是 2 周期 1 条。写不出来就回到第 2 章。这一步检验的是成因理解,不是电路记忆。
第二步,默写信号链。按“比较器产生 id_z → Control_Unit 用 id_z 产生 pcsource → nop 部件用 pcsource 决定 if_inst”这条链,把每个信号的产生者和使用者写清楚。然后对照下面这张自检表逐项打勾:
| 检查项 | 能说清 | 说不清时回看 |
|---|---|---|
| id_z 两种写法及等价性 | □ | 3.1 |
| pcsource 三个取值的含义 | □ | 3.2、5.3 |
| nop 部件何时输出空指令 | □ | 3.2 |
| 条件转移与数据冒险的叠加 | □ | 2.3、5.4 |
| 三类前推来源对应哪条指令 | □ | 4.2 |
| 转移不发生时的波形特征 | □ | 4.2、5.5 |
第三步,手算一段小序列。不用报告那 16 条,自己写 5 到 6 条:一条 ALU 指令、一条 load、一条 bne 且条件成立、一条 jump、一条转移目标指令。逐拍写出 ID 级是哪条、比较值从哪来、pcsource 取什么、IF 级输出空指令还是原指令。算完再和报告 4.1 的设计意图对照,看自己漏了哪类冒险。
查漏补缺的重点放在两个地方。一是“提前判断”和“废弃指令”是两件事,前者靠比较器,后者靠 nop 部件,缺一不可;很多人只记住比较器,忘了废弃,波形上就会看到错误指令继续往下走。二是结论边界,报告只做到 ID 级,EXE 级方案是留白,考试或答辩如果问“还有没有别的解法”,要能说出预测转移、延迟槽这类思路的存在,而不是把 ID 级方案当成唯一答案。
我自己的血泪经验是:第一次做这类实验时,波形对不上就反复改代码,结果越改越乱。后来强制自己先手算预期波形,再拿仿真去对,对不上就按“比较值来源 → pcsource → if_inst”三段定位,通常十分钟内能找到问题。从那以后我每次做流水线实验,都强制先写预期再跑仿真,这个习惯比记住任何一段代码都管用。这份报告把控制冒险的成因、电路改动和仿真验证串得比较完整,拿来当复习底稿,配合上面的清单过一遍,查漏补缺会快很多。希望帮到你。
本文还有配套的精品资源,点击获取