简介:这份电子科技大学2021年计算机体系结构实验四报告,面向正在学习流水线CPU设计、需要攻克控制冒险难题的高校学生与Verilog初学者。报告围绕JUMP无条件跳转与BNE、BEQ条件跳转两类指令展开,讲解如何在ID级提前比较寄存器值生成id_z信号、用nop部件将IF级预取指令替换为空指令,并配合指令序列仿真验证方案有效性,同时给出流水线结构图中新增功能部件与控制信号的标注说明。资源包共1个PDF文件,约1.34MB,完整收录实验原理、实验步骤、关键代码片段与电路结构图,便于对照复现。该报告已有841人学习,适合作为流水线CPU实验的参考范例,帮助读者理解控制冒险与数据冒险叠加时的处理思路,掌握Xilinx ISE平台下的Verilog实现方法。
1. 控制冒险:为什么一条跳转指令能让流水线“翻车”
流水线 CPU 最反直觉的一点是:指令还没执行完,后面几条就已经被取进来了。顺序执行时这没问题,可一旦遇到beq、bne、jump这类转移指令,前面预取的指令可能根本不该执行。控制冒险要解决的就是这个矛盾——转移结果还没算出来,流水线已经“抢跑”了。这份实验报告的价值在于,它没有停在教科书式的“暂停流水线”上,而是在 ID 级提前算出转移条件和目标地址,用一条空指令把抢跑的那条指令顶掉,把控制冒险的代价从多个周期压到最多一个周期。搞懂这套机制,你才能真正理解流水线里“预测”和“废弃”这两个动作是怎么落到电路上的。
2. 控制冒险的根源:转移指令到底让流水线错在哪
2.1 转移指令的延迟周期从哪来
控制冒险也叫转移相关问题,本质是程序计数器 PC 的更新时机和流水线取指节奏对不上。在顺序执行模型里,PC 每取一条指令就加一个指令字长,流水线各级按部就班往前推。但转移指令会改写 PC,而改写 PC 所需的两个信息——转移是否发生、转移目标地址——都不是在取指阶段就能拿到的。
实验报告里给了一个很关键的量化关系:如果转移指令从取指到执行完毕需要 n 个周期,那么有 n-1 条后续指令会受影响。这个 n-1 就是控制冒险的“罚款”。在经典的五级流水线里,如果转移条件在 EX 级由 ALU 算出来,那就要等三个周期,罚款是两条指令;如果提前到 ID 级算,罚款就降到一条指令。
这份实验的 CPU 把转移条件的判断和转移地址的偏移计算都放在了 ID 级,所以转移指令只需要两个时钟周期就能确定下地址。代价是 IF 级已经在这两个周期里取进了一条新指令,这条指令在转移发生时就是错的,必须被废弃。理解这个“两个周期、一条罚款”的账,是后面所有电路改动的出发点。
2.2 单周期 CPU 和流水线 CPU 的判断时机差异
单周期 CPU 里,条件转移的判断依赖 ALU 计算后的 Z 标志,控制部件拿到 Z 之后再产生rsrtequ信号,一切都在一个周期内完成,不存在“抢跑”问题。流水线 CPU 把一条指令拆到不同阶段,Z 标志要到 EX 级甚至更晚才稳定,而 IF 级不能干等着,它必须继续取指。
常见的实现思路有两种。第一种是暂停:Z 标志没准备好,就冻结 PC 和 IF/ID 寄存器一个周期,等 Z 出来再决定。这种办法简单,但每次条件转移都多花一个周期。第二种是提前判断:在 ID 级加一个比较器,直接比较寄存器堆读出的两个操作数,不等 ALU 的 Z 标志。实验报告采用的正是第二种,用id_z替代rsrtequ,把判断时机从 EX 级前移到 ID 级。
提示:提前判断不是没有代价的。它需要额外的比较器硬件,而且比较的是寄存器堆原始输出,必须配合数据前推才能拿到最新值,否则比较的就是过期数据。
2.3 条件转移和无条件转移的差异处理
条件转移beq、bne需要比较两个寄存器的内容,判断是否相等或不等;无条件转移jump则直接改写 PC,不需要比较。两者在 ID 级都会算出目标地址,区别在于是否要看id_z。
实验报告里Control_Unit的输出pcsource承担了这个区分:当 ID 级指令是转移指令且条件成立(或无条件转移)时,pcsource输出01或10,表示下地址来自转移目标;否则输出00,表示顺序执行。这个两比特信号同时驱动两个动作——更新 PC 和废弃 IF 级指令。把“判断”和“废弃”绑在同一个信号上,是这套设计最紧凑的地方。
3. ID 级提前判断:比较器、id_z 和 pcsource 怎么串起来
3.1 比较器的位置和 id_z 的两种写法
实验在 ID 级增加了一个比较器,输入是寄存器堆读出的两个值id_a和id_b,输出id_z。报告给了两种等价写法:
// 写法一:直接比较 assign id_z = (id_a == id_b) ? 1'b1 : 1'b0; // 写法二:按位异或后归约或 assign id_z = ~|(id_a ^ id_b);第二种写法在硬件上更自然:先对两个操作数逐位异或,只要有任何一位不同,异或结果就不为零,归约或|得到 1,取反后id_z为 0;全部相同则异或为零,id_z为 1。这种“异或加归约”的比较结构在综合工具里通常映射成一条比较链,延迟比减法器低,适合放在 ID 级这种对时序敏感的位置。
id_z的语义是“两个寄存器值相等”,它直接替代了原来从 ALU 拿的 Z 标志。对于beq,条件成立就是id_z == 1;对于bne,条件成立是id_z == 0。控制单元根据指令类型和id_z共同决定pcsource。
3.2 Control_Unit 里 pcsource 的产生逻辑
报告里Control_Unit的改动集中在转移指令的译码分支。用一段结构化的 Verilog 描述这个逻辑:
// 根据指令类型和 id_z 产生 pcsource // pcsource: 00 顺序执行, 01 条件转移成立, 10 无条件转移 always @(*) begin case (op) OP_BEQ: pcsource = (id_z == 1'b1) ? 2'b01 : 2'b00; OP_BNE: pcsource = (id_z == 1'b0) ? 2'b01 : 2'b00; OP_JUMP: pcsource = 2'b10; default: pcsource = 2'b00; endcase end这段逻辑的关键在于:pcsource不再等待 EX 级的 ALU 结果,而是在 ID 级指令译码的同一个周期内就产生。beq看id_z为 1 才跳,bne看id_z为 0 才跳,jump无条件给10。pcsource为01或10时,PC 的下一个值来自 ID 级算出的转移目标地址;为00时,PC 正常加四。
注意:
pcsource的位宽和编码必须和 PC 选择逻辑严格对齐。报告里用01表示条件转移成立、10表示无条件转移,这两个值在后续的 nop 部件和 PC 多路选择器里会被反复判断,编码一旦写错,表现就是跳转地址对但顺序指令被误废弃,或者反过来。
3.3 转移地址在 ID 级的计算路径
转移目标地址的计算也在 ID 级完成。条件转移的地址通常是当前 PC 加上指令中的立即数偏移,无条件转移则可能直接取指令中的目标字段。报告提到bpc和jpc两个目标地址信号,分别对应条件转移和无条件转移。
这条路径的时序压力不小:ID 级要同时完成寄存器读出、比较、立即数符号扩展、地址加法,然后送到 PC 多路选择器。常见的实现思路是把地址加法器独立出来,不和 ALU 复用,避免 ID 级和 EX 级争抢同一个加法资源。报告没有展开加法器的具体结构,但从pcsource在 ID 级就影响 IF 级这一点看,地址计算必须和比较器并行完成,不能串行等待。
4. 废弃抢跑指令:nop 部件如何把 IF 级指令换成空指令
4.1 if_inst 和 if_inst_org 的分工
这是整套设计里最巧妙的一处。报告新增了if_inst_org信号,作为 IF 级真正从指令存储器取出的原始指令;而送入流水线寄存器 IR 的if_inst则经过一个选择:
// 当 ID 级检测到转移时,用空指令替换 IF 级原始指令 assign if_inst = (pcsource == 2'b01 || pcsource == 2'b10) ? 32'h0 : if_inst_org;pcsource为01或10意味着 ID 级当前指令是“会跳”的转移指令,此时 IF 级那条按顺序取进来的指令就是错的,用32'h0顶掉。32'h0在全零指令编码下通常被译码成nop,它流过流水线时不写寄存器、不写存储器、不改 PC,等于一个无害的占位符。
这个设计把“废弃”做成了“替换”而不是“清空流水线寄存器”。清空需要额外的使能控制和多路选择,而替换只需要在 IF 级输出端加一个二选一,改动面小,时序也干净。
4.2 为什么不需要封锁 WMEM 和 WREG
报告里有一句容易被忽略但很关键的话:转移指令只可能改变 PC,不改变其它状态,因此不需要封锁WMEM、WREG信号。原因是转移指令本身的译码结果里,写存储器使能和写寄存器使能本来就是 0。被废弃的那条 IF 级指令虽然被换成了nop,但即便它原本是一条会写寄存器的指令,替换成nop之后写使能也为 0。
换句话说,这套方案的安全性建立在“nop 的译码结果全为无害”这个前提上。如果 nop 的编码定义不干净,比如某位控制信号默认为 1,那废弃动作就会引入副作用。这也是为什么报告强调用32'h0而不是随便一个无效编码。
4.3 空指令流过流水线时的时序
从仿真波形看,转移指令在 ID 级被识别的那一拍,pcsource变化,同一拍 IF 级输出被替换成nop。下一拍,nop进入 ID 级,而转移目标地址的指令进入 IF 级。再下一拍,nop进入 EX 级,目标指令进入 ID 级。整个过程只多了一个nop在流水线里占位,没有冻结 PC,也没有清空任何流水线寄存器。
这种“插入气泡”的方式和暂停有本质区别:暂停是让 PC 和 IF/ID 寄存器保持不动,气泡是让流水线继续流动,只是流进来的是无害指令。前者需要额外的使能逻辑,后者只需要一个选择器。报告选择后者,和它“在 ID 级提前判断”的整体思路是一致的——尽量用组合逻辑换时序,而不是用控制状态机。
5. 避坑与排查:控制冒险实现里最容易翻车的几处
5.1 现象:条件转移该跳却没跳,波形上 pcsource 一直是 00
原因通常是id_z的比较对象不对。ID 级比较的是寄存器堆读出的id_a和id_b,但如果前一条指令的结果还没写回,这两个值就是旧的。报告里明确提到,条件转移指令的比较寄存器与 ALU 或 LOAD 指令的目的寄存器相同时,存在数据冒险,必须依赖实验三建立的前推通路。
解决方式是确认前推优先级:EX 级 ALU 结果、MEM 级 ALU 结果、MEM 级 LOAD 结果,哪一级最新就用哪一级。报告仿真里bne取的r5分别来自mem_mo、exe_alu_result、mem_alu_result三条不同前推线,说明前推选择逻辑必须覆盖到 ID 级比较器的输入,而不是只覆盖 ALU 输入。
5.2 现象:跳转发生了,但跳转目标指令前面多执行了一条不该执行的指令
原因是if_inst的替换条件和pcsource的更新不在同一拍生效。如果pcsource晚一拍才影响 IF 级,那被废弃的指令已经写进 IR 了。报告的做法是让id_z在影响pcsource的同时影响IF_STAGE,两者在同一个组合逻辑路径上。
排查时看波形:转移指令进入 ID 级的那一拍,pcsource应该立刻变化,同一拍if_inst应该变成32'h0。如果if_inst晚一拍变,说明替换逻辑多打了一拍寄存器,需要把选择器挪到 IF 级输出端。
5.3 现象:无条件跳转后 PC 正确,但流水线里残留了一条会写寄存器的指令
这是jump的pcsource编码和 nop 替换条件没对齐。jump给的是10,如果替换条件只写了01,那jump后面的顺序指令就不会被顶掉。报告里替换条件是pcsource == 2'b01 || pcsource == 2'b10,两个值都要覆盖。
另一个容易漏的点是jump的地址计算。报告里jpc来自指令中的目标字段,如果符号扩展或拼接写错,PC 会跳到一个错误地址,波形上表现为目标指令取错,但pcsource看起来是对的。
5.4 现象:仿真通过,上板后偶发跑飞
组合逻辑路径太长是常见原因。ID 级要同时做寄存器读出、比较、地址加法、pcsource译码,再送到 IF 级做指令替换,这条路径如果超过一个时钟周期,就会在高速时钟下出错。报告用的是仿真验证,没有展开时序约束,但实际实现时这条路径需要重点关注。
解决思路是把地址加法和比较器并行化,不要让pcsource等待地址计算完成;如果仍然紧张,可以把pcsource中不依赖id_z的部分(比如jump的10)提前译码,减少关键路径上的逻辑级数。
6. 设计取舍:为什么把判断放在 ID 级而不是 EX 级
这套方案的核心取舍是“用硬件换周期”。把比较器搬到 ID 级,多了一个比较器和一条前推到 ID 级的通路,换来的是转移罚款从两条指令降到一条。报告在改进建议里也提到,其实还可以在 EX 级完成控制冒险处理,但时间限制只做了 ID 级。这句话背后是一个真实的工程权衡:ID 级方案罚款小,但组合逻辑路径长、前推网络复杂;EX 级方案路径短,但要多废弃一条指令,罚款更大。
从验证角度看,报告设计的指令序列很有代表性。它把条件转移和数据冒险交织在一起:bne比较的r5来自 MEM 级前推,beq比较的r4来自 EX 级前推,最后一条bne比较的r5来自 MEM 级 ALU 前推。三类前推路径各覆盖一次,等于用一条指令序列把“控制冒险 + 数据冒险”的交叉场景全测了。这种测试思路比单独测跳转更有说服力,因为真实程序里转移指令的操作数往往就是前一条指令的结果。
如果让我给这套设计提一个延伸方向,我会关注转移预测。ID 级提前判断本质上还是“算出来再决定”,罚款虽然降到一条,但每次转移仍然有一个气泡。静态预测(比如预测不跳)可以在 IF 级就按顺序取指,猜错了再废弃,猜对了零罚款。把预测和 ID 级判断结合,是流水线控制冒险从“消除”走向“隐藏”的下一步。
我自己的习惯是,每次改流水线控制通路,先把pcsource的每一种取值和 IF 级替换条件列成真值表,逐行对一遍波形,再跑指令序列。控制冒险的坑大多不在逻辑本身,而在“哪一拍生效”这个时序细节上。希望帮到你。
本文还有配套的精品资源,点击获取