流水线MIPS CPU
写在前面
本篇是对CO-P5~7的总结。由于通关P7后便进入期末周,故本篇是在考完试放假后写的,有一些具体的细节可能已经记不太清了,就权当总结贴吧。全部代码已在GitHub开源:点此进入
P5:流水线CPU的改造
什么是流水线CPU
流水线CPU,是指将CPU划分成不同的部分,每一个周期指令向前进入下一部分的架构。尽管每一条指令的执行周期从原来的一个周期变成了多个周期,但是在同一时刻可以有多条指令同时执行,大大提高了CPI和吞吐量。在我们的五级流水线CPU中,我们将CPU划分为F(Fetch)、D(Decode)、E(Execute)、M(Memory)、W(Writeback)五个级次,分别负责取指、解码、计算、读写内存、写回寄存器。
流水线寄存器
实现流水线的核心方法即为流水线寄存器。流水线寄存器,即在每两级之间加入一个寄存器堆,这样既可以将后面的级次需要用到的数据传下去,也实现了再每一个时钟周期只前进一个级次的效果。其实现也十分简单,只需要将需要流水的数据在时钟上升沿复制给下一级输出,在复位时将输出清空。以D/E流水线寄存器为例:
module DE(
input clk,
input reset,
input DE_en,
input DE_reset,
input [31:0] D_Instr,
input [31:0] D_PC,
input [31:0] D_PCplus8,
input [31:0] D_RD1,
input [31:0] D_RD2,
input [4:0] D_A3,
input [31:0] D_imm32,
output reg [31:0] E_Instr,
output reg [31:0] E_PC,
output reg [31:0] E_PCplus8,
output reg [31:0] E_RD1,
output reg [31:0] E_RD2,
output reg [4:0] E_A3,
output reg [31:0] E_imm32
);
always @(posedge clk) begin
if(reset | DE_reset) begin
E_Instr <= 32'b0;
E_PC <= 32'b0;
E_PCplus8 <= 32'b0;
E_RD1 <= 32'b0;
E_RD2 <= 32'b0;
E_A3 <= 32'b0;
E_imm32 <= 32'b0;
end
else begin
if(DE_en) begin
E_Instr <= D_Instr;
E_PC <= D_PC;
E_PCplus8 <= D_PCplus8;
E_RD1 <= D_RD1;
E_RD2 <= D_RD2;
E_A3 <= D_A3;
E_imm32 <= D_imm32;
end
end
end
endmodule冒险
可以想见,我们在同一时刻最多有五条指令进入流水线,但是我们后一条指令可能会用到前一条指令的结果,甚至后一条指令是否执行都是未定的。这便是冒险。冒险有三种情况:数据冒险,结构冒险与控制冒险。
- 数据冒险:后一条指令需要用到前一条指令的计算结果,但是这一结果还未存入寄存器。
- 结构冒险:当指令和数据存储器共享同一个内存时,如果取指和读写内存同时发生,便会产生冲突。
- 控制冒险:涉及到分支跳转指令时,下一条指令进入时尚且无法判断这条指令是否会被执行。
冒险的解决
- 数据冒险:使用转发和阻塞来解决。转发,即在产生可能会被后面的指令用到的数据后,通过导线向前面的级次传递,新的指令进入后判断是否使用(即使用多路选择器)。当然,有些时候下一条指令需要用到的数据,上一条指令还没有计算完成,这个时候我们就需要阻塞(即将中间的所有流水线寄存器清空,让中间的指令变成nop空泡,等到可以进行转发或者已经写回后再让后面的指令流动)。判断是否可以转发、是否使用转发的值的方法,即AT&T法,实际上就是对每一种指令何时可以转发、何时需要用到某个寄存器进行打表,然后再进行判断。具体而言:
令:级次为:
D-0 E-1 M-2 W-3,一条指令需要用到 rs/rt 寄存器的级次为 $t_{rs}$ 和 $t_{rt}$,产生写入寄存器值的级次为 $t$。
前一条指令到 D 的距离为 $t_1$,后一条指令到 D 的距离为 $t_2$。记:
$$t_{\text{rsuse}} = t_{rs} - t_2$$
$$t_{\text{rtuse}} = t_{rt} - t_2$$
$$t_{\text{new}} = t - t_1$$
则转发的触发条件为:
$$t_{\text{rsuse}} \ge t_{\text{new}}$$
$$t_{\text{rtuse}} \ge t_{\text{new}}$$
另外,若 $t_{\text{use}}$ 或 $t_{\text{new}} < 0$,则记为 0。
- 结构冒险:我们一直使用的结构是哈佛结构(即IM与DM分离),故不存在此冒险
- 控制冒险:解决控制冒险的方法有很多,如分支预测、延迟槽等。在mips中,我们使用延迟槽来解决。所谓延迟槽,即跳转指令的下一条指令一定被执行。这样,我们便无需关心跳转到哪里的问题。同时,为了保证第三条指令是正确的,我们将分支判断提前到D级(即我们的zero模块),这样我们就能及时判断第三条指令的PC值。当然,在实际编写mips程序时,我们往往并不想要在跳转指令后执行任何指令,而是直接跳转,这时我们可以加入
nop指令——这就是这个空泡指令的作用。
P6:新指令与新模块
P6相较于P5,并没有很大的迈步,核心的工作即为添加乘除法模块、增加许多简单的新指令的支持、存储器外置。
乘除法模块
为了实现简单,课程组不要求真正实现乘除法电路,而是可以直接用verilog的乘除运算符。同时,除了HI LO两个寄存器外,由于我们需要模拟实际乘除法的阻塞,我们需要一个计数器来存储当前指令运行的周期数。代码如下:
module MDU(
input clk,
input reset,
input [31:0] in1,
input [31:0] in2,
input [5:0] type,
input start,
output [31:0] out,
output busy
);
reg [31:0] HI;
reg [31:0] LO;
reg [3:0] cnt;
parameter MULT = 6'b010101,
MULTU = 6'b010110,
DIV = 6'b010111,
DIVU = 6'b011000,
MFHI = 6'b011001,
MFLO = 6'b011010,
MTHI = 6'b011011,
MTLO = 6'b011100;
always @(posedge clk) begin
if (reset) begin
HI <= 32'h00000000;
LO <= 32'h00000000;
cnt <= 4'h0;
end
else begin
if (type == MULT) begin
{HI, LO} <= $signed(in1) * $signed(in2);
end
else if (type == MULTU) begin
{HI, LO} <= in1 * in2;
end
else if (type == DIV) begin
if (in2 != 32'h00000000) begin
HI <= $signed(in1) % $signed(in2);
LO <= $signed(in1) / $signed(in2);
end
end
else if (type == DIVU) begin
if (in2 != 32'h00000000) begin
HI <= in1 % in2;
LO <= in1 / in2;
end
end
else if (type == MTHI) begin
HI <= in1;
end
else if (type == MTLO) begin
LO <= in1;
end
if (start == 1'b1) begin
if (type == MULT | type == MULTU) begin
cnt <= 4'd5;
end
else if (type == DIV | type == DIVU) begin
cnt <= 4'd10;
end
end
if (cnt != 4'b0) begin
cnt <= cnt - 4'b0001;
end
end
end
assign out = (type == MFHI) ? HI : (type == MFLO) ? LO : 32'b0;
assign busy = (cnt != 4'b0) ? 1'b1 : 1'b0;
endmodule新指令添加
这里绝大多数指令都是走流程即可,唯一值得一提的是内存存取中的lb lh sb sh这四个指令。由于这四个指令不再是整字读取,所以我们采取的方案是将每个字划分成四个字节,每一个字节具有单独的WE,我们最后再对读写的数据进行拼接。
存储模块外置
实际的CPU中,GRF、DM等是不封装在CPU中的。为了模拟与外部存储设备的交互,课程组在testbench中提供了存储器,而我们在CPU中只需要将原来存储器模块删除,并将输入输出与CPU顶层的输入输出相连。由于DM模块我们刚刚进行了改动,所以这里展示出用于读写内存的模块代码:
module DMLoad(
input [31:0] addr,
input [31:0] data,
input [5:0] type,
output [31:0] fixed_data
);
parameter LW = 6'b000110,
LH = 6'b010001,
LB = 6'b010010;
wire [15:0] half_word;
wire [7:0] byte;
assign half_word = (type == LH && addr[1] == 1'b0) ? data[15:0] :
(type == LH && addr[1] == 1'b1) ? data[31:16] :
16'b0;
assign byte = (type == LB && addr[1:0] == 2'b00) ? data[7:0] :
(type == LB && addr[1:0] == 2'b01) ? data[15:8] :
(type == LB && addr[1:0] == 2'b10) ? data[23:16] :
(type == LB && addr[1:0] == 2'b11) ? data[31:24] :
8'b0;
assign fixed_data = (type == LW) ? data :
(type == LH) ? {{16{half_word[15]}}, half_word} :
(type == LB) ? {{24{byte[7]}}, byte} :
data;
endmodulemodule DMSave(
input [31:0] addr,
input [31:0] data,
input [5:0] type,
output [3:0] WE,
output [31:0] fixed_data
);
parameter SW = 6'b000111,
SH = 6'b010011,
SB = 6'b010100;
assign WE = (type == SH && addr[1] == 1'b0) ? 4'b0011 :
(type == SH && addr[1] == 1'b1) ? 4'b1100 :
(type == SB && addr[1:0] == 2'b00) ? 4'b0001 :
(type == SB && addr[1:0] == 2'b01) ? 4'b0010 :
(type == SB && addr[1:0] == 2'b10) ? 4'b0100 :
(type == SB && addr[1:0] == 2'b11) ? 4'b1000 :
(type == SW) ? 4'b1111 : 4'b0000;
assign fixed_data = (type == SH && addr[1] == 1'b0) ? {16'b0, data[15:0]} :
(type == SH && addr[1] == 1'b1) ? {data[15:0], 16'b0} :
(type == SB && addr[1:0] == 2'b00) ? {24'b0, data[7:0]} :
(type == SB && addr[1:0] == 2'b01) ? {16'b0, data[7:0], 8'b0} :
(type == SB && addr[1:0] == 2'b10) ? {8'b0, data[7:0], 16'b0} :
(type == SB && addr[1:0] == 2'b11) ? {data[7:0], 24'b0} :
(type == SW) ? data : data;
endmoduleP7:中断异常的处理
终于来到了最终关!在这里,我们需要做的,是增加中断异常的处理,并将我们的mips.v变成CPU.v,而真正的顶层模块mips.v是封装了CPU、Timer、系统桥三个部分的完整CPU!(Timer模块由课程组给出) 同样地,这里依然不准备展示完整细节,而是用简单的语言介绍一下大体思路。
中断异常
不要被陷入内核、返回现场等等概念绕蒙了!我们实际要做的十分简单,就是在发生异常时,CP0模块向各个模块发送中断信号,并记录此时的地址,然后跳转到PC值为0x4180的地方执行那里的指令,直到遇到eret指令,返回刚刚记录的位置。所以,中断异常的处理,我认为,可以理解为一种特殊的跳转指令。
外部设备模拟
这里我们要做的,就是为我们的五级流水线CPU添加一个宏观PC,让他在外部看起来是一个单周期CPU,然后实现一个系统桥,负责CPU、Timer、外部输入输出之间的沟通。最后,将CPU、Timer0、Timer1、Bridge和外部输入输出的数据线在mips.v中进行连接即可。
设计文稿
下面附上P7部分的设计文稿,方便各位理解。
设计文稿
最后
十分感谢伟大的Kamonto学长的《一本书教你通关计组实验》!
- 文章链接:一本书教你通关计组实验(上)一本书教你通关计组实验(下)
- 作者:Kamonto
- 版权声明:本引述内容遵循 CC BY-NC-SA 4.0 许可协议。