简介:这份PDF是电子科技大学2021年计算机体系结构课程的实验四报告,面向正在学习流水线CPU设计、需要攻克控制冒险难题的高校学生与Verilog初学者。报告围绕JUMP无条件跳转及BNE、BEQ条件跳转三类指令展开,讲解如何在ID级提前计算转移地址与id_z信号、通过nop部件将IF级预取指令替换为空指令,并给出转移指令与数据冒险叠加时的处理思路及电路结构图修改说明。资源包共1个PDF文件,约1.34MB,内容含实验原理、目的、器材、步骤与仿真验证,配有代码片段和结构图,便于对照复现。已有838人学习下载,适合作为流水线CPU实验的参考范本与排错对照材料。
1. 控制冒险:为什么一条跳转指令能让流水线“翻车”
流水线 CPU 最反直觉的一点是:指令还没执行完,后面几条就已经被取进来了。顺序执行时这没问题,可一旦遇到beq、bne、jump这类转移指令,程序流的走向在取指阶段根本不知道,IF 级却已经按“顺序”把下一条指令搬进了流水线。等 ID 级算出“要跳”,那条被预取的指令已经站在错误的位置上——这就是控制冒险,也叫转移相关。它解决的不是“跳不跳”的语义问题,而是“跳转决定来得太晚,错误指令已经混进来”的时序问题。搞懂它,你才真正理解流水线里“预测—验证—冲刷”这套机制的底层逻辑,也才能看懂为什么有的设计宁可多花一个比较器,也不愿多停一个周期。
2. 转移条件为什么必须在 ID 级提前算出来
2.1 单周期 CPU 的 Z 标志在流水线里为什么“来不及”
单周期 CPU 里,条件转移的判断很直接:ALU 算完,Z 标志(结果是否为零)自然就有了,控制部件拿rsrtequ一判断,PC 该跳就跳。但流水线把“算”和“判”拆到了不同阶段——ALU 在 EXE 级,而转移指令的后续指令在 IF 级就已经进来了。如果老老实实等 Z 标志从 EXE 级传回来,流水线就得停一个周期,等 Z 准备好再决定 PC 走向。
这个“停一个周期”的代价,就是控制冒险最朴素的解法:暂停。暂停的逻辑是——转移指令只可能改 PC,不会改寄存器堆、不会写内存,所以对转移指令来说WMEM、WREG本来就是 0,不需要封锁它们,只需要把 PC 和 IF 级按住一个周期。但暂停终归是性能损失,每遇到一条条件转移就停一拍,流水线的意义就被削掉一块。
2.2 把比较电路搬到 ID 级:id_z 的表达式与硬件代价
这份实验报告里采用的思路,是把比较提前到 ID 级。ID 级本来就要从寄存器堆读出id_a和id_b两个操作数,那干脆在这里加一个比较器,直接判断两者是否相等,产生id_z信号,等价于提前拿到了rsrtequ。报告里给出的表达式是:
// ID 级提前比较两个源寄存器,产生转移条件 // 写法一:直接相等比较 assign id_z = (id_ra == id_rb) ? 1'b1 : 1'b0; // 写法二:按位异或后归约或,全零即相等 assign id_z = ~|(id_a ^ id_b);两种写法逻辑等价,第二种用“异或再归约或”的方式,在硬件上往往映射成更紧凑的门级结构——异或为 0 表示每一位都相同,归约或为 0 就说明整体相等。参数上,id_a、id_b是 ID 级寄存器堆读出的两个源操作数,id_z就是提前产生的转移条件信号,直接喂给控制部件去影响pcsource。
这么做的收益很明确:不用再等 EXE 级的 Z 标志,省掉了一个停顿周期。代价是“额外的硬件实现”——多一个比较器,以及把id_z接进控制通路的连线。这是一个典型的空间换时间取舍:用一点组合逻辑面积,换掉每条条件转移指令的一个气泡。我一般会这么理解:这不是“更聪明”,而是“把判断挪到了数据本来就到齐的地方”,ID 级恰好是操作数第一次同时可用的阶段。
2.3 转移地址也在 ID 级算完:2 个周期意味着什么
光有条件还不够,转移目标地址也得提前。报告里明确写了:转移条件在 ID 级计算得到,转移地址也在 ID 级完成偏移计算。于是整条转移指令从取指到“决定去向”只需要 2 个时钟周期,受影响的后续指令只有 1 条。
这个“2 个周期、1 条受影响指令”是整个方案的核心数字。它意味着:当转移指令走到 ID 级、pcsource被确定的那一刻,IF 级里正好躺着一条按顺序预取的指令,而这条指令就是唯一需要被处理掉的“错误指令”。受影响指令数从“n-1 条”压缩到 1 条,冲刷的代价就被压到了最小。如果转移地址要拖到 EXE 级才算,受影响的就是 2 条,冲刷逻辑和性能损失都会翻倍——这也是报告在改进建议里提到“其实还可以在 EXE 级完成”时,语气里带着遗憾的原因:EXE 级方案能 work,但要多冲一条。
3. 冲刷与空指令:pcsource 如何让 IF 级“换人”
3.1 nop 部件的本质:用 pcsource 做二选一
确定了“要跳”,接下来的问题是怎么把 IF 级那条错误指令处理掉。报告里的做法很干脆:不搞复杂的冲刷队列,而是在 IF 级输出处放一个二选一的“nop 部件”,由pcsource直接控制。
// 顶层模块中:根据 pcsource 决定 IF 级送出的指令 // pcsource == 01 或 10 表示 ID 级是转移指令(条件成立 / 无条件) // 此时强制送出空指令 32'h0,否则送正常预取指令 assign if_inst = (pcsource == 2'b01 || pcsource == 2'b10) ? 32'h0 : if_inst_org;if_inst_org是 IF 级原本取回的指令,if_inst才是真正送进流水线寄存器的指令。pcsource的编码在这里承担了“指令性质”的语义:01和10都代表 ID 级当前是一条会改变 PC 的转移指令,00则代表普通指令。一旦命中转移,if_inst被替换成32'h0,也就是一条空指令(nop),它流进流水线后不写寄存器、不写内存,等于什么都没干。
这个设计的巧妙之处在于“废弃”和“暂停”被统一成了同一个动作:把错误指令换成空指令。空指令会正常流过 IF、ID、EXE 各级,只是不产生副作用,所以不需要额外的清空流水线寄存器逻辑,控制复杂度大幅下降。代价是这条空指令仍然占了一个周期的时间片——但相比“停一拍等 Z 标志”,它至少让流水线继续流动,而不是硬停。
3.2 条件转移与无条件转移共用一条冲刷通路
报告里把beq、bne归为条件转移,jump归为无条件转移,但两者在“废弃 IF 级指令”这件事上走的是同一条路。区别只在pcsource的取值来源:条件转移看id_z是否成立,无条件转移则直接认定要跳。
// Control_Unit 内部:由 id_z 和指令类型共同决定 pcsource // rsrtequ 即顶层传入的 id_z(ID 级提前比较的结果) // 条件转移:id_z 成立 -> pcsource = 01(跳 bpc) // 无条件转移:直接 -> pcsource = 10(跳 jpc) // 普通指令:pcsource = 00这里rsrtequ就是id_z的别名,控制部件拿它和指令译码结果一起判断。条件成立或遇到jump,pcsource就离开00,IF 级的 nop 部件立刻把if_inst换成空指令,同时 PC 被改成bpc(条件转移目标)或jpc(无条件转移目标)。一条通路同时覆盖两类转移,硬件上省了一套冲刷逻辑,这是设计上的合并取舍——代价是pcsource的编码必须严格区分“跳 bpc”“跳 jpc”“不跳”三种状态,任何译码错误都会直接表现为取错指令。
提示:
pcsource同时驱动 PC 选择和 IF 级 nop 部件,这两处必须保持同一时钟沿的语义一致,否则会出现“PC 跳了但空指令没插进去”或“插了空指令但 PC 没跳”的错位。
4. 控制冒险撞上数据冒险:前推结果能不能直接喂给比较器
4.1 三类前推来源:EXE、MEM、LOAD 的数据怎么进 ID 级比较
控制冒险很少单独出现。报告里的测试序列刻意把条件转移和数据冒险叠在一起:beq、bne要比较的寄存器,可能正好是前面 ALU 指令或 LOAD 指令的目的寄存器。这时候 ID 级比较器拿到的id_a、id_b可能还是旧值,直接比较就会得出错误的转移结论。
好在实验三已经把数据前推做到了 ID 级,所以这里的比较可以直接用前推后的结果。报告把条件转移遇到的数据冒险分成三大类,并在仿真里逐一验证:
| 前推来源 | 典型场景 | 对 ID 级比较的影响 |
|---|---|---|
| EXE 级 ALU 结果 | 转移指令紧跟 ALU 指令 | 用exe_alu_result前推到 ID 级参与比较 |
| MEM 级 ALU 结果 | 中间隔了一条指令 | 用mem_alu_result前推到 ID 级 |
| MEM 级 LOAD 结果 | 转移指令依赖 LOAD 的目的寄存器 | 用mem_mo(访存结果)前推到 ID 级 |
这张表背后的机制是:ID 级比较器不是孤立工作的,它的输入必须挂到前推网络上,否则“提前比较”反而会提前比错。报告在结果分析里特别标注了每一处bne、beq所取的操作数分别来自哪条前推线——mem_mo、exe_alu_result、mem_alu_result——这其实是在证明:控制冒险的解决必须建立在数据冒险已解决的前提上,两者是耦合的。
4.2 一个容易忽略的细节:LOAD 型冒险仍需暂停
前推能解决大多数情况,但 LOAD 指令是个例外。LOAD 的数据要到 MEM 级末尾才真正从内存取回,如果紧随其后的指令在 EXE 级就要用这个数据,前推也救不了,只能暂停一个周期。报告里的测试序列第 3、4 条load r5,r1(2)和store r5,r2(3)就是这种情况:STORE 进 ID 级时与 EXE 级的 LOAD 存在 LOAD 数据冒险,仿真里明确出现了一个红色框标注的暂停周期,框内 ALU 输出无效。
这个细节说明:控制冒险的“提前比较”并没有绕开数据冒险的物理限制。ID 级比较器再早,也得等操作数真正可用;LOAD 的数据没回来,比较就只能等。所以完整的机制是“前推优先、前推不了就暂停”,控制冒险的冲刷逻辑叠加在这套数据冒险处理之上,而不是替代它。
5. 避坑与排查:仿真波形里那些让人怀疑人生的瞬间
5.1 现象:转移发生了,但 IF 级还是进了顺序指令
原因通常是pcsource只接到了 PC 选择端,没接到 IF 级的 nop 部件,或者两处用了不同的信号名。解决方法是确认if_inst的赋值表达式里判断的就是驱动 PC 的那个pcsource,且位宽、编码完全一致,别一个用2'b01另一个用1'b1。
5.2 现象:条件转移该跳却没跳,比较结果总是“不等”
多半是 ID 级比较器没接前推网络,拿到的是寄存器堆的旧值。排查时沿着id_a、id_b往上游追,看它们是否经过前推多路选择器;再对照仿真波形,确认比较时刻前推选择信号是否已经有效。报告里三类前推来源要逐一核对,漏掉mem_mo这条线是最常见的翻车点。
5.3 现象:插入了空指令,但流水线后续行为错乱
空指令32'h0必须被译码成“什么都不做”。如果控制部件把全零指令误译成某种有效操作,空指令就会偷偷写寄存器或写内存。排查方法是单独仿真一条32'h0,看WREG、WMEM是否都为 0;不为 0 就说明译码的默认分支没兜住。
5.4 现象:LOAD 后紧跟转移,结果时对时错
这是 LOAD 数据冒险没暂停导致的。LOAD 的数据在 MEM 级才回来,ID 级比较器此时拿到的是无效值。解决方法是保留 LOAD 冒险的暂停逻辑,让流水线停一拍再比较,别指望前推能覆盖所有情况。
5.5 现象:无条件转移后 PC 对了,但下一条执行的是错误指令
jump的pcsource编码如果和条件转移混用同一个值,nop 部件可能判断正确但 PC 选错目标(bpc与jpc搞反)。排查时把pcsource的三种取值和 PC 多路选择器的输入一一对照,确认01走bpc、10走jpc,别让编码在两层逻辑里各说各话。
6. 设计取舍:为什么“ID 级提前判断”不是唯一答案
把转移判断提前到 ID 级,本质是在“判断时机”和“硬件复杂度”之间做了一次明确的交换。越早判断,受影响的后续指令越少,冲刷代价越小;但越早判断,需要的额外硬件(比较器、前推到 ID 级的连线、nop 部件)越多,而且判断所依赖的数据可能还没准备好,反而要引入暂停。这份实验报告选择 ID 级,是因为它把受影响指令压到了 1 条,同时复用了实验三已有的 ID 级前推网络,边际成本最低。
但这不是唯一解。报告在改进建议里提到“其实还可以在 EXE 级完成”,这句话背后是一个真实的取舍:EXE 级方案不需要在 ID 级加比较器,判断逻辑可以复用 ALU 的零标志,硬件更省;代价是转移决定晚一个周期,受影响指令变成 2 条,冲刷逻辑要处理两条错误指令,或者多停一拍。换句话说,ID 级方案是“多花硬件换性能”,EXE 级方案是“省硬件但多冲一条”。没有绝对优劣,取决于你的流水线深度、转移指令占比,以及你愿意为每条转移付出多少气泡。
再往远看一步,真实处理器很少只靠“算完再冲”,而是引入预测:预测转移发生或不发生,先按预测取指,等真正算出来再验证,预测失败才冲刷。报告里也提到了“预测转移发生或预测转移不发生的方法,当发现预测失败或计算得转移目标地址时需要废弃前面的错误指令”。预测把“等待判断”变成了“赌一把再验证”,在转移规律性强时收益巨大,但预测失败时的冲刷代价和预测器的面积开销又是新的取舍。ID 级提前判断更像是这个谱系里“不预测、但尽量早算”的中间态——它不赌,只是把该算的算早一点。
验证这套机制是否真的 work,最直接的办法就是像报告那样构造一条混合了数据冒险和控制冒险的指令序列,在波形里逐拍核对:转移指令进 ID 级的那一拍,pcsource是否及时翻转、IF 级是否插入空指令、前推来源是否正确、LOAD 冒险处是否真的停了一拍。我自己的习惯是,每次改完流水线控制通路,都强制把“转移 + 前推 + LOAD 暂停”这三种情况叠在同一条测试序列里跑一遍波形,因为单独测控制冒险往往一切正常,一叠加数据冒险就原形毕露。从那以后我每次动 PC 选择或冲刷逻辑,都强制走一遍这条混合序列,省掉了很多事后回看波形的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取