在单周期CPU的基础上开发实现流水线CPU
说明:
1. 该PDF带有大纲功能,点击大纲中的对应标题,可以快速跳转。
2. 目录层级为:
一、一级标题
(二)二级标题
(3)三级标题
4.四级标题
e)五级标题
3. 波形分析见实验过程中对应实现的末尾。
把单周期的CPU升级为流水线的CPU,最重要的是使得各个阶段互相独立,互不影响。由于触发器仅仅在时钟上升沿的时候写入数据,在其他时刻数据均不发生变化,所以可以通过4级流水线寄存器把原来的数据通路分为5级
四级流水线寄存器的名字以上一级以及下一级的名称来命名:
首先列表分析各个寄存器所需要保存的数据:
流水线寄存器 |
保存数据 |
保存控制信号 |
if_id |
instruction, |
未译码,无 |
id_exe |
pc_add_4, |
op(操作码), |
exe_mem |
pc_add_4, |
mem_write, |
mem_wb |
pc_add_4, |
reg_write, |
然后使用verilog对四个流水线寄存器进行实现。
为了增强可读性,我单独定义了4个流水线寄存器,每一个流水线寄存器中的所有信号全部列出。
eg.对于if_id流水线寄存器如下:
module if_id( input clock, input reset, input write_enable, output reg [31:0] instruction, output reg [31:0] pc_add_4, input [31:0] nxt_instruction, input [31:0] nxt_pc_add_4 ); always @(posedge clock) begin if (!reset) begin instruction <= 32'b0; pc_add_4 <= 32'b0; end else if(write_enable) begin instruction <= nxt_instruction; pc_add_4 <= nxt_pc_add_4; end end endmodule |
其他寄存器采用类似的方法进行定义。
这一种定义的原因:
①可读性更好,可以通过对应名称确定流水线寄存器中这一项的作用,而不必去计算每一个功能对应的位。
②如果在后面需要增加新的控制信号,那么可以直接增加,旧有的内容不需要改变,便于进行增量式设计。
对于顶层模块,参考下图,对元器件进行连接。
在顶层模块定义线网,按照如下格式:
这样可以通过前面的标识符表示区分这一个线网究竟是哪一个阶段的,使得可读性更好。
对于if_id流水线寄存器的连线如下:
if_id IF_ID( //out .clock(clock), .reset(reset), .write_enable(IF_ID_write_enable),//表示是否对寄存器进行写 .instruction(ID_instruction), //表示把寄存器的instruction与ID级的相连 .pc_add_4(ID_pc_add_4), //in 这一个寄存器的下一个指令由IF给出 .nxt_instruction(IF_instruction),//该D触发器的D端的数据。 //当下一个时钟上升沿到来的时候,使用这些值来更新原有的值。 .nxt_pc_add_4(IF_pc_add_4) ); |
其余结构的连线详细见平台提交的代码。
对于多路选择器,由于在数据通路中使用的频次很高,所以使用更为简单的条件运算符进行实现:
.nxt_num_write( (ID_s_num_write == 2'b00) ? ID_instruction[20:16] : (ID_s_num_write == 2'b01) ? ID_instruction[15:11] : (ID_s_num_write == 2'b10) ? 31: 31 ), |
波形分析:
为了便于展现流水线CPU各个指令的传输,采用不同阶段的OP字段进行表示。
汇编代码:
addi $2, $zero, 1 addiu $3, $zero, 1 andi $4, $zero, 1 ori $5, $zero, 1 addi $6, $zero, 1 addiu $7, $zero, 1 |
得到的波形:
在波形中可以看到,一条指令随着时间的流逝,在流水级中不断向后方传递,每当到达一个新的周期,就会有新的指令被取入,这大大加快了指令的执行。
在流水线中,指令被分成五个阶段(IF,ID,EXE,MEM,WB),每个阶段都有独立的硬件执行,所以指令的不同阶段可以并行执行。
但是流水线中会遇到一些问题,即后续指令需要使用前一个指令的执行结果,而前一个指令还没有写回的时候,会产生数据冒险。
在这种情况下,如果不进行干预,后续指令无法获得前一个指令的正确结果,导致计算错误或结果不确定。
进行干预有多种选择,如果使用阻塞,那么执行的结果的正确性可以保证,但是会严重影响流水线的速度,所以要尽可能采用旁路机制,以实现对于数据冒险的处理,如果由于某些情况无法通过旁路处理,则还要进行阻塞。
在这里假设前面的指令的需要写入的寄存器单元的编号与后面的指令需要的编号相同:
首先,画出流水线的时空图,对于可能出现的数据冒险进行分析:
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
CC6 |
CC7 |
CC8 |
CC9 |
|
前面的指令 |
IF |
ID |
EX |
MEM |
WB |
|
|
|
|
|
后面的指令 |
一阶 |
|
IF |
ID |
EX |
MEM |
WB |
|
|
|
二阶 |
|
|
IF |
ID |
EX |
MEM |
WB |
|
|
|
三阶 |
|
|
|
IF |
ID |
EX |
MEM |
WB |
|
|
四阶 |
|
|
|
|
IF |
ID |
EX |
MEM |
WB |
在MIPS的指令集体系架构中,数据写回寄存器全部是在WB阶段发生的,现在对上图进行分析:
从上所述:前三阶的指令会发生数据冒险。第四阶以及之后的指令不会发生数据冒险。
进行分类讨论:
a)前面的指令为EXE型
如表:
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
CC6 |
|
前面的指令(A) |
IF |
ID |
EXE |
MEM |
WB |
|
|
后面的指令(B) |
一阶 |
|
IF |
ID |
EXE |
MEM |
WB |
在下面记前面的指令为A,后面的二阶指令为B
在表中可以看到,A在CC3阶段得到运算结果,在CC4可以从EXE/MEM流水线寄存器中读出A写入寄存器堆的数据。
B指令在CC4需要数据进行运算,所以可以采用MEM-EXE旁路策略。
b)前面的指令为MEM型
如表:
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
|
前面的指令(A) |
IF |
ID |
EXE |
MEM |
WB |
|
后面的指令(B) |
一阶 |
|
IF |
ID |
EXE |
MEM |
在表中可以看到,A在CC4阶段得到运算结果,在CC5可以从MEM/WB流水线寄存器中读出A写入寄存器堆的数据。
这一个时候,对于B指令进行细分。
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
CC6 |
CC7 |
|
前面的指令(A) |
IF |
ID |
EXE |
MEM |
WB |
|
|
|
后面指令(B) |
二阶 |
|
|
IF |
ID |
EXE |
MEM |
WB |
在下面记前面的指令为A,后面的二阶指令为B
为了简单起见,对于A指令,不论是EXE型还是MEM型,都可以从A的WB级把A需要更改的寄存器的值旁路到B的EXE阶段,使得在B执行的时候使用的值是A运算出来的最新的结果,即WB-EXE旁路。
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
CC6 |
CC7 |
CC8 |
|
前面的指令(A) |
IF |
ID |
EXE |
MEM |
WB |
|
|
|
|
后面的指令(B) |
三阶 |
|
|
IF |
ID |
EXE |
MEM |
WB |
|
在下面记前面的指令为A,后面的二阶指令为B
在图中可以看到,无论A是EXE型还是MEM型指令,在CC5的WB阶段可以得到A指令写入寄存器的值。
对于B,无法在CC6以及CC7阶段进行旁路,因为在CC6和CC7阶段,A的相关信息已经不在流水线中了,所以无法完成旁路。
但是在CC5阶段可以进行旁路。可以把A的MEM/WB流水线寄存器中的写入寄存器堆的数据旁路到B的ID级流水线寄存器,这样,后面的指令就可以使用A指令运算得到的值了,即使用WB-ID旁路策略。
根据上一个步骤,现在把解决数据冒险进行以下总体概括:
前面指令 |
EXE |
MEM |
||||
后面指令 |
一阶 |
二阶 |
三阶 |
一阶 |
二阶 |
三阶 |
EXE |
MEM-EXE |
WB-EXE |
WB-ID |
等待一个周期 |
WB-EXE |
WB-ID |
MEM |
WB-MEM |
为了分析方便,给指令起以下名字:
NO.1 |
A |
NO.2 |
B |
NO.3 |
C |
NO.4 |
D |
这四条指令顺序执行。
综上所述,冒险的处理有着优先级,即先考虑一阶冒险,然后考虑二阶冒险,最后考虑三阶冒险。
WB2EXE以及MEM2EXE是可以处理一阶(第一条指令为EXE型)以及二阶的冒险
根据之前的分析,
如图,s_forwardA3或者s_forwardA4选择0,表示MEM-EXE旁路;选择1表示WB旁路;选择2表示不进行旁路,直接使用ID/EXE流水线寄存器中的值。
根据“(4)解决数据冒险总体分析”中的分析,如果具有一阶的冒险,那么直接进行MEM-EXE旁路,如果具有与二阶的冒险,那么进行WB-MEM旁路。
判断是否冒险我认为有三个要素:
我在实际的代码中,通过if-else判断来决定优先级。
always @(*) begin //对于A3的值进行控制 if(MEM_reg_write == 1 && MEM_num_write != 0//判断我上文中提到的三个条件 && MEM_num_write == EXE_rs)//如果产生一阶的数据冒险 s_forwardA3 = 0; else if(WB_reg_write == 1 && WB_num_write != 0 && WB_num_write == EXE_rs) //如果产生二阶的数据冒险 s_forwardA3 = 1; else s_forwardA3 = 2;//不产生冒险 end always @(*) begin //对于B3的值进行控制 if(MEM_reg_write == 1 && MEM_num_write != 0//判断我上文中提到的三个条件 && MEM_num_write == EXE_rt) //如果产生一阶的数据冒险 s_forwardB3 = 0; else if(WB_reg_write == 1&& WB_num_write != 0 && WB_num_write == EXE_rt) //如果产生二阶的数据冒险 s_forwardB3 = 1; else s_forwardB3 = 2; //不产生冒险 end |
波形分析:
编写具有WB2EXE以及MEM2EXE冒险的汇编代码对CPU进行测试:
addi $1, $zero, 5 addi $1, $zero, 6 addi $2, $1, 0 #上面是测试MEM到EXE addi $1, $zero, 5 sll $zero, $zero, 0 addi $2, $1, 0 #此处测试WB到EXE |
波形如下
对于汇编指令第三行,其与第一行构成二阶冒险,与第二行构成一阶冒险,在这里应该优先使用一阶冒险,所以应该从MEM阶段进行旁路。对应波形图中s_forwardA3为0
对于汇编指令第六行,与第四行构成二阶数据冒险,波形图中s_forwardA3为1,从WB进行旁路
其余情况没有数据冒险,所以s_forwardA3 = 2’b10,表示不进行旁路,使用从ID阶段寄存器文件中得到的值。
这一个旁路是专门针对三阶冒险进行的。
如果检测到WB需要写入寄存器的编号与ID的寄存器编号一致且不为0,那么就进行这一个旁路。
优先级分析:
按照“(4)解决数据冒险总体分析”中的分析,这一种旁路应该是优先级最底的。在对于WB2ID直接实现就好,不需要特殊判断是不是有一阶或者二阶冒险。
在图中可以看到:如果当前指令有一级或者是二级冒险,那么在ID阶段得到的值在后面会被忽略(后面的MUX不会选择ID/EXE阶流水线寄存器中的值),即一二级冒险的优先级由于数据通路的设计,高于三级冒险。所以在此处发现冒险之后可以直接旁路,而不必关心有没有一二级冒险。
s_ forwardB3的控制模块:
always @(*) begin if(WB_reg_write == 1 && WB_num_write != 0 && WB_num_write == ID_rs) s_forwardA2 = 0; else s_forwardA2 = 1; end always @(*) begin if(WB_reg_write == 1 && WB_num_write != 0 && WB_num_write == ID_rt) s_forwardB2 = 0; else s_forwardB2 = 1; end |
波形分析:
编写汇编代码,其中具有三阶冒险。
addi $1, $zero, 5 sll $zero, $zero, 0 sll $zero, $zero, 0 addi $2, $1, 0 #与第一行的指令构成三阶冒险 |
波形如下:
可以看到,在ID级的指令为前三行的代码不会出现冒险,所以旁路到ID级的多路选择器的选择信号为2,对应gpr中读出来的数据。对于第四条指令,与第一条汇编指令发生三阶冒险,所以选择信号为1.以读取最新的值。
备注:MUX选择信号为1:WB-ID;选择信号为2:从gpr中读取。
在前面的讨论中,除了前面的指令为lw指令的情况下的一阶冒险没有考虑完之外,其他的冒险已经考虑完成。
对于前面的指令为lw并且为一阶冒险的情况,分为两种情况进行讨论。
根据下表:
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
|
前面的指令(A) |
IF |
ID |
EXE |
MEM |
WB |
|
后面的指令(B) |
一阶 |
|
IF |
ID |
EXE |
MEM |
必须阻塞一个周期,然后实现WB-EXE旁路。
在之前的讨论中,已经实现了WB到EXE的旁路,所以为了应对这一种情况,仅仅需要添加阻塞逻辑就可以了。
为了实现阻塞的效果,需要进行以下操作:
同时需要注意I型指令,I型指令的rt是I型指令写入的寄存器号,而不是I型指令执行所需要用到的信号,所以I型指令的rt并不参与冒险判断。
always @(*) begin if((//满足条件即阻塞 EXE_is_load_word && EXE_num_write != 0 && EXE_num_write == ID_rt //检查rt字段是否与前面的指令要写的寄存器相同 && ID_op != `instruction_op_ADDI//对于I型指令,不关心rt && ID_op != `instruction_op_ADDIU && ID_op != `instruction_op_ANDI && ID_op != `instruction_op_ORI && ID_op != `instruction_op_LUI && ID_op != `instruction_op_SW && ID_op != `instruction_op_LW ) || ( EXE_is_load_word && EXE_num_write != 0 && EXE_num_write == ID_rs //检查rs字段是否与前面要写的寄存器号相同 ) ) begin//阻塞的逻辑,保持PC以及IF_ID寄存器不变,把ID_EXE清零 IF_ID_write_enable = 0; PC_write_enable = 0; ID_EXE_flush = 1; end else begin IF_ID_write_enable = 1; PC_write_enable = 1; ID_EXE_flush = 0; end end |
波形分析:
首先编写以下的汇编代码
addi $1, $zero, 5 sw $1, 0($zero) lw $2, 0($zero) addi $3, $2, 0 #与第三条指令有数据冒险 |
波形如下:
从波形中可以看到,在第四条指令位于ID级的时候,发生了阻塞,导致PC在第五条指令处停滞了两个周期。
在波形图的倒数第3行,表示旁路信号。在阻塞了一个周期之后,正常旁路。倒数第2行可以看出,寄存器中的值仍然为初始值0,但是通过旁路,在a_latest中得到了正确的值(第三条汇编指令写入的5)
在这个时候可以通过WB-MEM进行实现,由数据通路图可以知道,如果有这样一种冒险,那么优先级是最高的。
具体实现如下:
always @(*) begin if( WB_op == `instruction_op_LW //判断前一条指令为lw && MEM_op == `instruction_op_SW //判断后一条指令为sw && WB_num_write != 0 && WB_num_write == MEM_rt ) s_forwardB4 = 1; else s_forwardB4 = 0; end |
波形分析:
首先编写如下的汇编代码:
addi $1, $zero, 5 sw $1, 0($zero) lw $2, 0($zero) sw $2, 4($zero) |
得到以下的波形结果:
在波形图中,可以看到,在最后一条sw位于mem的时候,旁路信号发生作用,实现WB-MEM的旁路。
检查存储器,发现:
地址为4($zero)的存储器被正确写入数字5。
控制冒险是指在计算机程序中由于分支指令(beq,j,jal,jr等)的执行可能导致的流水线停顿或指令执行顺序的错乱。
为了解决控制冒险,有以下的方法:
①使用阻塞。阻塞可以处理任何的冒险,后果就是使得程序执行的时间大大增加,所以应该尽量避免使用阻塞。
②把分支应用提前到ID级。通过增加多余的硬件,在ID级得到分支的地址,并在时钟的上升沿更改PC的值,这样只需要清除一条指令(阻塞一个周期)
③采用分支预测。当预测的准确度比较高,并且预测错误的代价比较小的时候,可以采用预测来事先执行,而不是等到需要执行的时候进行执行。预测主要有预测分支发生,预测分支不发生以及动态预测
④采用延时槽,在跳转指令之后放置一条与跳转不相关的指令,这样的话,无论分支是否发生,均不会形成阻塞。
在该实验中,采用在ID级应用分支的结果,采用分支总不发生的预测,同时在必须的时候使用阻塞。
如下图:
在图中可以看到,对于这一类的指令,主要需要处理两个方面:
①在ID级确定需要跳转的地址
②把跳转指令取到的下一条指令清空(对应图中的第二行)
要完成操作②,进行以下分析:
由于跳转指令的目标地址是在ID级确定的,因此在下一个时钟周期,已经进入IF级的指令将被无效化并阻塞执行。为了实现指令的清空,可以使用NOP指令代替已进入IF级的指令,将其机器码设置为0x0000_0000。为了实现设置机码的功能,可以给流水线寄存器增加一个flush信号,如果其有效,那么就同步复位。
在我的设计中,由于流水线寄存器有异步复位端口,所以我重用异步复位端口,其输入为(reset && (!flush) ),这样,就不必为流水线寄存器增加新的端口。
Flush的操作逻辑如下:
always @(*) begin//j,jr,jal指令阻塞,面对IF_ID if(ID_op == `instruction_op_J || ID_op == `instruction_op_JAL || (ID_op == `instruction_op_R_type && ID_funct == 6'b001000))begin IF_ID_flush = 1; end end |
之前的代码在置位的时候是把所有的控制信号设置为0,但是在这里需要把指令设置为全0,这一条指令代表sll,即逻辑左移运算。为了保证处理器得到正确的结果,必须把这一条指令进行实现。
查阅MIPS手册,得知这一条指令的格式如下:
这一条指令相比于之前的指令,增加了shamt字段。所以需要对流水线寄存器做出修改。详细设计步骤如下:
控制信号 |
s_npc |
s_data_write |
mem_write |
s_num_write |
s_b |
s_ext |
aluop |
reg_write |
取值 |
3 |
1 |
0 |
1 |
0 |
1 |
`alu_op_sll |
1 |
此时,控制信号设置完成。
计算地址的功能按照之前单周期的方式,进行连接即可。但是要注意,对于PC+4,这一个值来自于IF阶段,对于其他的地址来源于ID阶段。
波形分析:
在波形中使用j指令进行说明。
编写下面的汇编代码
addi $2, $zero, 1 //line 1 beq $zero, $zero, L1 //line 2 addi $3, $zero, 3 //line 3 L1: addi $4, $zero, 4 //line 4 |
在第二行指令位于ID级的时候,发生跳转。此时IF已经取了第三行的指令,所以应该进行清除。
对波形进行观察:
在波形中可以发现,IF_ID_flush按照预期置位为1,这表示清除掉j指令后面取的指令。
下图为寄存器的图,从图片中可以看到,第三条指令并未执行。
对于控制冒险(在EXE级使用分支结果),如果单纯使用阻塞,那么需要两个周期,显然比较浪费时间。为此,有以下两种优化方式:
当把指令提前到ID级进行执行的话,如果使用加法器进行比较是否相等,那么会严重拖慢ALU的速度,所以需要采用异或运算,如果得到全零,那么就代表寄存器文件中的两个值是相等的。
使用异或实现的细节:
assign ID_zero = ~(|(ID_a_latest^ID_b_latest)); |
Flush可以复用(1)中的flush模块,仅仅需要加入对于beq型指令的清空逻辑。如果分支跳转,那么把IF/ID流水线寄存器清空。否则正常执行(预测成功)
具体实现如下:
else if(ID_op == `instruction_op_BEQ)begin if(ID_zero == 1) IF_ID_flush = 1;//跳转,那么清空 else IF_ID_flush = 0;//预测成功,正常执行 end |
按照图示连接好数据通路即可。
波形分析:
由于在该CPU中使用分支不发生预测,所以如果分支不跳转,那么则执行beq指令之后相当于没有发生任何事情。
编写如下的汇编代码:
addi $1, $zero, 1 nop nop nop #用于处理数据冒险 beq $zero, $1, L1 addi $2, $zero, 2 L1: addi $3, $zero, 3 |
在该汇编代码中,分支不发生,预测正确,那么PC仍然按照原有的顺序执行。
波形图如下:
通过波形图可以看出,假如预测正确的话,那么就不采取行动,让原来的指令按照顺序执行即可。
由于分支结果在ID级就必须产生,所以在ID级也有可能产生数据冒险。对于ID级的数据冒险,分析如下:
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
CC6 |
CC7 |
CC8 |
CC9 |
|
前面的指令 |
IF |
ID |
EXE |
MEM |
WB |
|
|
|
|
|
后面的指令 |
一阶 |
|
IF |
ID |
EXE |
MEM |
WB |
|
|
|
二阶 |
|
|
IF |
ID |
EXE |
MEM |
WB |
|
|
|
三阶 |
|
|
|
IF |
ID |
EXE |
MEM |
WB |
|
|
四阶 |
|
|
|
|
IF |
ID |
EXE |
MEM |
WB |
TIME |
CC1 |
CC2 |
CC3 |
CC4 |
CC5 |
CC6 |
CC7 |
CC8 |
CC9 |
|
前面的指令(MEM) |
IF |
ID |
EXE |
MEM |
WB |
|
|
|
|
|
后面的指令 |
一阶 |
|
IF |
ID |
EXE |
MEM |
WB |
|
|
|
二阶 |
|
|
IF |
ID |
EXE |
MEM |
WB |
|
|
|
三阶 |
|
|
|
IF |
ID |
EXE |
MEM |
WB |
|
|
四阶 |
|
|
|
|
IF |
ID |
EXE |
MEM |
WB |
前面的指令 数据产生阶段 后面的指令 数据使用阶段 |
EXE |
MEM |
||||||
一阶 |
二阶 |
三阶 |
四阶 |
一阶 |
二阶 |
三阶 |
四阶 |
|
ID |
阻塞一个周期,MEM-ID旁路 |
MEM-ID |
WB-ID |
无冒险 |
阻塞两个周期,WB-ID旁路 |
阻塞一个周期,WB-ID旁路 |
WB-ID |
无冒险 |
(说明:(5)以及(6)先对相应的冒险进行分析,在(7)中做具体的实现)
前一条指令为EXE型指令,根据上表进行设计。
阻塞是在ID级进行阻塞,所以与数据冒险的阻塞情况相似,可以使用与MEM EXE型数据冒险使用同一根阻塞控制线。
其中阻塞的条件是:
ID级的指令为beq,而EXE级的指令①进行了写寄存器;②写寄存器目的地址不是0;③写寄存器目的地址与beq指令的rs,rt相同。
对于MEM-ID旁路,在数据冒险阶段并没有实现,所以在这里需要进行添加。
该旁路的条件为:
对于优先级问题,旁路到ID级的优先级为:
综上所述,先处理MEM-ID旁路,再处理WB-ID旁路。
旁路已经实现
旁路已经在数据冒险阶段实现,无需再次实现。
对于旁路机制,已经在数据冒险阶段实现了从WB到ID级的旁路,所以在这里仅仅需要考虑阻塞。
需要阻塞两个周期,阻塞均发生在ID阶段。对于第一个阻塞,与数据冒险中的lw-EXE型指令相同;对于第二个阻塞,判断条件为:
①MEM为lw且ID为beq
②MEM的写寄存器编号不为0
③MEM的写寄存器编号与ID级的rs或者rt相同
与一阶冒险的阻塞中的第二个阻塞相同,无需实现。
首先,增加旁路机制(MEM-ID旁路)
always @(*) begin if(MEM_reg_write == 1 && MEM_num_write != 0 //优先MEM-ID && MEM_num_write == ID_rs) s_forwardA2 = 0; else if(WB_reg_write == 1 && WB_num_write != 0 //然后考虑WB-ID && WB_num_write == ID_rs) s_forwardA2 = 1; else s_forwardA2 = 2; |
对于s_forwardB2同理。
然后按照数据通路示意图修改数据通路,完成旁路的工作。
然后再处理阻塞的情况,定义如下几个变量:
reg one_cycle_stall_judge1; reg one_cycle_stall_judge2; wire one_cycle_stall; reg second_cycle_stall; assign one_cycle_stall = one_cycle_stall_judge1 || one_cycle_stall_judge2; |
定义如下:(具体的分析见(5) )
if( (EXE_reg_write && EXE_num_write != 0 && EXE_num_write == ID_rs && ID_op == `instruction_op_BEQ ) || ( EXE_reg_write && EXE_num_write != 0 && EXE_num_write == ID_rt && ID_op == `instruction_op_BEQ ) ) one_cycle_stall_judge2 = 1; else one_cycle_stall_judge2 = 0; |
对于second_cycle_stall,具体实现如下(判断条件见(6)的一阶冒险的第二个阻塞)
if( MEM_op == `instruction_op_LW && ID_op == `instruction_op_BEQ && MEM_num_write != 0 && (MEM_num_write == ID_rs || MEM_num_write == ID_rt) ) second_cycle_stall = 1; else second_cycle_stall = 0; |
到此,完成了控制冒险以及前移分支判断所新增加的数据冒险的处理。
波形分析:
编写如下的汇编代码进行测试
addi $1, $zero, 7 addi $2, $zero, 8 beq $1, $2, L1 addi $3, $zero, 3 L1: addi $4, $zero, 4 |
对于测试指令的分析:Beq指令与第二行的指令构成了一阶冒险,需要进行阻塞。在阻塞完成之后,beq与第一行指令构成三阶冒险,应该采用WB-ID旁路。与第二行指令构成二阶冒险,应该采用MEM-ID旁路。
综合得到波形如下:
在波形中可以看出来,CPU在beq指令位于ID级的时候进行了阻塞。阻塞一个周期之后,对于rs,采用WB-ID旁路;对于rt,采用MEM-ID进行旁路。
通过旁路之后,a_latest与b_latest为正确的值。
提交之后,提示我的指令instruction出现问题,编写testbench检查:
增加了一个临时的输出pc_proceed
发现对于同样的assign赋值语句,输出的结果却不一样。
检查顶层模块,发现如下:
把IM的输出以及IF_ID的输出接到了一起,所以导致了结果不确定。
经过检查是指令译码的接线错误
在红色部分本来需要进行阻塞,但是我的代码并没有阻塞,使用仿真,编写以下代码:
.text lw $t5, 0($a2) addu $t7, $t5, $t6 add $t1, $a0, $v1 |
使用上述的汇编代码进行仿真:
发现接线位数不对(如图中的黄色部分),继续检查代码
把相关的位数进行更正。
继续仿真,还是发现有错误,
发现在test测试信号显示正常的情况下,nxt_is_load_word值错误。
再仔细检查,发现没有标明为二进制数,只需要把
ID_instruction[31:26] == 100011 ? 1 : 0 |
改为
ID_instruction[31:26] == 6'b100011 ? 1 : 0 |
更改之后发现可以正常阻塞
发现了仅有的一条错误:
由于在前面进行过阻塞,怀疑阻塞部分出现问题。
红色的框中应该为&&
更改问题四之后,出现最后错误
前述的控制信号等等均没有问题,偏偏在写存储器的时候出现了问题。
推测是发生了数据冒险,检查发现,我没有处理红框框起来的冒险。
但是在增加对印的控制模块之后,还是存在冒险。
推测冒险是MEM,编写以下汇编指令:
.text addi $t4, $zero, 5 sw $t4, 4($zero)#事先向1号存储单元存入5 add $t5, $zero, $zero addu $t7, $t5, $t6 add $t1, $a0, $v1 lw $2, 4($zero)#从1号存储单元读出 sw $2, 8($zero)#存入2号存储单元、 |
发现在我的代码中,MEM_op传递出现问题:
添加EXE_MEM流水线寄存器中的op以及nxt_op,发现信号位数不对(下图中最上面的信号),接线错误
在更改之后,op信号可以正常传递。
更正问题五之后,s_forwardB4(使用黄线标出)没有按照预期进行变化。
经过检查,未按照预期,多出一个阻塞
当指令不明确时,可能会产生以上问题,所以在原有的指令后面添加一些无关紧要的指令
.text addi $t4, $zero, 5 sw $t4, 4($zero)#事先向1号存储单元存入5 add $t5, $zero, $zero addu $t7, $t5, $t6 add $t1, $a0, $v1 lw $2, 4($zero)#从1号存储单元读出 sw $2, 8($zero)#存入2号存储单元 addu $t7, $t5, $t6 #后面填充指令,防止执行过程中出现指令不明确 add $t1, $a0, $v1 addu $t7, $t5, $t6 add $t1, $a0, $v1 |
再次仿真,发现仍然有阻塞,检查对印代码:
根据汇编指令,在0x301c处出现阻塞,这个时候lw在EXE级,sw在ID级,阻塞控制模块识别到lw在EXE,同时ID级的I型指令rt字段需要使用数据,所以发生了阻塞
if((//满足条件即阻塞 EXE_is_load_word && EXE_num_write != 0 && EXE_num_write == ID_rt && ID_op != `instruction_op_ADDI//对于I型指令,不关心rt && ID_op != `instruction_op_ADDIU && ID_op != `instruction_op_ANDI && ID_op != `instruction_op_ORI && ID_op != `instruction_op_LUI && ID_op != `instruction_op_SW && ID_op != `instruction_op_LW ) || ( EXE_is_load_word && EXE_num_write != 0 && EXE_num_write == ID_rs ) ) |
在代码中可以看到,我的代码已经考虑了如果是I型指令,那么rt与lw写寄存器的寄存器号一致时,不会阻塞。进行波形仿真:
在黄色框内,发现op字段位数不对(本来是6位,但是我这里只有5位),进行更改。
更正完成之后,顺利通过测试
在实现上一条指令为EXE型的一阶数据冒险的时候,发现流水线寄存器被意外清零。
回忆发现:在之前解决控制冒险的时候,对IF/ID这一个流水线寄存器进行过置零。
我在代码中,使用ID级的比较器进行判断。即,如果比较器的输出zero为1,则发生跳转,清空IF/ID级的寄存器;否则正常执行。
具体实现如下:
always @(*) begin//j,jr,jal指令阻塞,面对IF_ID if(ID_op == `instruction_op_J || ID_op == `instruction_op_JAL || (ID_op == `instruction_op_R_type && ID_funct == 6'b001000))begin IF_ID_flush = 1; end else if(ID_op == `instruction_op_BEQ)begin //判断逻辑为这两句: if(ID_zero == 1) IF_ID_flush = 1; else IF_ID_flush = 0; end else begin IF_ID_flush = 0; end end |
在之前的设计中,没有考虑冒险,所以只要遇到ID级位beq指令,如果zero为0,那么直接把刚取的指令清零。
但是如果由于ID级所需要的数据在这一时刻无法通过旁路得到,那么就必须要发生阻塞,阻塞的时候,不可以对IF/ID级流水线进行置0.把控制逻辑修改如下:
其中one_cycle_stall表示是否为阻塞一个周期的情况。
if(ID_zero == 1 && (~one_cycle_stall)) IF_ID_flush = 1; else IF_ID_flush = 0; |
在测试样例中部的时候,我的逻辑控制没有问题,但是数据部分出现错误,推测是数据通路连接不正常。由于问题出现在MEM阶段,检查我的数据通路。
发现我的流水线寄存器的输入并不是经过数据冒险处理之后的,而是直接把ID级的数据赋给了EXE/MEM流水线寄存器。(如图中红色的线)
错误代码如下:
exe_mem EXE_MEM( ……… .nxt_b(EXE_b), ……… ) |
需要把下一级流水线的输入与多路选择器的输出进行连接。
发现在第二次阻塞时出现问题。
本来需要两次,但是我的程序仅仅阻塞了一次。编写汇编代码进行测试:
addi $t1, $zero, 20 sw $t1, 4($zero) addi $2, $zero, 20 lw $1, 4($zero) beq $1, $2, L1 sll $zero, $zero, 0 sll $zero, $zero, 0 sll $zero, $zero, 0 sll $zero, $zero, 0 L1: sll $zero, $zero, 0 |
在代码中,beq指令前面有一条lw指令,可以用来测试我的CPU
波形图如图,第二次阻塞的控制信号为second_cycle_stall,黄色方框圈出来的就是我的程序需要阻塞的周期,明显看到,是因为second_cycle_stall信号出现了问题。
在代码中进行检查,发现:IF/ID级的寄存器同样被清空,根据 问题七 中的情况,迅速定位到控制冒险中。
红色框圈住的为新增加的语句。
在问题九的内容得到了修正之后,发现second_cycle_stall的值还是不正常。为此,根据确定second_cycle_stall的条件语句进行拆分,见代码:
wire test1; wire test2; wire test3; assign test1 = MEM_op == `instruction_op_LW && ID_op == `instruction_op_BEQ; assign test2 = MEM_num_write != 0 ; assign test3 = (MEM_num_write == ID_rs || MEM_num_write == ID_rt); |
然后继续仿真,得到的波形如图:
在图中发现,if中的条件表达式全部为x(即test1, test2, test3,均不确定),猜测可能没有完成接线。检查代码发现对于新增加的MEM_op和MEM_num_write没有接线。
更改之后,发现正常
单周期CPU:一个周期执行一条指令,所需要的时钟周期是由最慢的那一条指令来决定的(即lw),包含了IF,ID,EXE,MEM,WB的时间之和。
流水线CPU:把单周期CPU的执行阶段划分为5个阶段,在同一时间,在每一个阶段执行不同的指令。由于指令是“重叠”执行的,所以流水线CPU的CPI与单周期的CPU一致,均为1。但是由于阶段的划分,使得流水线CPU的时钟周期约为单周期CPU时钟周期的五分之一。
评价性能,主要是考虑一条指令的执行延迟以及单位时间内执行的指令数。
对于执行单条指令的延迟,以lw指令为例子。单周期CPU执行这一条指令的时间就是时钟周期T。但是对于流水线而言,如果流水级划分不均匀,那么最慢的流水级的执行时间大于T/5,所以执行这一条指令的时间也大于T。
由此可见,流水线CPU如果在流水级划分不均匀的情况下,执行单条指令的速度比单周期CPU更慢。
对于程序运行的时候,有大量的指令需要执行,这些指令可以填满流水级,使得各个部件满负荷运转。此时,单周期CPU与流水线CPU的CPI可以认为近似为1.而流水线CPU的时钟周期约为单周期CPU时钟周期的1/5,所以流水线CPU更快。
综上所述:流水线CPU相对于单周期CPU并没有提高单条指令的执行时间。但是对于一个程序而言,其有许多的指令,流水线CPU对于大量指令的执行时间是单周期CPU的1/5,执行的效率显著提高,对于用户而言,等待的时间减短。