当前位置:   article > 正文

CPU设计实战-第一条指令ori的实现即最简单的五级流水线的实现

cpu设计实战

目录

CPU的一般性设计方法

五级流水线的过程

 defines头文件的定义

一、取指阶段的实现

1.1PC模块

1.2 IF/ID模块即取指和译码中间阶段的模块

二、译码阶段

2.1 ID模块

2.2 regfile寄存器堆模块具体实现

2.3 ID/EX模块的实现

三、执行阶段的实现

3.1 EX模块

3.2 EX/MEM模块

四、访存阶段的实现

4.1 MEM模块

4.2 MEM/WB模块

五、回写阶段的实现


CPU的一般性设计方法

cpu设计就是设计数据通路+控制逻辑。因为cpu就是一个数字逻辑电路,包含组合逻辑和时序逻辑。数据通路就是输入,运算,存储的数据在输出的数据都在组合逻辑电路和时序逻辑电路上流转。同时,因为数据通路中会有多路选择器、时序逻辑器件,所以还要有相应的控制信号,产生这些控制信号的逻辑称为控制逻辑。所以,从宏观的视角来看,设计一个CPU就是设计它的“数据通路+控制逻辑"。

市面上不同的书籍介绍的实现方法不同,如最开始看的CPU设计实战,在介绍实现方法时一次考虑所有的指令、所有的情况,然后给出代码。在后来代码实现时有些吃力故又找来下面这本书。

自己动手写CPU这本书中,笔者借鉴了软件开发中的“增量模型”概念,也称为迭代思想:先考虑最简单的情况,给出代码,然后考虑稍微多一 点的情况,修改、补充代码,随着考虑情况的增多,不停地修改、补充代码,最终,使代码实现需求。适合初学者跟着实现一遍。

五级流水线的过程

  • 取指阶段:从指令存储器读出指令,同时确定下一条指令地址。
  • 译码阶段:对指令进行译码,从通用寄存器中读出要使用的寄存器的值,如果指令中含有立即数,那么还要将立即数进行符号扩展或无符号扩展。如果是转移指令,并且满足转移条件,那么给出转移目标,作为新的指令地址。
  • 执行阶段:按照译码阶段给出的操作数、运算类型,进行运算,给出运算结果。如果是Load/Store指令,那么还会计算Load/Store的目标地址。
  • 访存阶段: 如果是Load/Store指令,那么在此阶段会访问数据存储器,反之,只是将执行阶段的结果向下传递到回写阶段。同时,在此阶段还要判断是否有异常需要处理,如果有,那么会清除流水线,然后转移到异常处理例程入口地址处继续执行。
  • 回写阶段:将运算结果保存到目标寄存器。

 defines头文件的定义

defines.v此文件定义了下面模块中所用到一些宏定义,如常用的数据宽度,数据长度以及指令码的定义。

使用头文件只需在模块最前面加上`include"defines.v"

  1. //全局
  2. `define RstEnable 1'b1
  3. `define RstDisable 1'b0
  4. `define ZeroWord 32'h00000000
  5. `define WriteEnable 1'b1
  6. `define WriteDisable 1'b0
  7. `define ReadEnable 1'b1
  8. `define ReadDisable 1'b0
  9. `define AluOpBus 7:0
  10. `define AluSelBus 2:0
  11. `define InstValid 1'b0
  12. `define InstInvalid 1'b1
  13. `define Stop 1'b1
  14. `define NoStop 1'b0
  15. `define InDelaySlot 1'b1
  16. `define NotInDelaySlot 1'b0
  17. `define Branch 1'b1
  18. `define NotBranch 1'b0
  19. `define InterruptAssert 1'b1
  20. `define InterruptNotAssert 1'b0
  21. `define TrapAssert 1'b1
  22. `define TrapNotAssert 1'b0
  23. `define True_v 1'b1
  24. `define False_v 1'b0
  25. `define ChipEnable 1'b1
  26. `define ChipDisable 1'b0
  27. //指令
  28. `define EXE_ORI 6'b001101
  29. `define EXE_NOP 6'b000000
  30. //AluOp
  31. `define EXE_OR_OP 8'b00100101
  32. `define EXE_ORI_OP 8'b01011010
  33. `define EXE_NOP_OP 8'b00000000
  34. //AluSel
  35. `define EXE_RES_LOGIC 3'b001
  36. `define EXE_RES_NOP 3'b000
  37. //指令存储器inst_rom
  38. `define InstAddrBus 31:0
  39. `define InstBus 31:0
  40. `define InstMemNum 131071
  41. `define InstMemNumLog2 17
  42. //通用寄存器regfile
  43. `define RegAddrBus 4:0
  44. `define RegBus 31:0
  45. `define RegWidth 32
  46. `define DoubleRegWidth 64
  47. `define DoubleRegBus 63:0
  48. `define RegNum 32
  49. `define RegNumLog2 5
  50. `define NOPRegAddr 5'b00000

一、取指阶段的实现

取指阶段整体的作用:取出指令存储器中的指令,PC值递增,准备取下一条指令。

1.1PC模块

首先思考接口部分的实现,需要了解以下两点:

1.PC模块的实现目标:

给出指令地址就是32位的pc

2.与前后模块的关系:

之前没有模块就是基本的时钟clk和复位信号rst;

之后的模块是指令存储器(因为要去这里根据指令地址把指令码取出来)因此在取指的时候需要把指令存储器的使能端口打开这里记为ce。

接下来思考内部的功能实现,这里我们肯定要了解PC模块实现的功能,这里可以根据输出信号来设计:

PC:每一个时钟周期+4,要注意只有在存储器打开的时候才行否则一直为0;

CE:根据复位信号对指令存储器进行使能,复位的时候存储器不可用

  1. module pc_reg(
  2. input wire clk,
  3. input wire rst,
  4. output reg [31:0] pc,
  5. output reg ce,
  6. );
  7. //复位时使能无效
  8. always @(posedge clk) begin
  9. if (rst) begin
  10. ce = 0;
  11. end else begin
  12. ce = 1;
  13. end
  14. end
  15. //使能无效时为0,有效+4
  16. always @(posedge clk ) begin
  17. if (ce == 0) begin //先写特殊情况
  18. pc <= 32'h0000_0000;
  19. end
  20. else begin //再写更多的那种情况
  21. pc <= pc + 4'h4;
  22. end
  23. end
  24. endmodule

1.2 IF/ID模块即取指和译码中间阶段的模块

首先思考接口部分的实现,需要了解以下两点:

1.IF/ID模块的实现目标:

暂时保存取指阶段的指令和指令地址,在下一个时钟周期转递给译码阶段。加上这个模块是为了确保流水线的正常运行,因为流水线是按照时钟周期划分的如下图。

2.与前后模块的关系:

之前的模块就是PC模块和指令存储器模块,接收这两个模块中输出的指令地址if_pc和指令码if_inst;

之后的模块就是译码模块,我们需要将保存的指令和指令地址输出给译码模块进行译码,输出指令地址id_pc,输出指令为id_inst。

接下来思考内部的功能实现,已经了解IF/ID模块实现的功能就是暂时保存并在下一个上升周期输出给译码阶段,所以只需要一个触发器,不过要考虑复位时要清零。

  1. module if_id(
  2. input clk,
  3. input rst,
  4. input [31:0] if_pc,
  5. input [31:0] if_inst,
  6. output reg [31:0] id_pc,
  7. output reg [31:0] id_inst
  8. );
  9. //复位时输出都为0,其他直接赋值
  10. always @(posedge clk ) begin
  11. if (rst) begin
  12. id_pc <= 0;
  13. id_inst <= 0;
  14. end else begin
  15. id_pc <= if_pc;
  16. id_inst <= if_inst;
  17. end
  18. end
  19. endmodule

二、译码阶段

我们需要先回顾下ori指令的实现:

指令用法为: ori rs, rt, immediate,作用是将指令中的16位立即数immediate进行无符号
扩展至32位,然后与索引为rs的通用寄存器的值进行逻辑“或”运算,运算结果保存到索引
为rt的通用寄存器中。

2.1 ID模块

在分析ID模块的接口时首先需要了解ID模块实现了哪些功能?

ID模块的作用是对指令进行译码,得到最终运算的类型、子类型、源操作数1、源操作
数2、要写入的目的寄存器地址等信息,其中运算类型指的是逻辑运算、移位运算、算术运算
等,子类型指的是更加详细的运算类型,比如:当运算类型是逻辑运算时,运算子类型可以是,逻辑“或"运算、逻辑“与”运算、逻辑“异或”运算等。

根据上述功能,输入肯定有pc_i和inst_i;

因为要对寄存器中源操作数进行操作,所以要根据寄存器里的数据地址去寄存器堆里读写读数据。故输出有reg_addr和reg_data。又因为不同的指令操作,需要读写寄存器的数量不同,ori指令只需要读一个寄存器数据即可,根据下图一共有三种类型的指令,最多对两个寄存器进行读操作,所以这里的reg_addr和reg_data各有两个。此外每个读操作还具有一个读使能来判断是否进行读写即reg1_read_o和reg2_read_o。

 因为最终要得到最终运算的类型(aluop_o)、子类型(alusel_o)、源操作数1(reg1)、源操作数2(re2)、要写入的目的寄存器地址(wd_0)等信息,这些信息是为了送到执行阶段进行运算。而又根据上图可以看到J类型的指令不需要wd_0,所以需要一个使能信号wreg_0判断是否写入目的寄存器,不过写回操作是流水线的最后一个操作,所以要一直到跟着流水到写回操作。

据此ID模块的输入输出信号分析完毕,可以看到对输入输出信号的分析需要结合模块功能以及上下模块的关系来综合考虑。输入输出信号如下图所示:

输出信号具体实现判断条件的来源
alu_op根据不同指令得到op
alusel_o同上op
reg1_o根据读使能判断等于reg1_data_i或immreg1_read_o
reg2_o根据读使能判断等于reg2_data_i或immreg2_read_o
wd_o根据指令判断输出目的地址op
wreg_o根据指令判断使能op
reg1_addr_o根据输入inst_i指令码得到
reg2_addr_o根据输入inst_i指令码得到
reg1_read_o根据指令判断使能op
reg2_read_o根据指令判断使能op

根据每个输出信号的判断条件(这里的判断条件就是指让输出信号变化的来源,比如对于aluop_o它只随着指令的变化而改变,所以判断条件op),可以分为两段来写:

1.以op信号为判断条件的输出信号赋值

  1. //取得指令码功能码操作地址等
  2. wire [5:0] op = inst_i[31:26];
  3. reg [31:0] imm;//为什么是reg?
  4. //根据不同指令进行译码,输出操作类型以及使能信号
  5. //回答:是否进行写,写地址;要从寄存器读几个数据;需要立即数吗;
  6. //其实都是需要输出到下一阶段使用的信号
  7. always @(*) begin
  8. //根据指令的特点对使能等信号进行赋值
  9. case (op)
  10. `EXE_ORI:begin
  11. aluop_o = `EXE_OR_OP;
  12. alusel_o = `EXE_RES_LOGIC;
  13. reg1_read_o = 1;
  14. reg2_read_o = 0;
  15. wreg_o = 1;
  16. imm = {16'b0,inst_i[15:0]};
  17. wd_o = inst_i[20:16];
  18. end
  19. default: begin
  20. end
  21. endcase
  22. end

 2.以读使能信号为判断条件的输出信号

  1. //根据寄存器堆返回的数据进行输出,需要考虑的情况有:复位,读使能有效,读使能无效即输出立即数
  2. always @(*) begin
  3. if (rst) begin
  4. reg1_o = 0;
  5. end
  6. else if (!reg1_read_o) begin
  7. reg1_o = imm;
  8. end
  9. else if (reg1_read_o) begin
  10. reg1_o = reg1_data_i;
  11. end
  12. else begin
  13. reg1_o = 0;
  14. end
  15. end
  16. always @(*) begin
  17. if (rst) begin
  18. reg2_o = 0;
  19. end
  20. else if (!reg2_read_o) begin
  21. reg2_o = imm;
  22. end
  23. else if (reg2_read_o) begin
  24. reg2_o = reg2_data_i;
  25. end
  26. else begin
  27. reg2_o = 0;
  28. end
  29. end

3.最后还要对每个输出信号赋予初始值以确保每个状态都有输出避免xz的产生,复位时全为0即可,其他时候只需要给出两个源操作数和目的寄存器的最常用地址即可。

  1. always @(*) begin
  2. //对所有输出信号进行初始化操作,确保每个信号在任何状态下都有输出
  3. if (rst) begin
  4. aluop_o = `EXE_NOP_OP;
  5. alusel_o = `EXE_RES_NOP;
  6. //reg1_o = 0;//为什么不对输出信号全部初始化?
  7. //reg2_o = 0;
  8. wd_o = `NOPRegAddr;
  9. wreg_o = 0;
  10. reg1_addr_o = `NOPRegAddr;
  11. reg2_addr_o = `NOPRegAddr;
  12. reg1_read_o = 0;
  13. reg2_read_o = 0;
  14. imm = 0;
  15. end
  16. else begin
  17. aluop_o = `EXE_NOP_OP;
  18. alusel_o = `EXE_RES_NOP;
  19. //reg1_o = 0;
  20. //reg2_o = 0;
  21. wd_o = inst_i[15:11];//最常用的rd,先这样解释?
  22. wreg_o = 0;
  23. reg1_addr_o = inst_i[25:21];
  24. reg2_addr_o = inst_i[20:16];
  25. reg1_read_o = 0;
  26. reg2_read_o = 0;
  27. imm = 0;
  28. end
  29. //根据指令的特点对使能等信号进行赋值
  30. case (op)
  31. `EXE_ORI:begin
  32. aluop_o = `EXE_OR_OP;
  33. alusel_o = `EXE_RES_LOGIC;
  34. reg1_read_o = 1;
  35. reg2_read_o = 0;
  36. wreg_o = 1;
  37. imm = {16'b0,inst_i[15:0]};
  38. wd_o = inst_i[20:16];
  39. end
  40. default: begin
  41. end
  42. endcase
  43. end

2.2 regfile寄存器堆模块具体实现

在分析ID模块的功能时我们发现需要去寄存器堆进行读数据操作,所以还需要一个寄存器堆Regfile模块,具体实现在之前有讲过CPU设计实战-verilog实现MIPS下CPU中的寄存器堆

不过在具体实现中还是稍有不同,因为参考的书目不同。

首先每个寄存器端口都加了一个读使能信号,这个在上述讲过,因为不是所有指令都要读两个寄存器;然后对写入功能也加了写使能信号,原因同理,不是所有指令都要写回目标寄存器。

另外在写操作的具体实现中,需要注意0寄存器不能写入,因为规定恒为0;

在读操作中需要注意复位和使能信号:

1.复位和读地址为0时输出数据为0

2.当读写地址一样时,直接把写入的数据给输出信号

3.写操作是时序逻辑操作,与电平同步,读操作是组合逻辑操作,确保随时能读到

代码如下:

  1. module regfile(
  2. input clk,
  3. input rst,
  4. input [4:0] raddr1,
  5. output reg[31:0]rdata1,
  6. input re1,
  7. input [4:0] raddr2,
  8. output reg[31:0]rdata2,
  9. input re2,
  10. input [4:0] waddr,
  11. input [31:0]wdata,
  12. input we
  13. );
  14. //32个32位寄存器堆
  15. reg [31:0] regs [0:31];
  16. //写操作
  17. always @(posedge clk ) begin
  18. if (!rst && we && (waddr != 0)) begin
  19. regs[waddr] <= wdata;
  20. end
  21. end
  22. //读操作
  23. always @(*) begin
  24. if (!rst && re1) begin
  25. rdata1 = regs[raddr1];
  26. end
  27. else if (!rst && re1 && (raddr1 == 0)) begin//0号寄存器恒为0
  28. rdata1 = 0;
  29. end
  30. //读写同一个地址,把写地址直接赋值,保证写优先,这样才能读到最新的数据,这也是为什么先写写操作的代码
  31. else if (!rst && re1 && we && (raddr1 == waddr)) begin
  32. rdata1 = wdata;
  33. end
  34. else begin
  35. rdata1 = 0;
  36. end
  37. end
  38. always @(*) begin
  39. if (!rst && re2) begin
  40. rdata2 = regs[raddr2];
  41. end
  42. else if (!rst && re2 && (raddr2 == 0)) begin//0号寄存器恒为0
  43. rdata2 = 0;
  44. end
  45. else if (!rst && re2 && we && (raddr2 == waddr)) begin
  46. rdata2 = wdata;
  47. end
  48. else begin
  49. rdata2 = 0;
  50. end
  51. end
  52. end
  53. endmodule

2.3 ID/EX模块的实现

作用与之前的IF/ID模块一样都是暂时存储数据,在下一个高电平时传递给下一阶段EXE执行模块。那么输入输出接口也比较简单,都是一样的。

具体实现仅需考虑复位时的额外情况,其他时候直接输入对应输出即可。

代码如下:

  1. module id_ex(
  2. input clk,
  3. input rst,
  4. input [7:0] id_aluop,
  5. input [2:0] id_alusel,
  6. input [31:0]id_reg1,
  7. input [31:0]id_reg2,
  8. input [4:0] id_wd,
  9. input id_wreg,
  10. output reg [7:0] ex_aluop,
  11. output reg [2:0] ex_alusel,
  12. output reg [31:0] ex_reg1,
  13. output reg [31:0] ex_reg2,
  14. output reg [4:0] ex_wd,
  15. output reg ex_wreg
  16. );
  17. always @(posedge clk ) begin
  18. if (rst) begin
  19. ex_aluop <= 0;
  20. ex_alusel <= 0;
  21. ex_reg1 <= 0;
  22. ex_reg2 <= 0;
  23. ex_wd <= 0;
  24. ex_wreg <= 0;
  25. end
  26. else begin
  27. ex_aluop <= id_aluop;
  28. ex_alusel <= id_alusel;
  29. ex_reg1 <= id_reg1 ;
  30. ex_reg2 <= id_reg2 ;
  31. ex_wd <= id_wd ;
  32. ex_wreg <= id_wreg ;
  33. end
  34. end
  35. endmodule

三、执行阶段的实现

3.1 EX模块

其实之前的译码阶段的输出为什么要分为最终运算的类型、子类型、源操作数1、源操作数2主要是根据执行这个阶段来考虑的,因为需要根据不同指令不同的运算逻辑去选择执行的运算。

执行阶段的作用就是把源操作数进行运算的结果写入目的寄存器中

就ori这个运算来说,执行阶段实现的逻辑运算很简单就是根据源操作数进行或运算,最终输出计算好的写入目的寄存器的数据wdata_o和地址wd_o,i因为不是所有指令操作都有写入目的寄存器的值,所以需要加一个使能信号wreg_o是否有要写入的目的寄存器,,这个之前也说过。

具体实现做的事情:

1.依据aluop_i指示的运算子类型进行运算,此处只有逻辑“或”运算

2.依据alusel_ i指示的运算类型,选择一个运算结果作为最终结果,此处只有逻辑运算结果

对输出信号进行分析

wdata_0根据op和sel对操作数进行运算
wd_o根据输入wd_i
wreg_o根据输入wreg_i
  1. `include "defines.v"
  2. module ex(
  3. //input clk,全是组合逻辑运算
  4. input rst,
  5. input [7:0] aluop_i,
  6. input [2:0] alusel_i,
  7. input [31:0]reg1_i,
  8. input [31:0]reg2_i,
  9. input [4:0] wd_i,
  10. input wreg_i ,
  11. output reg [31:0] wdata_o,
  12. output reg [4:0] wd_o,
  13. output reg wreg_o
  14. );
  15. reg [31:0] logicout;
  16. always @(*) begin
  17. if (rst) begin
  18. logicout = 0;
  19. end
  20. else begin
  21. case (aluop_i)
  22. `EXE_OR_OP:begin
  23. logicout = reg1_i | reg2_i;
  24. end
  25. default:begin
  26. logicout = 0;
  27. end
  28. endcase
  29. end
  30. end
  31. always @(*) begin
  32. wd_o = wd_i;
  33. wreg_o = wreg_i;
  34. case (alusel_i)
  35. `EXE_RES_LOGIC: begin
  36. wdata_o = logicout;
  37. end
  38. default: begin
  39. wdata_o = 0;
  40. end
  41. endcase
  42. end
  43. endmodule

 

3.2 EX/MEM模块

EX模块的输出连接到EX/MEM模块,后者的作用是将执行阶段取得的运算结果,在下一个时钟传递到流水线访存阶段,其接口描述如下图:

  1. module ex_mem(
  2. input clk,
  3. input rst,
  4. input [31:0] ex_wdata,
  5. input [4:0] ex_wd,
  6. input ex_wreg,
  7. output reg [31:0] mem_wdata,
  8. output reg [4:0] mem_wd,
  9. output reg mem_wreg
  10. );
  11. always @(posedge clk ) begin
  12. if (rst) begin
  13. mem_wdata <= 0;
  14. mem_wd <= 0;
  15. mem_wreg <= 0;
  16. end
  17. else begin
  18. mem_wdata <= ex_wdata;
  19. mem_wd <= ex_wd;
  20. mem_wreg <= ex_wreg;
  21. end
  22. end
  23. endmodule

四、访存阶段的实现

4.1 MEM模块

现在,ori 指令进入访存阶、段了,但是由于ori 指令不需要访问数据存储器,所以在访存.阶段,不做任何事,只是简单地将执行阶段的结果向回写阶段传递即可。

  1. module mem(
  2. input rst,
  3. input [31:0] wdata_i,
  4. input [4:0] wd_i,
  5. input wreg_i,
  6. output reg [31:0]wdata_o,
  7. output reg [4:0] wd_o,
  8. output reg wreg_o
  9. );
  10. always @(*) begin
  11. if (rst) begin
  12. wdata_o = 0;
  13. wd_o = 0;
  14. wreg_o = 0;
  15. end
  16. begin
  17. wdata_o = wdata_i;
  18. wd_o = wd_i;
  19. wreg_o = wreg_i;
  20. end
  21. end
  22. endmodule

 

4.2 MEM/WB模块

MEM/WB模块的作用是将访存阶段的运算结果,在下一个时钟传递到回写阶段,其接口描述如下图所示。

MEM/WB的代码与MEM模块的代码十分相似,都是将输入信号传递到对应的输出端口,但是MEM/WB模块中的是时序逻辑电路,即在时钟上升沿才发生信号传递,而MEM模块中是组合逻辑电路。

  1. module mem_wb(
  2. input clk,
  3. input rst,
  4. input [31:0] mem_wdata,
  5. input [4:0] mem_wd,
  6. input mem_wreg,
  7. output reg [31:0] wb_wdata,
  8. output reg [4:0] wb_wd,
  9. output reg wb_reg
  10. );
  11. always @(posedge clk ) begin
  12. if (rst) begin
  13. wb_wdata <= 0;
  14. wb_wd <= 0 ;
  15. wb_reg <= 0 ;
  16. end
  17. else begin
  18. wb_wdata <= mem_wdata;
  19. wb_wd <= mem_wd ;
  20. wb_reg <= mem_wreg ;
  21. end
  22. end
  23. endmodule

五、回写阶段的实现

回写阶段其实就是把访存阶段的输出:目的寄存器地址以及要写入目的寄存器的数据传回寄存器堆,寄存器堆已经实现,将相关信号连接即可。

至此,一个最简单的实现一个指令的五级流水线模块已经写好,整体模块架构如下:

至此,第一条简单指令ORI在五级流水线的实现过程全部完成,下一章将对其进行验证,在验证之前需要对以上模块设计一个顶层模块my_mips用于给各个模块之间的连线。

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号