HITwh NSCSCC Team | 哈尔滨工业大学(威海)全国大学生计算机系统能力培养大赛小组
[TOC]
流水线暂停位于《自己动手写CPU》第七章,里面已经实现了很多东西,所以这篇文档的代码和之前的不兼容,于是只截取重点部分给大家讲解。
不过因为大家学习本文档的初衷是理解五级流水如何实现,所以代码并不重要,重要的是理解思维。
前面说过,视觉上来说,流水线暂停其实是在检测到流水线相关问题时,在流水线中插入一些暂停周期,等待前面的指令完全执行完毕后再恢复流水线。
对于相邻两条ORI产生数据相关时:
- 前一条指令在WB阶段的最后一个时钟周期把数据写入RegFile
- 后一条指令在ID阶段请求一个正确的值
所以理论上,我们要插入三个暂停周期,即将第二条指令的ID阶段推迟到第一条指令的WB阶段后,但是在此之前,我们已经在RegFile里面实现了数据前推:
// 当读地址与写地址相同,且写使能,且端口1读使能,则要把写入的数据直接读出来
// 数据前推的实现,后面会提及
end else if((raddr1 == waddr) && (we == `WriteEnable)
&& (re1 == `ReadEnable)) begin
rdata1 <= wdata;
// 否则读取相应寄存器单元
end
所以我们只需要插入两个暂停周期,将第二条指令的ID阶段推迟到第一条指令的WB阶段,其实这是一个“带数据前推的流水线暂停”。
一种直观的实现方式是:
- 保持PC的值不变
- 保持流水线各个阶段的寄存器不变
我们采用一个改进的暂停方式:
- 保持PC的值不变
- 保持流水线第n阶段以及之前的各个阶段的寄存器不变
- 第n阶段后面的指令继续运行
比如EX阶段请求暂停,则保持IF/ID/EX阶段的流水线寄存器的值不变,而处于MEM/WB阶段的指令继续执行。
为此我们要新增一个模块,这个模块不属于流水线的任何一个阶段。
CTRL模块的输入来自于ID、EX模块的请求暂停信号 stallreq。CTRL模块会对暂停请求信号进行判断,然后输出流水线暂停信号stall。看图可知stall输出到PC、IF/ID、ID/EX、EX/MEM、MEM/WB等模块,从而控制PC值以及各个阶段的流水线寄存器。
CTRL模块的接口如图所示:
其中stall信号是一个宽度为6的信号,用每位的数值(0或者1)来给出是否暂停的信号。
代码如下:
`include "defines.v"
module ctrl(
input wire rst,
input wire stallreq_from_id, // 来自ID阶段的暂停请求
input wire stallreq_from_ex, // 来自EX阶段的暂停请求
output reg[5:0] stall
);
// 1表示暂停,0表示继续执行
always @ (*) begin
if(rst == `RstEnable) begin
stall <= 6'b000000;
end else if(stallreq_from_ex == `Stop) begin
stall <= 6'b001111;
end else if(stallreq_from_id == `Stop) begin
stall <= 6'b000111;
end else begin
stall <= 6'b000000;
end //if
end //always
endmodule
PC模块的代码如下:
always @ (posedge clk) begin
if (ce == `ChipDisable) begin
pc <= 32'h00000000;
// 如果IF阶段不需要暂停,则PC值继续增加
end else if(stall[0] == `NoStop) begin
pc <= pc + 4'h4;
end
end
修改IF/ID模块:
always @ (posedge clk) begin
if (rst == `RstEnable) begin
id_pc <= `ZeroWord;
id_inst <= `ZeroWord;
// 当IF阶段暂停且ID阶段继续的时候,用空指令作为下一个周期进入译码阶段的指令
end else if(stall[1] == `Stop && stall[2] == `NoStop) begin
id_pc <= `ZeroWord;
id_inst <= `ZeroWord;
// 否则继续执行
end else if(stall[1] == `NoStop) begin
id_pc <= if_pc;
id_inst <= if_inst;
end
end
ID模块可以请求流水线暂停,采用流水线暂停解决ORI指令数据冲突就需要在这个地方发出流水线暂停请求,但我们建议采用数据前推的方式解决流水线数据冲突问题,我们给出具体思路:
if(与前面指令数据相关) begin
stallreq_from_id <= `Stop;
end
那如何判断与前面的指令数据相关呢?我们给出两种思路:
- 和数据前推类似,从EX和MEM段查看是否有读写相同寄存器的情况发生
- 在ID模块增加寄存器,存储前两条指令的写寄存器地址,用于判断是否发生冲突
对于第一种,其实已经和数据前推类似,这里感觉“多此一举”
对于第二种,我们可以说强烈不推荐,因为这增加了ID段的“负担”,而且不利于整个CPU的“鲁棒性”
注意:
对于Load指令的数据相关问题,我们只能用流水线暂停完成(大家可以思考为什么?)
修改ID/EX模块:
always @ (posedge clk) begin
if (rst == `RstEnable) begin
ex_aluop <= `EXE_NOP_OP;
ex_alusel <= `EXE_RES_NOP;
ex_reg1 <= `ZeroWord;
ex_reg2 <= `ZeroWord;
ex_wd <= `NOPRegAddr;
ex_wreg <= `WriteDisable;
// 如果ID阶段暂停而且EX阶段继续执行,则给EX阶段传递空操作
end else if(stall[2] == `Stop && stall[3] == `NoStop) begin
ex_aluop <= `EXE_NOP_OP;
ex_alusel <= `EXE_RES_NOP;
ex_reg1 <= `ZeroWord;
ex_reg2 <= `ZeroWord;
ex_wd <= `NOPRegAddr;
ex_wreg <= `WriteDisable;
// 否则继续执行
end else if(stall[2] == `NoStop) begin
ex_aluop <= id_aluop;
ex_alusel <= id_alusel;
ex_reg1 <= id_reg1;
ex_reg2 <= id_reg2;
ex_wd <= id_wd;
ex_wreg <= id_wreg;
end
end
EX模块可以请求暂停,但是由于目前ORI指令不会在EX阶段请求暂停(感兴趣的同学自行学习),所以忽略EX模块的修改。
修改EX/MEM模块:
always @ (posedge clk) begin
if(rst == `RstEnable) begin
mem_wd <= `NOPRegAddr;
mem_wreg <= `WriteDisable;
mem_wdata <= `ZeroWord;
// 如果EX阶段暂停而且MEM阶段继续执行,则给MEM阶段传递空操作
end else if(stall[3] == `Stop && stall[4] == `NoStop) begin
mem_wd <= `NOPRegAddr;
mem_wreg <= `WriteDisable;
mem_wdata <= `ZeroWord;
// 否则继续执行
end else if(stall[3] == `NoStop) begin
mem_wd <= ex_wd;
mem_wreg <= ex_wreg;
mem_wdata <= ex_wdata;
end //if
end //always
endmodule
此阶段只需要修改MEM/WB模块:
always @ (posedge clk) begin
if(rst == `RstEnable) begin
wb_wd <= `NOPRegAddr;
wb_wreg <= `WriteDisable;
wb_wdata <= `ZeroWord;
// 如果MEM阶段暂停而且WB阶段继续执行,则给WB阶段传递空操作
end else if(stall[4] == `Stop && stall[5] == `NoStop) begin
wb_wd <= `NOPRegAddr;
wb_wreg <= `WriteDisable;
wb_wdata <= `ZeroWord;
// 否则继续执行
end else if(stall[4] == `NoStop) begin
wb_wd <= mem_wd;
wb_wreg <= mem_wreg;
wb_wdata <= mem_wdata;
end //if
end //always
至此,流水线暂停已经实现!