Skip to content
This repository has been archived by the owner on Jun 5, 2023. It is now read-only.

TCL606/MipsPipeline

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MIPS五级流水线CPU

本仓库使用verilog编写MIPS五级流水线CPU。

设计方案

基本框架

5级流水线,并实现forwarding相关电路。同时,我也实现了Branch指令在ID阶段提前跳转的功能,并做出了一系列调整保证CPU安全稳定的运行,成功避免了冒险的产生,加速了CPU的运行。

存储结构上采用哈佛结构,数据存储器与指令存储器分离。

设计实现的指令集

设计的流水线CPU,能够实现大多数MIPS指令,在春季学期在单周期、多周期CPU上已实现的指令外,还增添了以下指令:lb、bne、blez、bgtz、bltz、jal、jalr、jr、jalr 等。

设计框图

设计框图如下:

design

原理说明与部分代码实现

控制信号

控制信号在我的代码中,由Control.v实现译码。根据指令的OpCodeFunct,将生成以下控制信号:Branch、RegWrite、RegDst、MemRead、MemWrite、MemtoReg、ALUSrc1、ALUSrc2、ExtOp、LuOp、Jop、LoadByte

相比于多周期CPU,新增添的控制信号为JOpLoadByte,前者用于指示该条指令是否为跳转指令,方便CPU进行跳转与stall;后者用于指示该条指令是否为lb指令,方便CPU从主存中直接取出字节。

五级流水线原理

将指令的执行阶段划分为5个阶段,分别为:指令获取(IF)、指令译码(ID)、计算执行(EX)、访问主存(MEM)、写回寄存器堆(WB)。每两个阶段间,设计一个暂存的寄存器,用于存储该条指令在接下来的阶段中会用到的控制信号。

由于总共需要有4组寄存器,来存取5个阶段间的信息传递,我将这4组寄存器命名为:IF_ID、ID_EX、EX_MEM、MEM_WB。其中IF_ID寄存器的输入有flushhold信号,用于刷新与保持寄存器信息;ID_EX寄存器的输入有flush信号,用于刷新寄存器信息。它们的具体用法在下面涉及stall的时候详细介绍。

Stall 原理与实现

分支或跳转指令后stall

在分支指令或跳转指令后,由于两种指令我都设计为在ID阶段就完成跳转,因此在它们之后都只需要stall一个周期。stall的具体方法为:如果在ID阶段的Branch信号为真,或者JOp信号为真,则设置IF_ID寄存器的flush信号,使IF_ID寄存器在下一周期刷新,同时设置下一帧的PC为跳转的地址(若Branch指令判断为False,则PC还是会变为PC+4)。

设置flush_IFID的代码如下:

assign flush_IFID = Branch_ID || JOp_ID;

设置PC下一帧的代码如下:

assign PC_new = (RegWrite_EX && Branch_ID && (Rw_EX == rs_ID || Rw_EX == rt_ID) && Load_EX) ? PC_now - 4 :
hold_IFID ? PC_now :
PCSrc_ID == 1 ? {PC_ID[31:28], rs_ID, rt_ID, rd_ID, Shamt_ID, Funct_ID, 2'b00} :
PCSrc_ID == 2 ? dataA_ID + 4:
Branch_ID ? PC_Branch :
PC_now + 4;      

其中,第3行是针对j指令跳转的表达式,第4行是针对jr等指令跳转的表达式,第5行是针对Branch指令跳转的表达式。Branch指令在ID阶段就已完成判断,因此PC_BranchID阶段就已经被计算好,这样跳转就不会发生问题。

PC_Branch的计算方法如下

assign PC_Branch = Branch_ID && Zero ? PC_ID + 4 + ImmExtShift_ID : PC_ID + 4; 

其中Zero信号会根据Branch指令的不同来对应产生,如beq指令产生两输入是否相等的信号,bne指令产生两输入是否不等的信号。

分支指令前stall

由于在ID阶段提前判断了分支指令,这里可能会产生数据冒险,因此分支指令前也可能需要stall

细节而言,分为两种情况:

情形一:分支指令前是R型指令或计算型的I型指令

如果Branch的前一条指令是R型指令或计算型的I型指令,且前一条指令要写回的寄存器是分支指令需要用于比较的寄存器rsrt时,会引起数据冒险。

branch_front_stall

如图所示,如果Branch前是R型指令或计算型的I型指令,且有数据冒险时,ALU的计算结果要到Branch指令的ID阶段结束之后才会被计算出来,这已经无法使用forwarding的方法让Branch指令正确运行了。此时需要让Branch指令stall一个周期后,再将前一条指令的ALUOut转发到Branch指令的ID阶段。如下图所示:

branch_front_forwarding

转发操作的实现在下面的转发单元中再仔细介绍,这里先介绍stall是如何实现的。

这里Branch指令需要stall一个周期,只需将IF_ID寄存器保持住,ID_EX寄存器刷新即可。

虽然在stall的时候,PC的值仍会变化,但是由于无论如何,当Branch指令执行完ID后,都会给PC一个新值,故此时stall不需要关注PC的变化。

情形二:分支指令前是lblw指令

如果分支前的指令是lblw指令,且Load出来的数据要被Branch指令用到的话,也会引起数据冒险。与情形一不同,此时数据最早出现在Load指令的MEM阶段,因此Branch指令需要stall两个周期。

数据冒险如图所示:

branch_load_stall

stall两个周期后,就可以实现转发,示意图如下:

branch_load_forwarding

这里stall执行起来相比情形一,略微复杂一些。

具体操作是:首先要flush寄存器IF_ID和寄存器ID_EX,然后需要将PC-4。这是因为如果仅仅hold IF_ID寄存器,只能stall一个周期;只有通过flush IF_ID寄存器的同时,将当前PC(即已经执行到BranchID阶段时,在IF阶段取出来的PC)重新置为PC-4才能保证stall两个周期。

置为PC-4时一定是正确的,这是因为我已经确定了前一条被执行的指令是Load指令,而不是跳转或分支指令。

情形一与情形二的代码细节

控制信号flush_IFIDhold_IFIDflush_IDEX的逻辑如下:

assign flush_IFID = Branch_ID || JOp_ID;
assign hold_IFID = ((RegWrite_EX && Branch_ID && (Rw_EX == rs_ID || Rw_EX == rt_ID)) && Load_EX == 0) ||
                   (MemRead_EX && (rt_EX == rs_ID || rt_EX == rt_ID) && Load_EX);  // next inst is branch && !Load, stall || load use hazard
assign flush_IDEX = (RegWrite_EX && Branch_ID && (Rw_EX == rs_ID || Rw_EX == rt_ID)) ||
                    (MemRead_EX && (rt_EX == rs_ID || rt_EX == rt_ID) && Load_EX);

这里hold_IFIDflush_IFID的后面那部分是Load-Use冒险检测,前面那部分才是分支指令相关。

其中,flush_IFIDhold_IFID都是对IF_ID寄存器的控制,在不同情况下有着不同的优先级,具体实现代码如下:

always @(posedge clk or posedge reset) begin
    if(reset || (flush_IFID && Load_EX)) begin      
        // flush
        // ...
    end
    else if (hold_IFID) begin
        // hold
        // ...
    end
    else if (flush_IFID) begin
        // flush
        // ...
    end
    else begin
        // decode
        OpCode <= Instruction[31:26];
        rs <= Instruction[25:21];
        rt <= Instruction[20:16];
        rd <= Instruction[15:11];
        Shamt <= Instruction[10:6];
        Funct <= Instruction[5:0];
        PC_ID <= PC_IF;
    end
end

当目前EX阶段是Load指令时,flush_IFIDhold_IFID有着更高的优先级,这是因为此时需要stall两个周期;当目前EX阶段不是Load指令时,hold_IFIDflush_IFID有更高的优先级。

设置PC-4的代码如下:

assign PC_new = (RegWrite_EX && Branch_ID && (Rw_EX == rs_ID || Rw_EX == rt_ID) && Load_EX) ? PC_now - 4 :
hold_IFID ? PC_now :
PCSrc_ID == 1 ? {PC_ID[31:28], rs_ID, rt_ID, rd_ID, Shamt_ID, Funct_ID, 2'b00} :
PCSrc_ID == 2 ? dataA_ID + 4:
Branch_ID ? PC_Branch :
PC_now + 4;    

第1行就是设置PC-4的代码,具体逻辑是:如果EX阶段是Load,下一条指令是Branch,且Load要写回的寄存器是Branch要用到的,则下一帧的PC设为PC-4

Load-Use冒险检测并stall

当前一条指令是lblw,下一条指令是R型指令或计算型的I型指令,且Load要写入的寄存器会被下一条指令用到时,会引起数据冒险。此时在Load指令后需要stall一个周期。原理图如下:

loaduse_stall

Load出来的数据最早在MEM阶段后才出现,而Use的时候在EX阶段就已经需要了,因此Load后要stall一个周期,并转发LoadData。如下图所示:

loaduse_forwarding

具体实现为:执行到Load指令的EX阶段时,可以判断下一条指令是否为Use且是否存在数据冒险。如果存在,则在下一周期保持Use指令的IF_ID寄存器,并清空ID_EX寄存器。

代码上就是:

assign hold_IFID = ((RegWrite_EX && Branch_ID && (Rw_EX == rs_ID || Rw_EX == rt_ID)) && Load_EX == 0) ||
                   (MemRead_EX && (rt_EX == rs_ID || rt_EX == rt_ID) && Load_EX);  // next inst is branch && !Load, stall || load use hazard
assign flush_IDEX = (RegWrite_EX && Branch_ID && (Rw_EX == rs_ID || Rw_EX == rt_ID)) ||
                    (MemRead_EX && (rt_EX == rs_ID || rt_EX == rt_ID) && Load_EX);

上面代码中,hold_IFIDflush_IDEX的后半部分,是Load-Use的冒险检测部分。

Forwarding 原理与实现

Forwarding 到ID阶段

由于在我的设计中,Branch指令需要在ID阶段提前判断,因此我需要实现转发到ID阶段的操作,以解决Branch指令中存在的数据冒险。

我设置了BrForwardingABrForwardingB两个转发单元控制信号,来控制ID阶段中Branch指令判断的两个输入。

Branch指令用于判断的两个输入变量,可以来自于三个方面:

  • WriteData_WB:即上一条指令从DataMem中取出的数据,适用于分支指令前为Load指令的场景。
  • ALUOut_MEM:即上一条指令的ALU输出,适用于分支指令前为R型指令或计算型I型指令的场景。
  • dataA_ID or dataB_ID:直接从寄存器堆中根据rsrt的值取出的数据,适用于没有数据冒险时的场景。

以上三个场景分别对应于BrForwarding控制信号为:2、1、0。

我设计的Branch转发单元实现如下:

assign BrForwardingA = rs == Rw_WB && Load_WB ? 2 : rs == Rw_MEM && RegWrite_MEM ? 1 : 0;
assign BrForwardingB = rt == Rw_WB && Load_WB ? 2 : rt == Rw_MEM && RegWrite_MEM ? 1 : 0;

BrForwardingA为例:

  • 如果rs == Rw_WB && Load_WB,说明前一条指令是Load(已经stall了两个周期),且写回的寄存器与rs相同,因此将BrForwardingA设为2。
  • 如果rs == Rw_MEM && RegWrite_MEM,说明前一条指令是R型指令或计算型I型指令(已经stall了一个周期),且写回的寄存器与rs相同,因此将BrForwardingA设为1。
  • 没有数据冒险时,BrForwardingA默认是0。

然后,ID阶段对Branch判断的输入BrJudger,会根据BrForwarding信号进行选择,代码如下:

assign BrJuderA = BrForwardingA == 1 ? ALUOut_MEM : BrForwardingA == 2 ? WriteData_WB : dataA_ID;
assign BrJuderB = BrForwardingB == 1 ? ALUOut_MEM : BrForwardingB == 2 ? WriteData_WB : dataB_ID;

Forwarding 到EX阶段

EX阶段ALU的输入,可能会有4种来源,分别是:

  • dataA_EX or dataB_EX:从寄存器堆中读取出来并随流水线传到EX阶段的数据。
  • 移位量Shamt或立即数ImmExtOut
  • ALUOut_MEM:上一条指令的ALU计算结果。
  • WriteData_WB:上上条指令ALU计算结果,或者是上条指令Load的结果。

我设置的转发选择信号为ALUChooseAALUChooseB。以上四个场景分别对应于ALUChoose为:0、1、2、3。

我设计的转发单元代码如下:

assign ALUChooseA = ALUSrcA_EX == 1 ? 1 :
                    (RegWrite_MEM && (Rw_MEM == rs_EX) && (Rw_MEM != 0)) ? 2 :   // 优先判断MEM阶段,即前一条指令
                    (RegWrite_WB && (Rw_WB == rs_EX) && (Rw_WB != 0)) ? 3 : 0;
assign ALUChooseB = ALUSrcB_EX == 1 ? 1 : 
                    (RegWrite_MEM && (Rw_MEM == rt_EX) && (Rw_MEM != 0)) ? 2 :   // 优先判断MEM阶段,即前一条指令
                    (RegWrite_WB && (Rw_WB == rt_EX) && (Rw_WB != 0)) ? 3 : 0;

这里,ALUSrcA_EXALUSrcB_EX是指令译码单元解码出来的

控制信号,用于指示是否要使用移位量或立即数。后面的判断就是关于转发的判断。

优先判断前一条指令是否满足转发条件,不满足时再判断前前条指令是否满足条件。

ALUChooseA为例,判断的逻辑是:如果前一条指令要写回寄存器堆,且写回的寄存器为rs,且该寄存器不为$0,则将前一条指令的ALU输出转发到目前EX阶段指令的输入。如果前一条指令不满足转发条件,则看前前条指令(也包括前一条指令为Load的情况)。如果在WB阶段的要写回寄存器堆,且WB阶段写回的寄存器为rs,且该寄存器不是$0,则将要写回的值转发到ALU的输入。如果上述的转发条件都不满足,则直接使用从寄存器堆中读取的值。

有了ALUChoose信号后,就可以对ALU的输入进行选择,代码如下:

assign ALUinA = ALUChooseA == 1 ? {27'h0000000, Shamt_EX} :
                ALUChooseA == 2 ? ALUOut_MEM :
    			ALUChooseA == 3 ? WriteData_WB: dataA_EX;
assign ALUinB = ALUChooseB == 1 ? ImmExtOut_EX :
    			ALUChooseB == 2 ? ALUOut_MEM :
    			ALUChooseB == 3 ? WriteData_WB: dataB_EX;

数据存储器

数据存储器的大小我设置为512个字大小,字节地址从0x000000000x000007FF

在字节地址为0x4000000C的位置,我设置其对应外部LEDs的控制信息;在字节地址为0x40000010的位置,我设置其对应七段数码管的控制信息。

Load Byte 的实现

Load Byre大体上和Load Word类似。我只是单独添加了一个LoadByte控制信号,并根据该控制信号来选择是LoadByte还是LoadWord

大概思路是,先用LoadWord把一个字取出来,再根据地址的后2位,选取对应的Byte,并进行符号拓展后返回。

代码如下:

assign ReadData_MEM = LoadByte_MEM == 0 ? ReadData_Temp :   
                      ALUOut_MEM[1:0] == 2'b00 ? {{24{ReadData_Temp[7]}}, ReadData_Temp[7:0]} :
                      ALUOut_MEM[1:0] == 2'b01 ? {{24{ReadData_Temp[15]}}, ReadData_Temp[15:8]} :
                      ALUOut_MEM[1:0] == 2'b10 ? {{24{ReadData_Temp[23]}}, ReadData_Temp[23:16]} :
                      {{24{ReadData_Temp[31]}}, ReadData_Temp[31:24]};

其中,ReadData_Temp是从DataMemory中读取出的字。

About

Mips五级流水线CPU

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published