CPU流水线冲突详解:转发、停顿与分支预测的Verilog实现
2026/9/19 11:27:14 网站建设 项目流程

简介:《解决CPU流水线冲突技术的设计与实现》是针对CPU流水线设计的计算机体系结构专业参考文献,适合处理器设计学习者、FPGA开发者及高校相关专业师生阅读,内容聚焦流水线冲突的成因与工程化解决手段。资源为单份PDF原文,大小仅179KB,收录于2008年《现代电子技术》期刊,由北航学者撰写,系统覆盖五级MIPS CPU结构、数据冲突/控制冲突/结构冲突分析,并重点讲解数据旁路与动态分支预测的实现细节。目前已有158人学习浏览,可用作课程设计、毕业设计或处理器优化实践的核心参考资料。文中不仅给出了各流水段的功能划分与模块关系,还通过具体指令序列仿真对比,展示了采用上述技术后执行指令所需时钟周期数的显著降低,能帮助读者直观理解旁路转发逻辑和分支预测对流水线吞吐率的影响,为后续自行设计高性能CPU或复现相关实验提供明确指导。

1. 流水线冲突到底争在什么硬件:三类冲突与最小五级流水线模型

做课设的时候,最难受的不是单周期每条指令都能跑对,而是把同一套设计改成流水线后,寄存器里的值开始出现“幽灵写入”:某条读数指令明明排在后面,却拿到了前一条指令还没写回的结果。这不是仿真器的Bug,而是CPU流水线冲突在裸流水线上的必然表现。冲突的本质是时间重叠带来的数据依赖和资源竞争:IF段的指令还没译码,EX段的结果还没落地,后面的指令已经在等这个结果。

MIPS五级流水线是最小可讲透的模型:取指IF、译码ID、执行EX、访存MEM、写回WB。三条指令同时处于不同阶段时,结构冲突、数据冲突、控制冲突会交替出现。结构冲突是硬件资源只装得下一份;数据冲突是结果还没写回寄存器就被后一条指令读走;控制冲突是分支要等好几拍才知道目标地址。下面按“数据→控制”两条线,把转发、停顿、分支冲刷的判定逻辑和Verilog实现完整走一遍,适合正在做CPU课设、考研复试或FPGA项目的读者。

2. 数据冲突的硬件解法:转发通路设计与RAW检测

2.1 为什么整数五级流水线只盯RAW,WAR/WAW可以先不管

数据冲突按读写顺序分成三类:写后读(RAW,真依赖)、读后写(WAR,反依赖)、写后写(WAW,输出依赖)。不少资料把三类放在一起讲,但落到MIPS五级流水线的整数实现里,能真正造成错误的只有RAW。原因是顺序流水线中,读操作在ID段发生,写操作在WB段发生,指令按发射顺序执行,WAR和WAW只出现在乱序执行、寄存器重命名或双写端口场景中。

顺序五级流水线上,RAW已经足够棘手。比如add $t1, $t0, $s0在ID段读$t0时,前一条lw $t0, 0($zero)的结果要等该指令的MEM段结束、WB段才能写入寄存器堆,时间差最多两个时钟周期。不加处理的话,CPU只能靠插入空泡等待,流水线效率立刻掉一半。这就是转发(forwarding)的用武之地:把尚未写回的数据直接从EX/MEM或MEM/WB流水线寄存器接回ALU输入,而不等它真正落进寄存器堆。

选择转发而不是加大寄存器堆写回带宽,主要看硬件成本。转发只需要在ALU两个输入前各加一个MUX,控制信号由专门的转发单元产生,不涉及寄存器堆结构改动。相比做多端口寄存器堆或专用旁路缓存,转发的最小面积解法几乎没有额外时序负担,代价是设计时要清楚每个信号在哪一级流水线寄存器里,以及它何时有效。

forward_a/forward_b数据来源适用场景
2'b00寄存器堆读输出无依赖,或依赖源太早且已写回
2'b01MEM/WB流水线寄存器上一指令访存结果,或更早但未写回
2'b10EX/MEM流水线寄存器上一指令ALU刚算出的结果

2.2 转发通路的三个候选来源与Forwarding Unit判定逻辑

转发是把“寄存器堆写回”提前到“结果刚产生”的时机。在五级流水线中,结果可能出现在两个位置:EX/MEM流水线寄存器(上一指令ALU刚算完)和MEM/WB流水线寄存器(上一指令访存刚完成)。再加上寄存器堆读出的原始值,一共三个来源。ALU输入端的MUX用两位选择信号区分:00用原始值,10用EX/MEM回送,01用MEM/WB回送。

module forwarding_unit ( input wire [4:0] id_ex_rs, input wire [4:0] id_ex_rt, input wire [4:0] ex_mem_rd, input wire [4:0] mem_wb_rd, input wire ex_mem_regwrite, input wire mem_wb_regwrite, output reg [1:0] forward_a, output reg [1:0] forward_b ); always @(*) begin forward_a = 2'b00; forward_b = 2'b00; // ALU 的 A 输入:优先取 EX/MEM 段的结果 if (ex_mem_regwrite && (ex_mem_rd != 5'd0) && (ex_mem_rd == id_ex_rs)) forward_a = 2'b10; else if (mem_wb_regwrite && (mem_wb_rd != 5'd0) && (mem_wb_rd == id_ex_rs)) forward_a = 2'b01; // ALU 的 B 输入:同样按就近优先 if (ex_mem_regwrite && (ex_mem_rd != 5'd0) && (ex_mem_rd == id_ex_rt)) forward_b = 2'b10; else if (mem_wb_regwrite && (mem_wb_rd != 5'd0) && (mem_wb_rd == id_ex_rt)) forward_b = 2'b01; end endmodule

判定逻辑是纯组合的。ex_mem_rd是上一指令的目的寄存器号,id_ex_rs是本指令的源寄存器号,两个条件同时成立才产生转发。先比较EX/MEM段,再比较MEM/WB段,这是就近优先策略。照抄这个模块时最容易漏掉ex_mem_rd != 5'd0条件:0号寄存器恒为0,若某指令写了$zero再把它定向往后送,数值上没差别,但会让转发MUX长期处于忙碌状态,甚至覆盖更关键的转发来源。

MUX接线处也要与转发信号对齐,标准接法写在EX段:

wire [31:0] alu_src_a = (forward_a == 2'b10) ? ex_mem_alu_result : (forward_a == 2'b01) ? mem_wb_result : id_ex_reg_data_a;

id_ex_reg_data_a来自ID/EX流水线寄存器里保存的原始读数据。注意mem_wb_result取自MEM/WB输出端,而不是寄存器堆的写数据端口。理解这个差别的关键点是:MEM/WB输出端在时钟上升沿到来前保持旧值,转发单元在一个周期内可以稳定地从输出端取数,不受写回动作影响。

2.3 就近优先策略与0号寄存器的边界条件

两条不同周期的转发来源同时命中时,就近优先保证距离当前指令最近的指令结果生效。如果前一条指令恰好是把结果写进$zero的伪指令,就近优先会跳过它,继续找更早的MEM/WB来源,这个行为由else if分支实现。还要处理更隐蔽的情况:被测指令本身是不写寄存器的类型(store、branch),regwrite信号必须来自流水线寄存器里保存的控制信号,而不是当前阶段临时生成的信号。设计时把该信号放进EX/MEM与MEM/WB寄存器组,转发单元才能拿到正确的历史状态。

3. 转发兜不住的时候:load-use停顿与Hazard Detection Unit

3.1 从lw到add的典型窗口:为什么这一拍必然停滞

转发解决了“ALU结果早一拍可用”的问题,但load指令的数据要等MEM阶段访存结束才有。若下一条指令在ID段就要读这个数据,即使转发把MEM/WB的结果送回ALU,也必须等load从内存读出后的那个周期。五级流水线里这形成固定的load-use窗口:lw在EX段、后一条指令在ID段时,两者相差两个周期,必须插入一拍停滞才能让数据可用。

识别这个窗口不靠猜,靠的是ID/EX段里保存的MemRead信号。只要ID/EX段的指令是load,并且它的目标寄存器号等于IF/ID段指令的任一源寄存器号,流水线就应当暂停。这个判定和转发判定必须分开:转发解决的是“结果已在数据通路上”,停顿解决的是“结果还没离开访存单元”。

3.2 hazard_detection的Verilog实现与信号约定

module hazard_detection ( input wire id_ex_memread, input wire [4:0] id_ex_rt, input wire [4:0] if_id_rs, input wire [4:0] if_id_rt, output wire stall_pc, output wire stall_ifid, output wire clear_ctrl ); wire stall = id_ex_memread && ((id_ex_rt == if_id_rs) || (id_ex_rt == if_id_rt)); assign stall_pc = stall; // 冻结 PC,不取新指令 assign stall_ifid = stall; // 冻结 IF/ID 寄存器内容 assign clear_ctrl = stall; // 把 ID/EX 控制信号清 0,形成空泡 endmodule

三个输出对应硬件上的三个动作。stall_pc让PC停止变化,使取指段原地等待;stall_ifid保住在IF/ID寄存器中排队的那条指令;clear_ctrl把ID/EX段里已经译码的控制信号全部清零,等效强制插入一条nop。需要注意,clear_ctrl清的是控制路径而不是数据路径,寄存器堆写使能必须一并被清掉,否则停顿那一拍会把错误数据写进寄存器。

信号取1时的动作取0时的动作
stall_pcPC寄存器保持不变PC照常加4或载入分支目标
stall_ifidIF/ID寄存器保持当前指令IF/ID寄存器载入新取到的指令
clear_ctrlID/EX控制信号全部清0控制信号正常传递

3.3 停顿与转发如何衔接成同一套硬件动作

停顿一拍后,转发单元并非什么都不做,而是会在随后的周期自动接管。以lw $t0, 0($zero); add $t1, $t0, $s0为例:停顿发生时lw处于ID/EX段;下一拍lw进MEM,add仍在ID等待;再下一拍lw进WB,add才进入EX,此时ALU的A输入可以直接从MEM/WB输出端取到lw的结果,forward_a会变成2'b01。因此设计上不需要额外的“闭锁”逻辑,转发单元天然覆盖这个场景,前提是转发判定使用的mem_wb_regwrite来自MEM/WB流水线寄存器。

这一拍停顿与转发衔接的设计思路,直接影响吞吐率:数据依赖的惩罚从无条件的两拍降为一拍,且仅当依赖源是load时才发生。若某个寄存器中保存的MemRead信号未随流水线寄存器更新,而是直接连到当前阶段控制线,那么load-use窗口会偏离一个周期,造成停顿位置错误或者漏停。

4. 控制冲突的分支处理:提前判决、延迟槽与动态预测

4.1 分支判决在ID段完成,penalty从3降为1

控制冲突的产生来自指令的顺序错位。带分支的流水线中,在未确认跳转之前,IF段会继续取后续指令。若分支最终不跳转,这些取指工作白费;若跳转,这些指令必须全部作废。经典MIPS五级流水线把分支判定放在EX段,产生2拍停顿;一个常见的优化是把比较器与目标地址加法器搬到ID段,使分支结果在译码阶段就可判定,penalty降为1拍。代价是ID段需要额外一组比较逻辑和加法器,但换来的是控制冲突最直接的缓解。

4.2 静态方案:flush与延迟槽的取舍

在分支目标确定前,最简单的做法是每条分支指令都让流水线冲刷后续指令,然后转向目标地址。这个方案硬件开销最低,但每条分支的惩罚固定为1~2拍。MIPS使用的延迟槽则是让紧随分支指令之后的那条指令无论如何都执行,相当于把作废指令变成有效工作。延迟槽对编译器调度有要求:必须找到一条在分支两侧都无副作用的指令填充到槽中。对硬件而言,延迟槽实现的改动极小,只需保证分支跳转期间不冲刷紧随的一条指令。

静态策略硬件改动量每条分支惩罚适用场景
flush全部后续指令极小,PC与IF/ID使能3拍课设演示、教学版
分支判定移到ID段中,加比较器和加法器1拍单发射有序流水线
延迟槽中,需要编译器配合0(槽内指令有效)MIPS、RISC-V兼容的编译器优化

4.3 动态预测:2-bit饱和计数器的判定与BHT索引

静态方案对规律性循环很吃亏。动态预测做法是用上次行为预测下次行为,典型实现是2-bit饱和计数器:四种状态按“强不跳→弱不跳→弱跳→强跳”循环迁移,预测方向取状态最高位。相比1-bit方案,它在连续2次翻转分支时不至于立刻被带偏,指令吞吐的抖动更小。

module branch_predictor #( parameter BHT_ENTRIES = 256 )( input wire clk, input wire reset, input wire [31:0] pc, input wire branch_resolved, input wire branch_taken, output wire predict_taken ); reg [1:0] bht [0:BHT_ENTRIES-1]; wire [7:0] idx = pc[9:2]; // 取 PC 对应位做索引 wire [1:0] state = bht[idx]; assign predict_taken = state[1]; // 状态最高位即预测方向 always @(posedge clk or posedge reset) begin if (reset) bht[idx] <= 2'b01; else if (branch_resolved) begin case (state) 2'b00: bht[idx] <= branch_taken ? 2'b01 : 2'b00; 2'b01: bht[idx] <= branch_taken ? 2'b10 : 2'b00; 2'b10: bht[idx] <= branch_taken ? 2'b11 : 2'b01; 2'b11: bht[idx] <= branch_taken ? 2'b11 : 2'b10; endcase end end endmodule

BHT_ENTRIES是分支历史表的容量,哈希索引取PC的[9:2]位,能映射256个连续地址的分支。branch_resolved在ID段确认分支真实方向后拉高一个周期,branch_taken表示实际是否跳转。状态迁移本质是“方向正确则顺向走一格,方向错误则逆向走一格”。代码把reset置为弱不跳转,是为了让冷启动预测不盲目清空,同时收敛速度比强状态更快。

4.4 预测失败的Flush作用域要照顾到哪一级

预测失败时,需要同时清理IF/ID和ID/EX两级流水线寄存器。predict_takenbranch_taken异或得到的mispredict信号,接到两级寄存器的同步复位端;同时为了让目标PC正确进入取指段,还要用分支目标覆盖PC。这里最常见的错误是只清了IF/ID而漏掉ID/EX,导致ID/EX里的半条错误指令进入EX段执行一次,污染后续状态。

5. 从单周期CPU改成流水线的关键改动与波形验证

5.1 寄存器堆的读写时序先约定好

将单周期CPU改成流水线的第一步是确定寄存器堆时序。常见做法让写入发生在时钟下降沿、读取发生在上升沿。这样保证同一周期内能先读后写,流水对读写的约束也更宽松。若沿用上升沿写、下降沿读,则读操作会拿到上一周期的旧值,可能绕过转发直接造成错误数据。

5.2 最小改造清单

改造项影响模块必须注意的点
插入四组流水线寄存器pipeline_regs.v使能端接stall/flush信号
ALU输入MUXforwarding_unit.v转发源取EX/MEM或MEM/WB
PC与IF/ID冻结hazard_detection.v只冻结不放行,不产生新指令
ID/EX控制信号清零pipeline_regs.v清成空泡,不能清成写使能
分支提前到ID段branch_unit.vflush信号覆盖IF/ID与ID/EX

5.3 用三条指令同时触发RAW、load-use与控制冲突

main: lw $t0, 0($zero) # load-use:下一拍 add 就要用 t0 add $t1, $t0, $s0 # RAW:t0 在 EX 段才产生 beq $t1, $zero, target # 控制冲突:目标未知 nop # 延迟槽 target: sw $t1, 4($zero)

这段代码覆盖三条主线:lwadd之间的load-use停顿、addbeq间的RAW转发、分支预测失败后的flush。跑仿真时,把PC、forward_astall_pcflush全部引出:

iverilog -o sim top_tb.v pipeline.v forwarding_unit.v hazard_detection.v branch_predictor.v vvp sim gtkwave dump.vcd

5.4 波形里看什么:stall、forward、flush的对齐

验证目标是让信号按预定周期对齐:lw进EX段时检测到load-use,stall_pc拉高一拍;add进入EX时forward_a变为2'b01;分支判决时若预测不跳转而实际跳转,flush拉高且IF/ID、ID/EX同步置空。观察点是停顿时PC保持不变、IF/ID数据不被覆盖;随后一拍add的ALU输入正确来自MEM/WB输出。若转发MUX信号有毛刺,优先检查ex_mem_rd != 5'd0判断与就近优先的else分支。逐条对比单周期模型与流水线模型的寄存器最终值,二者应完全一致。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询