五级流水线CPU(精简指令)-Verilog设计

MIPS五级流水线CPU-Verilog设计

设计方案

  • 整体思路采用分布式译码(在每个极分别实例化一个Control模块,并输出相应的选择信号)。
  • 冒险处理采用分布式判断(在每个极分别使用一个模块用于判断是否进行转发和阻塞)。

流水级

  • I极:取指令。
  • D极:寄存器堆GRF所在极,同时进行beq,jr,jal等跳转指令的跳转地址计算与跳转实现。
  • E极:ALU模块所在极,计算ALU结果。
  • M极:内存模块DM所在极,实现内存读写。
  • W极:将一次流水线进程计算后需要写回寄存器堆的值写回GFR。

AT模型

  • AT模型即Address-Time,通过流水线某一极指令使用寄存器所需要的时间与其前序指令写入寄存器堆的时间,以及这两个寄存器的地址是否相同,判断流水线此时是否需要转发或阻塞。
  • 阻塞判断:根据其定义,需要tUse和tNew两个时间,将后序极的tNew和需要改变的寄存器的地址传入当前极进行判断,如果寄存器地址相同且tUse < tNew(通俗地说也就是我马上要使用该寄存器了,但前序指令对此寄存器值的修改还需要一段时间才能完成)则需要进行阻塞。
    • 阻塞方法:在保持D极指令,I极pc值与输出的指令不变,在D极到E极之间插入一条nop指令。
  • 转发判断:如果寄存器地址相同且tUse >= tNew&&tNew==0&&RegWrite==1(也就是说,我使用该寄存器的时间还早,前序指令有足够的时间改变此寄存器的值,但是我又无法回头再次读取GRF的输出值),则进行转发。

GRF内部转发

用于处理同时读写同一个寄存器时产生的矛盾,此时只需要将GRF输出值替换为写入值即可(注意:0号寄存器保持输出0)。

流水线正常运行模块

PC

  1. 输入端口设置
    • [31:0]imm:PC跳转值
    • PCset:beq类PC跳转选择
    • PCjump:J指令跳转选择
    • clk:时钟信号
    • reset:同步复位信号
  2. 输出端口设置
    • [31:0]PCout:当前PC值
  3. 模块内部实现:根据PCset与PCjump选择PC寄存器值的变化行为,reset有效时PC寄存器值设置为0x3000。
  4. 部分代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    always @(posedge clk) begin
    if (reset == 1) begin
    pc <= 16'h3000;
    end else begin
    if (freez == 0) begin
    if (PCset == 1) begin
    pc <= imm;
    end else if (PCjump == 1) begin
    pc <= imm;
    end else begin
    pc <= pc + 4;
    end
    end else begin
    pc<=pc;
    end
    end
    end

IM

  1. 输入端口:[31:0] pc
  2. 输出端口:[31:0] code
  3. 模块内部实现:设置32位寄存器数组Rom[0:4095]用于存放指令,根据pc值选择相应指令输出到code。
  4. 部分代码实现
    1
    2
    3
    4
    5
    reg [31:0] Rom[0:12'd4095];
    initial begin
    $readmemh("code.txt",Rom);
    end
    assign code = Rom[(pc-16'h3000)>>2];

Control

根据输入的opcode和func判断当前指令为add,sub,ori,beq,lw,sw,jr,jal或lui,并据此判断各输出端口输出情况,具体如下表。

类型输出 add sub ori beq lw sw jr jal lui
RegDst 1 1 0 0 0 0 0 0 0
ALUsrc 0 0 1 0 1 1 0 0 0
MemtoReg 0 0 0 0 1 0 0 0 0
RegWrite 1 1 1 0 1 0 0 1 1
MemWrite 0 0 0 0 0 1 0 0 0
PCset 0 0 0 1 0 0 0 0 0
PCjump 0 0 0 0 0 0 1 1 0
EXTop 00 00 00 00 01 01 00 00 10
lui 0 0 0 0 0 0 0 0 1
PCtoReg 0 0 0 0 0 0 0 1 0
RegtoPC 0 0 0 0 0 0 1 0 0
ALUop 00 01 10 01 00 00 00 00 00
1
2
3
4
5
6
always @(*) begin
case (code[31:26])
`zero: begin
case (code[5:0])
`addfun: begin
//TODO......

GRF

  1. 输入端口设置
    • [4:0]A1:PD1输出对应寄存器地址
    • [4:0]A2:PD2输出对应寄存器地址
    • [31:0]WD:写入寄存器的值
    • [4:0]A3:写入寄存器地址
    • [31:0]pc:当前pc值,用于输出测试
    • clk:时钟信号
    • reset:同步复位信号
    • WE:写使能信号
  2. 输出端口设置
    • [31:0]PD1:A1寄存器对应值
    • [31:0]PD2:A2寄存器对应值
  3. 模块内部实现
    设置一32位寄存器数组grf[0:31]作为寄存器堆,reset有效时全部归零,若写使能信号有效则系统调用输出所需测试的信息,若A3!=0则写入到对应位置(0号寄存器恒为0)。
  4. 部分代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    always @(posedge clk) begin
    if (reset==1) begin
    for (i = 0;i<32 ;i=i+1 ) begin
    grf[i]<=0;
    end
    end
    else begin
    if (WE==1) begin
    if (A3!=0) begin
    grf[A3]<=WD;
    end
    $display("@%h: $%d <= %h", pc, A3, WD);
    end
    end
    end
    assign PD1 = (A3==A1&&WE==1&&A3!=0)?WD:grf[A1];
    assign PD2 = (A3==A2&&WE==1&&A3!=0)?WD:grf[A2];

EXT

  1. 输入端口设置
    • EXTop:0时零扩展,1时符号扩展
    • [15:0]input1:需要扩展的数
  2. 输出端口设置
    • [31:0]output1:扩展后的数
  3. 根据上述信息实现即可
  4. 部分代码实现
    1
    2
    3
    assign output1 = (EXTop == 1)?{{16{input1[15]}},input1}:
    (EXTop == 2)?{input1,{16{1'b0}}}:
    {{16{1'b0}},input1};

ALU

  1. 输入端口设置
    • [31:0]input1:输入数
    • [31:0]input2:输入数
    • [1:0]op:为00时做加法,01时做减法,10时做或运算,11比较两个输入相等输出1,否则输出0
  2. 输出端口设置
    • [31:0]output1:输出对应计算结果
  3. 根据上述信息实现即可
  4. 部分代码实现
    1
    2
    3
    4
    5
    assign output1 = (op==0)?(input1+input2):
    (op==1)?(input1-input2):
    (op==2)?(input1|input2):
    (op==3)?(input1==input2):
    0;

DM

  1. 输入端口设置
    • [13:0]addr:读写内存地址
    • [31:0]W:写入内存的数据
    • WR:读写控制信号,为1时写入,0时读出
    • clk:时钟信号
    • reset:同步复位信号
    • [31:0]pc:当前pc值,用于输出测试数据
  2. 输出端口设置
    • [31:0]out:读出内存的值
  3. 具体实现
    使用一个8位寄存器数组Ram[0:0x3fff]作为内存,reset有效时所有内存清零,读写控制信号为1时,将addr,addr+4,addr+8,addr+12的寄存器值写为输入值,输出同理。
  4. 部分代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    always @(posedge clk) begin
    if (reset==1) begin
    for (i = 0;i<=14'h3fff ;i=i+1 ) begin
    Ram[i]<=0;
    end
    end
    else begin
    if (WR==1) begin
    Ram[addr] <= W[7:0];
    Ram[addr+1] <= W[15:8];
    Ram[addr+2] <= W[23:16];
    Ram[addr+3] <= W[31:24];
    $display("@%h: *%h <= %h", pc, {{18{1'b0}},addr}, W);
    end
    end
    end
    assign out = (WR==0)?{Ram[addr+3],Ram[addr+2],Ram[addr+1],Ram[addr]}:
    0;

CMP

比较模块,两个输入值相等时输出1

IF_DR

  • 用于将I极产生的指令机器码和pc值传递至D极,内部用寄存器实现分隔I极与D极

DR_EX

  • 用于将D极的机器码,pc值,GRF的两个输出值PD1、PD2,sw指令需要写入内存的值传递至E极

EX_DM

  • 用于将E极的机器码,pc值,ALU计算结果,sw指令需要写入内存的值传递至M极

DM_WB

  • 用于将M极的机器码,pc值,ALU计算值,内存读出值传递至W极

冒险判断模块设计

为处理D,E,M产生的数据冲突,分别设计dangerD,dangerE,dangerMw三个控制器,控制器中保存指令的tUse,tNew用于判断是否需要转发或阻塞,为了阻塞的方便执行,此CPU阻塞仅在D极执行。

  • dangerD
    • freez信号:判断是否需要阻塞
    • tl与tr信号:判断CMP模块两个输入端是否需要转发,以及sw指令需要写入内存的寄存器的值是否需要转发
    • 特别地:由于sw指令所要使用的两个寄存器的使用时间并不相同,此处采用的方法为:对sw一类指令设计两个tUse,分别表示两个寄存器的使用时间,其中用于计算写入内存的地址的寄存器的值留到E极ALU模块以前随其他指令一同判断转发,而需要写入内存的寄存器则在D极与beq等指令一同判断是否需要转发或阻塞
    • 部分代码实现
      1
      2
      3
      4
      5
      6
      7
      8
      9
      assign freez = (((code[31:26]==`zero&&code[5:0]==`addfun)||(code[31:26]==`zero&&code[5:0]==`subfun)||code[31:26]==`beq)&&((tUse < tE && (code[25:21] == regE || code[20:16] == regE)&&regE!=0)||(tUse < tM && (code[25:21] == regM || code[20:16] == regM)&&regM!=0)||(tUse < tW && (code[25:21] == regW || code[20:16] == regW)&&regW!=0)))||
      ((code[31:26]==`ori||code[31:26]==`lw||code[31:26]==`sw||(code[5:0]==`jrfun&&code[31:26]==`zero))&&((tUse < tE && (code[25:21] == regE)&&code[25:21]!=0)||(tUse < tM && (code[25:21] == regM)&&code[25:21]!=0)||(tUse < tW && (code[25:21] == regW)&&code[25:21]!=0)))||
      (code[31:26]==`sw&&((tUseR < tM && (code[20:16] == regM)&&code[20:16]!=0)||(tUseR < tW && (code[20:16] == regW)&&code[20:16]!=0)));
      assign tL = (tUse >= tM&&tM==0&&RegWriteM==1&& (code[25:21] == regM)&&code[25:21]!=0)?1:
      (tUse >= tW&&tW==0&&RegWriteW==1&& (code[25:21] == regW)&&code[25:21]!=0)?2:
      0;
      assign tR = (RegWriteM==1&&tUse >= tM && tM==0&& (code[20:16] == regM)&&code[20:16]!=0)?1:
      (RegWriteW==1&&tUse >= tW &&tW==0&& (code[20:16] == regW)&&code[20:16]!=0)?2:
      0;
  • dangerE
    与dangerD大体相同,但不进行阻塞判断,同时需要输出tNew的值与当前指令需要写入寄存器堆的地址
    • 部分代码实现
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      assign tL = (tM==0&&RegWriteM==1&& (code[25:21] == regM)&&code[25:21]!=0)?1:
      (tW==0&&RegWriteW==1&&(code[25:21] == regW)&&code[25:21]!=0)?2:
      0;
      assign tR = (RegWriteM==1&&tUse >= tM && tM==0&& (code[20:16] == regM)&&code[20:16]!=0)?1:
      (RegWriteW==1&&tUse >= tW &&tW==0&& (code[20:16] == regW)&&code[20:16]!=0)?2:
      0;
      assign tN=tNew;
      assign regEOut=((code[31:26]==`zero&&code[5:0]==`addfun)||(code[31:26]==`zero&&code[5:0]==`subfun))?code[15:11]:
      (code[31:26]==`ori||code[31:26]==`lw||code[31:26]==`lui)?code[20:16]:
      (code[31:26]==`jal)?5'b11111:
      ((code[5:0]==`jrfun&&code[31:26]==`zero)||code[31:26]==`beq||code[31:26]==`sw)?0:
      0;
  • dangerMw
    输出部分与dangerE相同,但是特别地,由于sw写入内存的值可能由于前一条指令对寄存器堆的修改而改变,故需要在M极判断是否需要转发
    • 部分代码实现
      1
      2
      3
      4
      5
      6
      7
      assign RegOut=((code[31:26]==`zero&&code[5:0]==`addfun)||(code[31:26]==`zero&&code[5:0]==`subfun))?code[15:11]:
      (code[31:26]==`ori||code[31:26]==`lw||code[31:26]==`lui)?code[20:16]:
      (code[31:26]==`jal)?5'b11111:
      ((code[5:0]==`jrfun&&code[31:26]==`zero)||code[31:26]==`beq||code[31:26]==`sw)?0:
      0;
      assign memAddrTrans=(MemWrite==1)?((RegWriteW==1&&code[20:16]==regW&&code[20:16]!=0)?1:0):
      0;

备注

Control各输出端口含义

  • RegDst:有效时选择rd值作为GRF写入地址
  • ALUsrc:有效时选择EXT输出值作为ALU其中一个输入,反之为寄存器输出其中一个输出值
  • MemtoReg:有效时选择DM读出值作为寄存器写入值
  • RegWrite:作为GRF写使能信号
  • MemWrite:作为DM读写控制信号
  • PCset:PC模块对应输入,有效时,若ALU输出结果为1则将imm符号扩展后左移2为的值作为PC模块输入。
  • PCjump:PC模块对应输入,有效时将jump地址作为PC模块输入
  • EXTop:EXT模块op端口输入
  • lui:有效时选择imm加载到高位的值作为寄存器写入数据
  • PCtoReg:有效时将PC值写入31号寄存器
  • RegtoPC:有效时选择相应位置寄存器输出作为jump地址
  • ALUop:作为ALU模块op端口输入

部分问题的解释

  1. 结构冒险的处理:此CPU采用哈佛架构,指令存储器与数据存储器分离,不存在此冒险。
  2. 控制冒险的处理:由于分支跳转在D极实现,此时I极已经读出了跳转指令的下一条指令,此指令理论上应当不执行,但其进入了流水线,会对CPU运行造成干扰。此时引入延迟槽的概念,延迟槽即在分支跳转语句之间插入一条无关指令(CPU会执行此指令,但不会对结果造成任何影响),此指令为编译器所加,不需要CPU设计者考虑(即不需要考虑控制冒险)。但是,由于分支跳转语句下一条指令为无关指令,jal指令在跳转时需要将返回值+4再写入31号寄存器。