简介:这份资源是2021年电子科技大学计算机体系结构实验四的完整实验报告,面向正在学习流水线CPU设计、需要理解控制冒险解决思路的高校学生与Verilog初学者。报告围绕JUMP、BNE、BEQ三类跳转指令展开,讲解如何在ID级提前计算转移地址与id_z信号、通过nop部件将IF级预取指令替换为空指令,并给出转移指令与数据冒险叠加时的处理方式及电路结构图修改说明。资源包共1个PDF文件,大小约1.34MB,内容涵盖实验原理、目的、器材、步骤与仿真验证,配有代码片段和结构图,便于对照复现实验流程、梳理控制冒险与数据冒险的结合场景。目前已有838人学习,适合作为流水线CPU实验的参考材料,帮助读者理解单周期与流水线CPU的差异并掌握Xilinx ISE环境下的调试思路。
1. 控制冒险:为什么一条跳转指令能让流水线“翻车”
流水线 CPU 最反直觉的一点是:指令并不是一条接一条“执行完”的,而是像流水线工厂一样,取指、译码、执行、访存、写回五道工序同时开工。理想情况下每个时钟周期都能产出一条指令的结果,CPI 逼近 1。可一旦遇到beq、bne、jump这类转移指令,这套节奏立刻被打乱——因为转移指令要改变 PC,而 PC 一变,之前按顺序预取进流水线的那几条指令就全成了“错的人”。
这就是控制冒险,也叫转移相关。它的本质不是数据算错了,而是取指方向错了。在单周期 CPU 里这根本不是问题:一条指令做完再取下一条,跳转与否当场就知道。但流水线为了吞吐率,必须提前取指,于是“提前”和“跳转”天然冲突。实验报告里给出的数字很关键:若转移指令从取指到执行完毕需要 n 个周期,就有 n-1 条后续指令受影响。本实验把转移条件和转移地址都提前到 ID 级算,转移指令只需 2 个周期,受影响的只有 1 条指令——但就是这 1 条,处理不好整个程序结果就全错。
搞懂这个机制的价值在于:它是理解“流水线为什么需要旁路、暂停、预测、冲刷”的总入口。数据冒险解决的是“值还没算出来”,控制冒险解决的是“路走错了”,后者更彻底,因为走错路的指令可能已经改了寄存器。这份实验报告用 Verilog 把 ID 级提前判断 + 空指令冲刷的完整链路写了出来,是少见的能把“为什么这么设计”和“代码怎么落地”对上的材料。
2. 转移条件为什么必须提前到 ID 级:Z 标志的时序账
2.1 单周期 CPU 的 Z 标志从哪来,为什么流水线里不能照搬
单周期 CPU 里,条件转移的判断依据是 ALU 计算后产生的 Z 标志。指令在同一个周期内走完取指、译码、执行,ALU 算完,Z 就绪,控制部件据此产生rsrtequ,决定 PC 下一拍取哪条。整个链条在一个时钟周期内闭合,没有任何时序缺口。
流水线把这个链条拉长成了五拍。如果还等 ALU 在 EXE 级算完 Z 再决定跳不跳,那么从取指到 Z 可用之间隔了好几个周期,这期间 IF 级已经顺着顺序地址取了好几条指令。等 Z 终于出来发现要跳转,前面取的指令全部作废。报告里说得很直白:条件转移指令需要先判断 Z 标志是否能用,如果 Z 没有准备好,要暂停流水线一个时钟周期等待 Z 标志。暂停能保证正确,但代价是每个条件转移都白等一拍,流水线效率被硬生生拉低。
更麻烦的是,暂停只能挡住“继续取指”,挡不住“已经取进来的指令”。所以暂停和冲刷必须配合使用,这是后面所有设计的基调。
2.2 ID 级加比较器:用硬件换掉那一拍等待
实验采用的核心手法,是在 ID 级直接增加一个比较电路,把寄存器堆读出的两个操作数id_a和id_b当场比掉,提前产生id_z。报告给出的表达式有两种等价写法:
// 方式一:直接比较 assign id_z = (id_a == id_b) ? 1'b1 : 1'b0; // 方式二:按位异或后归约或,全 0 即相等 assign id_z = ~|(id_a ^ id_b);第二种写法值得多说一句。id_a ^ id_b逐位异或,只有对应位全部相同结果才全 0;再用归约或|压成一位,全 0 时归约或为 0,取反就是 1。这在硬件里比一个 32 位等值比较器更贴近门级实现,综合出来就是一排异或门加一棵或树,延迟低、面积可控。我一般会这么理解:等值比较在 RTL 里写==最直观,但真正落到门电路,异或归约才是它的“素颜”。
这个id_z直接顶替了原来从 ALU 送来的rsrtequ,接到Control_Unit的输入上。于是控制部件在 ID 级就能知道“这条转移到底跳不跳”,pcsource也就在 ID 级产生,而不是等到 EXE 级。报告里明确写了:据此就可以在 ID 级检测出无条件转移指令和转移条件发生的条件转移指令,并输出正确的下地址控制信号pcsource。
代价是什么?多了一个 32 位比较器,以及id_z这条跨模块信号。报告自己也承认:上述方法不需要停顿等待一个周期到 Z 标志准备好,节省了时间,但需要额外的硬件实现。这就是典型的空间换时间——用一块组合逻辑,换掉每个条件转移指令固定的一拍停顿。
2.3 转移地址也在 ID 级算完:2 周期转移的由来
光有条件还不够,还得知道跳到哪。实验里转移地址的偏移计算同样放在 ID 级完成,配合 ID 级已经拿到的指令字段和 PC 值,目标地址在 ID 级就能算出来。条件和地址都在 ID 级就绪,转移指令从取指到“决策完成”只需要 2 个时钟周期:第一拍进 IF,第二拍进 ID 并当场判定。
这个 2 周期是整篇报告的时间基准。它意味着当转移指令走到 ID 级时,IF 级刚好已经取进了它后面那一条顺序指令。也就是说,受影响的指令数被压缩到了 1 条。设计目标随之变得非常清晰:只要在 ID 级判定为“要跳”的那一拍,把 IF 级这条刚取进来的指令换成空指令,问题就解决了。不需要复杂的多级冲刷,只需要一个二选一。
提示:把条件和地址都压到 ID 级,本质是在“流水线深度”和“判定提前度”之间找平衡点。压得越靠前,受影响的指令越少,但 ID 级的组合逻辑越长,可能拖慢整个时钟周期。这是后面要反复权衡的一条主线。
3. 冲刷那一拍:nop 部件如何把 IF 级指令换成空指令
3.1 pcsource 三种取值对应的三种命运
整个控制冒险的决策,最后都收敛到pcsource这个两位信号上。报告里把它和指令类型、IF 级行为绑得很紧:
| pcsource | 含义 | IF 级输出 |
|---|---|---|
| 2'b00 | ID 级是非转移指令 | 正常输出下一条指令if_inst_org |
| 2'b01 | 条件转移且条件成立 | 输出空指令 32'h0 |
| 2'b10 | 无条件转移 jump | 输出空指令 32'h0 |
这张表是理解整个机制的钥匙。pcsource不只是“选下一条 PC”,它还兼任“要不要冲刷 IF 级”的开关。报告里把这个动作抽象成一个 nop 部件:当pcsource为 01 或 10 时,表示 ID 级指令为转移指令,此时 nop 部件输出空指令;当pcsource为 00 时,输出下一条指令if_inst_org。
为什么空指令能“消除影响”?因为空指令32'h0在译码后不会产生任何有效的写寄存器、写内存控制信号,它流过流水线各段时对架构状态零副作用。它占了一拍流水线位置,但不会污染结果。这就是流水线里最便宜的“后悔药”。
3.2 顶层那行三目运算符:冲刷逻辑的落点
真正把 nop 部件落到代码上的,是顶层模块里对if_inst的重新赋值。报告给出的写法是:
// if_inst_org 是 IF_STAGE 正常取出的指令 // 一旦 ID 级判定为转移(条件成立或无条件),就把进流水线的指令换成空指令 assign if_inst = (pcsource == 2'b01 || pcsource == 2'b10) ? 32'h0 : if_inst_org;这行代码看着简单,机制上却做了两件事。第一,它把“决策”和“执行”解耦:pcsource在 ID 级产生,if_inst的替换发生在指令进入流水线寄存器之前,两者在同一拍内完成组合,不需要额外的状态机。第二,它把冲刷范围精确限定在 IF 级这一条指令上,因为 2 周期转移只影响这一条,多冲是浪费,少冲是错误。
参数上要注意:if_inst_org必须是 IF_STAGE 的原始输出,不能被前面的逻辑改写;pcsource必须来自 ID 级当拍的判定结果,不能是打拍后的延迟版本,否则冲刷会晚一拍,那条错误指令就已经进流水线了。这两个时序约束是这段代码能 work 的前提,也是仿真里最容易翻车的地方。
3.3 为什么不用暂停,而用冲刷
有人会问:既然数据冒险用暂停解决,控制冒险为什么不也暂停一拍等决策?因为暂停解决不了根本问题。暂停只是让流水线停住,但 IF 级那条已经取进来的顺序指令还在流水线寄存器里,等暂停结束它照样往下走。要让它消失,必须主动把它替换掉。冲刷是“删掉错的”,暂停是“等对的”,控制冒险需要的是前者。
报告里也提到预测转移发生或预测转移不发生的方法,预测失败时同样要废弃错误指令。本实验没有走预测路线,而是走“ID 级提前判定 + 精确冲刷”,好处是不需要预测器、不需要失败恢复逻辑,代价是 ID 级组合逻辑变长。这是一条更朴素但更可控的路径,适合教学和中小规模设计。
4. 控制冒险撞上数据冒险:前推结果怎么喂给 ID 级比较器
4.1 三类前推路径,喂的是同一个比较器
控制冒险单独存在时并不难,难的是它和数据冒险叠在一起。报告点明了关键场景:条件转移指令 beq、bne 的比较寄存器,如果正好是前面 ALU 指令或 LOAD 指令的目的寄存器,就存在数据冒险。而实验三已经把计算结果前推到 ID 级,所以 ID 级比较可以直接使用前推结果。
这句话背后是一条完整的旁路网络。ID 级比较器要拿id_a和id_b,但这两个值可能还没写回寄存器堆,得从流水线中途“借”过来。报告在仿真分析里把来源分得很细:bne 取的 r5 来自 mem_mo 前推,beq 取的 r4 来自 exe_alu_result 前推,另一处 beq 取的 r5 来自 mem_alu_result 前推。归纳起来就是三类:
- EXE 级 ALU 结果前推:上一条 ALU 指令刚算完,结果还在 EXE 级,直接旁路到 ID 级比较器。
- MEM 级 ALU 结果前推:ALU 结果已进 MEM 级,从 MEM 段旁路回来。
- MEM 级 LOAD 前推:LOAD 指令的数据刚从内存取出,从 MEM 段旁路回来。
这三条路径最终都汇到 ID 级比较器的两个输入端,由前推选择逻辑按优先级挑一个。优先级通常是 EXE 最新、MEM 次之、寄存器堆最旧,因为越新的结果越接近真实值。
4.2 一个容易忽略的细节:比较器看到的是“前推后”的值
这里有个时序陷阱。ID 级比较器是组合逻辑,它的输入必须在同一拍内稳定。如果前推选择信号晚到,比较器输出的id_z就会抖动,pcsource跟着抖,if_inst的冲刷决策就可能出错。所以前推逻辑和比较器必须放在同一级组合路径里,不能中间插寄存器。
报告里那句“ID 级比较可以直接使用前推结果”,翻译成实现语言就是:前推多路选择器的输出直接连到比较器输入,中间不隔拍。这也解释了为什么实验要把前推做到 ID 级——如果前推只做到 EXE 级,ID 级比较器就只能读寄存器堆里的旧值,条件判断会错,控制冒险解决得再漂亮也没用。数据冒险和控制冒险在这里是耦合的,不能分开看。
4.3 指令序列怎么把两类冒险同时逼出来
报告设计的测试序列很讲究,不是随便凑几条指令,而是刻意让数据冒险和控制冒险交织。序列里 add、ori、load、store 制造出 EXE、MEM 级的数据相关,紧接着 bne、beq、jump 制造控制相关,中间还夹着 srl、sll 这类 ALU 指令继续制造前推需求。
比如第 5 条bne r4,r5,6'h09,它的 r5 来自第 3 条 load 的结果,走的是 MEM 级 LOAD 前推;第 8 条beq r4,r8,2的 r4 来自第 7 条 sll,走 EXE 级 ALU 前推;第 16 条bne r4,r5,6'h09的 r5 来自第 14 条 xor,走 MEM 级 ALU 前推。三条转移指令,三种前推来源,正好把三类路径全覆盖。这种“一条序列打穿所有分支”的设计思路,比分别写三个小测试更能暴露时序问题,因为真实程序里冒险本来就是混着来的。
5. 避坑与排查:控制冒险实现里最容易翻车的几处
5.1 现象:跳转发生了,但后面还是执行了顺序指令
原因通常是pcsource产生晚了一拍,或者if_inst的替换用的是打拍后的pcsource。ID 级判定当拍没来得及冲刷,那条顺序指令已经进了流水线寄存器,后面再想删就删不掉了。
解决:确认pcsource和if_inst的三目运算在同一拍组合路径内,中间不插寄存器;仿真时盯住 ID 级判定那一拍的if_inst值,看它是否当拍变成32'h0。
5.2 现象:条件转移该跳却没跳,id_z一直是 0
多半是前推没接进 ID 级比较器,比较器读的是寄存器堆旧值。尤其是 LOAD 后紧跟 bne 的场景,寄存器堆还没写回,旧值必然错。
解决:检查前推多路选择器的输出是否直连比较器输入;按 EXE、MEM、寄存器堆的优先级核对选择信号;用报告里第 5 条 bne 这种 LOAD 相关场景单独验证。
5.3 现象:jump 指令后面跟了一条莫名其妙的写寄存器操作
pcsource为 10 时冲刷没生效,或者空指令32'h0被译码成了有效指令。有些设计的译码器对全 0 指令处理不干净,会误产生控制信号。
解决:确认空指令在控制单元里被显式识别为无操作,所有写使能强制为 0;检查pcsource == 2'b10是否真的进了冲刷分支,别被优先级更高的条件覆盖。
5.4 现象:仿真波形里 ALU 输出出现一段无效值,误以为算错了
这其实是暂停或冲刷期间的正常现象。报告在图 9-1 里就标注了红色框内 ALU 输出无效,那是 LOAD 数据冒险暂停的一拍。控制冒险冲刷时同样会有无效拍。
解决:不要盯着无效拍的 ALU 输出下结论,要看有效指令流过 EXE 级时的蓝色结果线;把无效拍和指令边界对齐后再判断对错。
5.5 现象:单独测控制冒险没问题,混着数据冒险就错
典型原因是前推优先级写反,或者控制冒险的冲刷信号把前推路径打断了。两类冒险共享 ID 级资源,任何一方时序被拉长都会互相拖累。
解决:用报告那种“数据+控制”混合序列做回归,而不是分开测;重点看转移指令进 ID 级那一拍,比较器输入是否已经是前推后的正确值。
6. 把判定放在 ID 级,是一笔什么样的取舍
这套方案的核心决策只有一个:把转移条件和转移地址都提前到 ID 级。它换来的是转移指令只需 2 个周期、只影响 1 条后续指令、冲刷逻辑简化成一个二选一。代价是 ID 级的组合逻辑变长——比较器、地址计算、前推选择全挤在一级里,关键路径可能成为时钟频率的瓶颈。报告结尾那句“很遗憾,由于时间限制只完成了 ID 级处理控制冒险,实际上还可以在 EXE 级完成”,其实点出了另一条路:判定放 EXE 级,组合逻辑短、频率高,但受影响的指令更多,冲刷和暂停的代价更大。
这就是流水线设计里反复出现的权衡:判定越早,浪费越少,但单级逻辑越重;判定越晚,逻辑越轻,但流水线要吞下更多错误指令。没有绝对优解,只有和工艺、频率目标、面积预算匹配的解。教学实验选 ID 级,是因为它把“提前判定 + 精确冲刷”这条主线暴露得最干净,代码量也可控。
验证这类设计,我习惯抓三个观察点:转移指令进 ID 级那一拍pcsource是否当拍正确、if_inst是否当拍被替换成32'h0、以及被冲刷指令之后的 PC 是否落在目标地址上。这三点对上,机制就是通的;对不上,就顺着前推优先级和组合路径往回查。从那以后我每次改流水线控制逻辑,都强制先把这三拍的波形对齐再看结果,不然很容易被无效拍的假象带偏。希望帮到你。
本文还有配套的精品资源,点击获取