将陆续上传本人写的新书《自己动手写处理器》(尚未出版),今天是第11篇,我尽量每周四篇
第4章 第一条指令ori的实现
前面几章介绍了很多预备知识,也描绘了即将要实现的OpenMIPS处理器的蓝图,各位读者是不是早已摩拳擦掌,迫切希望一展身手了,好吧,本章我们将实现OpenMIPS处理器的第一条指令ori,为什么选择这条指令作为我们实现的第一条指令呢?答案就两个字——简单,指令ori用来实现逻辑“或”运算,选择一条简单的指令有助于我们排除干扰,将注意力集中在流水线结构的实现上,当然也可以选择其它类似的指令,只要简单即可。通过这条简单指令的实现,本章在4.2节将初步建立OpenMIPS的五级流水线结构,当我们在后面章节中实现其余指令的时候,都是在这个初步建立的流水线结构上进行扩充。
在ori指令实现后,要验证其实现是否正确,所以在4.3节建立了最小SOPC,仅仅包含OpenMIPS、指令存储器,用于验证ori指令是否实现正确,后续章节验证其余指令的时候,都是在这个最小SOPC或者其改进模型上进行验证。
本章最后介绍了MIPS编译环境的建立。
4.1 ori指令说明
ori是进行逻辑“或”运算的指令,其指令格式如图4-1所示。
从指令格式中可以知道,这是一个I类型的指令,ori指令的指令码是6‘b001101,所以当处理器发现正在处理的指令的高6bit是6‘b001101时,就知道当前正在处理的是ori指令。
指令用法为:ori rs, rt, immediate,作用是将指令中的16位立即数immediate进行无符号扩展至32位,然后与索引为rs的通用寄存器的值进行逻辑“或”运算,运算结果保存到索引为rt的通用寄存器中。这里需要说明以下两点。
(1)无符号扩展
在MIPS32指令集架构中,经常会有指令需要将其中的立即数进行符号扩展,或者无符号扩展,一般都是将n位立即数扩展为32位,其中,符号扩展是将n位立即数的最高位复制到扩展后的32位数据的高(32-n)位,无符号扩展则是将扩展后的32位数据的高(32-n)位都置为0。以将指令中的16位立即数扩展为32位为例,表4-1给出了当16位立即数分别是0x8000、0x1000时的符号扩展、无符号扩展的结果。
(2)通用寄存器
在MIPS32指令集架构中定义了32个通用寄存器$0-$31,OpenMIPS实现了这32个通用寄存器,使用某一个通用寄存器只需要给出相应索引,这个索引占用5bit,ori指令中的rs、rt就是通用寄存器的索引,例如:当rs为5‘b00011时,就表示通用寄存器$3。
4.2 流水线结构的建立
4.2.1 流水线的简单模型
数字电路有组合逻辑、时序逻辑之分,其中时序逻辑最基本的器件是寄存器,此处的寄存器不是在4.1节中提到的MIPS架构规定的通用寄存器$0-$31,后者是一个更高层面的概念,前者是类似于D触发器这种数字电路的基本器件。寄存器按照给定时间脉冲来进行时序同步操作,其使得时序逻辑电路具有记忆功能。而组合逻辑电路则由逻辑门组成,提供电路的所有逻辑功能。实际的数字电路一般是组合逻辑与时序逻辑的结合。如果寄存器的输出端和输入端存在环路,这样的电路称为“状态机”。如图4-2所示。如果寄存器之间有连接,而没有上述环路,这样的电路结构称为“流水线”。如果4-3所示。
在流水线结构中,信号在寄存器之间传递,每传递到一级都会引起相应的组合逻辑电路变化,对这种模型进行抽象描述就是寄存器传输级(RTL:Register Transfer Level)。本节接下来要实现的原始的OpenMIPS五级流水线结构就是图4-3的扩充。
4.2.2 原始的OpenMIPS五级流水线结构
扩充图4-3,可以得到OpenMIPS的原始数据流图如图4-4所示,这个数据流图还很不完整,在后续章节中会随着实现指令的增加而丰富,但这个原始的数据流图已经可以表达本节要实现的ori指令在流水线中的处理过程了。
图中深色部分对应的是图4-3中的D触发器,深色部分之间的部分对应的是图4-3中的组合逻辑。各个阶段完成的主要工作如下。
- 取指:取出指令存储器中的指令,PC值递增,准备取下一条指令。
- 译码:对指令进行译码,依据译码结果,从32个通用寄存器中取出源操作数,有的指令要求两个源操作数都是寄存器的值,比如or指令,有的指令要求其中一个源操作数是指令中立即数的扩展,比如ori指令,所以这里有两个复用器,用于依据指令要求,确定参与运算的操作数,最终确定的两个操作数会送到执行阶段。
- 执行阶段:依据译码阶段送入的源操作数、操作码,进行运算,对于ori指令而言,就是进行逻辑“或”运算,运算结果传递到访存阶段。
- 访存阶段:对于ori指令,在访存阶段没有任何操作,直接将运算结果向下传递到回写阶段。
- 回写阶段:将运算结果保存到目的寄存器。
图4-5是为实现上述数据流图而设计的OpenMIPS系统结构,图中显示了各个模块的接口、连接关系。每个模块上方是模块名,下方是对应的Verilog HDL程序文件名。本节接下来将分别实现图中各个模块。
4.2.3 一些宏定义
在正式开始介绍流水线结构实现之前,需要给出一些宏定义,因为在OpenMIPS的实现过程中,为了提高代码的可读性和易懂性,使用了较多的宏,全部的宏都在文件defines.v中定义。此处列举在本章中会使用到的一部分宏,后面随着OpenMIPS功能的不断完善,会有更多的宏添加进来,届时会对新增加的宏进行说明。
//******************* 全局的宏定义 *************************** `define RstEnable 1'b1 //复位信号有效 `define RstDisable 1'b0 //复位信号无效 `define ZeroWord 32'h00000000 //32位的数值0 `define WriteEnable 1'b1 //使能写 `define WriteDisable 1'b0 //禁止写 `define ReadEnable 1'b1 //使能读 `define ReadDisable 1'b0 //禁止读 `define AluOpBus 7:0 //译码阶段的输出aluop_o的宽度 `define AluSelBus 2:0 //译码阶段的输出alusel_o的宽度 `define InstValid 1'b0 //指令有效 `define InstInvalid 1'b1 //指令无效 `define True_v 1'b1 //逻辑“真” `define False_v 1'b0 //逻辑“假” `define ChipEnable 1'b1 //芯片使能 `define ChipDisable 1'b0 //芯片禁止 //********************* 与具体指令有关的宏定义 ***************************** `define EXE_ORI 6'b001101 //指令ori的指令码 `define EXE_NOP 6'b000000 //AluOp `define EXE_OR_OP 8'b00100101 `define EXE_NOP_OP 8'b00000000 //AluSel `define EXE_RES_LOGIC 3'b001 `define EXE_RES_NOP 3'b000 //********************* 与指令存储器ROM有关的宏定义 ********************** `define InstAddrBus 31:0 //ROM的地址总线宽度 `define InstBus 31:0 //ROM的数据总线宽度 `define InstMemNum 131071 //ROM的实际大小为128KB `define InstMemNumLog2 17 //ROM实际使用的地址线宽度 //********************* 与通用寄存器Regfile有关的宏定义 ******************* `define RegAddrBus 4:0 //Regfile模块的地址线宽度 `define RegBus 31:0 //Regfile模块的数据线宽度 `define RegWidth 32 //通用寄存器的宽度 `define DoubleRegWidth 64 //两倍的通用寄存器的宽度 `define DoubleRegBus 63:0 //两倍的通用寄存器的数据线宽度 `define RegNum 32 //通用寄存器的数量 `define RegNumLog2 5 //寻址通用寄存器使用的地址位数 `define NOPRegAddr 5'b00000
4.2.4 取指阶段的实现
取指阶段取出指令存储器中的指令,同时,PC值递增,准备取下一条指令,包括PC、IF/ID两个模块。
1、PC模块
PC模块的作用是给出指令地址,其接口描述如表4-2所示。
PC模块对应的源文件是pc_reg.v,代码如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。读者可以使用任何文本编辑工具编辑该文件,笔者习惯使用UltraEdit,所有的代码都是使用它编辑的,当然也可以使用Windows自带的记事本。
module pc_reg( input wire clk, input wire rst, output reg[`InstAddrBus] pc ); always @ (posedge clk) begin if (rst == `RstEnable) begin ce <= `ChipDisable; // 复位的时候指令存储器禁用 end else begin ce <= `ChipEnable; // 复位结束后,指令存储器使能 end end always @ (posedge clk) begin if (ce == `ChipDisable) begin pc <= 32'h00000000; // 指令存储器禁用的时候,PC为0 end else begin pc <= pc + 4'h4; // 指令存储器使能的时候,PC的值每时钟周期加4 end end endmodule
其中使用到了一些define.v中定义的宏,InstAddrBus宏表示指令地址线的宽度,此处定义为32,RstEnable宏表示复位信号有效,定义为1‘b1,也就是当输入rst为高电平时,表示复位信号有效。
在复位的时候,输出的指令存储器使能信号为ChipDisable,表示指令存储器禁用,其余时刻指令存储器使能信号为ChipEnable,表示指令存储器使能。
当指令存储器禁用时,PC的值保持为0,当指令存储器使能时,PC的值会在每时钟周期加4,表示下一条指令的地址,因为一条指令是32位,而我们设计的OpenMIPS是可以按照字节寻址,一条指令对应4个字节,所以PC加4指向下一条指令地址。读者需要注意区分:在2.7节设计的简单取指电路是按照字寻址的,所以每时钟周期PC加1。
2、IF/ID模块
IF/ID模块的作用是暂时保存取指阶段取得的指令,以及对应的指令地址,并在下一个时钟传递到译码阶段。其接口描述如表4-3所示。
IF/ID模块对应的源代码文件是if_id.v,代码如下,读者可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module if_id( input wire clk, input wire rst, //来自取指阶段的信号,其中宏定义InstBus表示指令宽度,为32 input wire[`InstAddrBus] if_pc, input wire[`InstBus] if_inst, //对应译码阶段的信号 output reg[`InstAddrBus] id_pc, output reg[`InstBus] id_inst ); always @ (posedge clk) begin if (rst == `RstEnable) begin id_pc <= `ZeroWord; // 复位的时候pc为0 id_inst <= `ZeroWord; // 复位的时候指令也为0,实际就是空指令 end else begin id_pc <= if_pc; // 其余时刻向下传递取指阶段的值 id_inst <= if_inst; end end endmodule
从代码可以知道,其中只有一个时序电路,IF/ID模块只是简单地将取指阶段的结果在每个时钟周期的上升沿传递到译码阶段。
4.2.5 译码阶段的实现
参考图4-5可知,IF/ID模块的输出连接到ID模块,好了,我们的指令此时已经进入了译码阶段,在此阶段,将对取到的指令进行译码:给出要进行的运算类型,以及参与运算的操作数。译码阶段包括Regfile、ID和ID/EX三个模块。
1、Regfile模块
Regfile模块实现了32个32位通用整数寄存器,可以同时进行两个寄存器的读操作和一个寄存器的写操作。其接口描述如表4-4所示。
Regfile模块对应的源代码文件是regfile.v,代码如下,可以在本书附带光盘的Code\Chapter4\目录下找到regfile.v文件。
module regfile( input wire clk, input wire rst, // 写端口 input wire we, input wire[`RegAddrBus] waddr, input wire[`RegBus] wdata, // 读端口1 input wire re1, input wire[`RegAddrBus] raddr1, output reg[`RegBus] rdata1, // 读端口2 input wire re2, input wire[`RegAddrBus] raddr2, output reg[`RegBus] rdata2 ); /**************************************************************** *********** 第一段:定义32个32位寄存器 ********* *****************************************************************/ reg[`RegBus] regs[0:`RegNum-1]; /**************************************************************** *********** 第二段:写操作 ********* *****************************************************************/ always @ (posedge clk) begin if (rst == `RstDisable) begin if((we == `WriteEnable) && (waddr != `RegNumLog2'h0)) begin regs[waddr] <= wdata; end end end /**************************************************************** *********** 第三段:读端口1的读操作 ********* *****************************************************************/ always @ (*) begin if(rst == `RstEnable) begin rdata1 <= `ZeroWord; end else if(raddr1 == `RegNumLog2'h0) begin rdata1 <= `ZeroWord; end else if((raddr1 == waddr) && (we == `WriteEnable) && (re1 == `ReadEnable)) begin rdata1 <= wdata; end else if(re1 == `ReadEnable) begin rdata1 <= regs[raddr1]; end else begin rdata1 <= `ZeroWord; end end /**************************************************************** *********** 第四段:读端口2的读操作 ********* *****************************************************************/ always @ (*) begin if(rst == `RstEnable) begin rdata2 <= `ZeroWord; end else if(raddr2 == `RegNumLog2'h0) begin rdata2 <= `ZeroWord; end else if((raddr2 == waddr) && (we == `WriteEnable) && (re2 == `ReadEnable)) begin rdata2 <= wdata; end else if(re2 == `ReadEnable) begin rdata2 <= regs[raddr2]; end else begin rdata2 <= `ZeroWord; end end endmodule
Regfile模块可以分为四段进行理解。
(1)第一段:定义了一个二维的向量,元素个数是RegNum,这是在defines.v中的一个宏定义,为32,每个元素的宽度是RegBus,这也是在defines.v中的一个宏定义,也为32,所以此处定义的就是32个32位寄存器。
(2)第二段:实现了写寄存器操作,当复位信号无效时(rst为RstDisable),在写使能信号we有效(we为WriteEnable),且写操作目的寄存器不等于0的情况下,可以将写输入数据保存到目的寄存器。之所以要判断目的寄存器不为0,是因为MIPS32架构规定$0的值只能为0,所以不要写入。WriteEnable是defines.v中定义的宏,表示写使能信号有效,这些宏定义的含义十分明显,从名称上就可以知道具体含义,所以本书后面对宏定义不再作出说明,除非这个宏定义的含义从名称上不易明白。
(3)第三段:实现了第一个读寄存器端口,分以下几步依次判断。
- 当复位信号有效时,第一个读寄存器端口的输出始终为0
- 当复位信号无效时,如果读取的是$0,那么直接给出0
- 如果第一个读寄存器端口要读取的目标寄存器与要写入的目的寄存器是同一个寄存器,那么直接将要写入的值作为第一个读寄存器端口的输出
- 上述情况都不满足,那么给出第一个读寄存器端口要读取的目标寄存器地址对应寄存器的值
- 第一个读寄存器端口没有使能时,直接输出0
(4)第四段:实现了第二个读寄存器端口,具体过程与第三段是相似的,不再重复解释。
注意一点:读寄存器操作是组合逻辑电路,也就是一旦输入的要读取的寄存器地址raddr1或者raddr2发生变化,那么会立即给出新地址对应的寄存器的值,这样可以保证在译码阶段取得要读取的寄存器的值,而写寄存器操作是时序逻辑电路,写操作发生在时钟信号的上升沿。
2、ID模块
ID模块的作用是对指令进行译码,得到最终运算的类型、子类型、源操作数1、源操作数2、要写入的目的寄存器地址等信息,其中运算类型指的是逻辑运算、移位运算、算术运算等,子类型指的是更加详细的运算类型,比如:当运算类型是逻辑运算时,运算子类型可以是逻辑“或”运算、逻辑“与”运算、逻辑“异或”运算等。ID模块的接口描述如表4-5所示。
ID模块对应的代码文件是id.v,其内容如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module id( input wire rst, input wire[`InstAddrBus] pc_i, input wire[`InstBus] inst_i, // 读取的Regfile的值 input wire[`RegBus] reg1_data_i, input wire[`RegBus] reg2_data_i, // 输出到Regfile的信息 output reg reg1_read_o, output reg reg2_read_o, output reg[`RegAddrBus] reg1_addr_o, output reg[`RegAddrBus] reg2_addr_o, // 送到执行阶段的信息 output reg[`AluOpBus] aluop_o, output reg[`AluSelBus] alusel_o, output reg[`RegBus] reg1_o, output reg[`RegBus] reg2_o, output reg[`RegAddrBus] wd_o, output reg wreg_o ); // 取得指令的指令码,功能码 // 对于ori指令只需通过判断第26-31bit的值,即可判断是否是ori指令 wire[5:0] op = inst_i[31:26]; wire[4:0] op2 = inst_i[10:6]; wire[5:0] op3 = inst_i[5:0]; wire[4:0] op4 = inst_i[20:16]; // 保存指令执行需要的立即数 reg[`RegBus] imm; // 指示指令是否有效 reg instvalid; /**************************************************************** *********** 第一段:对指令进行译码 ********* *****************************************************************/ always @ (*) begin if (rst == `RstEnable) begin aluop_o <= `EXE_NOP_OP; alusel_o <= `EXE_RES_NOP; wd_o <= `NOPRegAddr; wreg_o <= `WriteDisable; instvalid <= `InstValid; reg1_read_o <= 1'b0; reg2_read_o <= 1'b0; reg1_addr_o <= `NOPRegAddr; reg2_addr_o <= `NOPRegAddr; imm <= 32'h0; end else begin aluop_o <= `EXE_NOP_OP; alusel_o <= `EXE_RES_NOP; wd_o <= inst_i[15:11]; wreg_o <= `WriteDisable; instvalid <= `InstInvalid; reg1_read_o <= 1'b0; reg2_read_o <= 1'b0; reg1_addr_o <= inst_i[25:21]; // 默认通过Regfile读端口1读取的寄存器地址 reg2_addr_o <= inst_i[20:16]; // 默认通过Regfile读端口2读取的寄存器地址 imm <= `ZeroWord; case (op) `EXE_ORI: begin // 依据op的值判断是否是ori指令 // ori指令需要将结果写入目的寄存器,所以wreg_o为WriteEnable wreg_o <= `WriteEnable; // 运算的子类型是逻辑“或”运算 aluop_o <= `EXE_OR_OP; // 运算类型是逻辑运算 alusel_o <= `EXE_RES_LOGIC; // 需要通过Regfile的读端口1读取寄存器 reg1_read_o <= 1'b1; // 不需要通过Regfile的读端口2读取寄存器 reg2_read_o <= 1'b0; // 指令执行需要的立即数 imm <= {16'h0, inst_i[15:0]}; // 指令执行要写的目的寄存器地址 wd_o <= inst_i[20:16]; // ori指令是有效指令 instvalid <= `InstValid; end default: begin end endcase //case op end //if end //always /**************************************************************** *********** 第二段:确定进行运算的源操作数1 ********* *****************************************************************/ always @ (*) begin if(rst == `RstEnable) begin reg1_o <= `ZeroWord; end else if(reg1_read_o == 1'b1) begin reg1_o <= reg1_data_i; // Regfile读端口1的输出值 end else if(reg1_read_o == 1'b0) begin reg1_o <= imm; // 立即数 end else begin reg1_o <= `ZeroWord; end end /**************************************************************** *********** 第三段:确定进行运算的源操作数2 ********* *****************************************************************/ always @ (*) begin if(rst == `RstEnable) begin reg2_o <= `ZeroWord; end else if(reg2_read_o == 1'b1) begin reg2_o <= reg2_data_i; // Regfile读端口2的输出值 end else if(reg2_read_o == 1'b0) begin reg2_o <= imm; // 立即数 end else begin reg2_o <= `ZeroWord; end end endmodule
ID模块中的电路都是组合逻辑电路,另外,从图4-5可知ID模块与Regfile模块也有接口连接。其代码可以分为三段进行理解。
(1)第一段:实现了对指令的译码,依据指令中的特征字段区分指令,对指令ori而言,只需通过识别26-31bit的指令码是否是6‘b001101,即可判断是否是ori指令,其中的宏定义EXE_ORI就是6‘b001101,op就是指令的26-31bit,所以当op等于EXE_ORI时,就表示是ori指令,此时会有以下译码结果。
- 要读取的寄存器情况:ori指令只需要读取rs寄存器的值,默认通过Regfile读端口1读取的寄存器地址reg1_addr_o的值是指令的21-25bit,参考图4-1可知,正是ori指令中的rs,所以设置reg1_read_o为1,通过图4-5可以reg1_read_o连接Regfile的输入re1,reg1_addr_o连接Regfile的输入raddr1,结合对Regfile模块的介绍可知,译码阶段会读取寄存器rs的值。指令ori需要的另一个操作数是立即数,所以设置reg2_read_o为0,表示不通过Regfile读端口2读取寄存器,这里暗含使用立即数作为运算的操作数。imm就是指令中的立即数进行零扩展后的值。
- 要执行的运算:alusel_o给出要执行的运算类型,对于ori指令而言就是逻辑操作,即EXE_RES_LOGIC。aluop_o给出要执行的运算子类型,对于ori指令而言就是逻辑“或”运算,即EXE_OR_OP。这两个值会传递到执行阶段。
- 要写入的目的寄存器:wreg_o表示是否要写目的寄存器,ori指令要将计算结果保存到寄存器中,所以wreg_o设置为WriteEnable。wd_o是要写入的目的寄存器地址,此时就是指令的16-20bit,参考图4-1可知,正是ori指令中的rt。这两个值也会传递到执行阶段。
(2)第二段:给出参与运算的源操作数1的值,如果reg1_read_o为1,那么就将从Regfile模块读端口1读取的寄存器的值作为源操作数1,如果reg1_read_o为0,那么就将立即数作为源操作数1,对于ori而言,此处选择从Regfile模块读端口1读取的寄存器的值作为源操作数1。该值将通过reg1_o端口被传递到执行阶段。
(3)第三段:给出参与运算的源操作数2的值,如果reg2_read_o为1,那么就将从Regfile模块读端口2读取的寄存器的值作为源操作数2,如果reg2_read_o为0,那么就将立即数作为源操作数2,对于ori而言,此处选择立即数imm作为源操作数2。该值将通过reg2_o端口被传递到执行阶段。
3、ID/EX模块
参考图4-5可知,ID模块的输出连接到ID/EX模块,后者的作用是将译码阶段取得的运算类型、源操作数、要写的目的寄存器地址等结果,在下一个时钟传递到流水线执行阶段。其接口描述如表4-6所示。
ID/EX模块对应的代码文件是id_ex.v,其内容如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module id_ex( input wire clk, input wire rst, // 从译码阶段传递过来的信息 input wire[`AluOpBus] id_aluop, input wire[`AluSelBus] id_alusel, input wire[`RegBus] id_reg1, input wire[`RegBus] id_reg2, input wire[`RegAddrBus] id_wd, input wire id_wreg, // 传递到执行阶段的信息 output reg[`AluOpBus] ex_aluop, output reg[`AluSelBus] ex_alusel, output reg[`RegBus] ex_reg1, output reg[`RegBus] ex_reg2, output reg[`RegAddrBus] ex_wd, output reg ex_wreg ); always @ (posedge clk) begin if (rst == `RstEnable) begin ex_aluop <= `EXE_NOP_OP; ex_alusel <= `EXE_RES_NOP; ex_reg1 <= `ZeroWord; ex_reg2 <= `ZeroWord; ex_wd <= `NOPRegAddr; ex_wreg <= `WriteDisable; end else begin ex_aluop <= id_aluop; ex_alusel <= id_alusel; ex_reg1 <= id_reg1; ex_reg2 <= id_reg2; ex_wd <= id_wd; ex_wreg <= id_wreg; end end endmodule
代码十分清晰,其中只有一个时序电路,ID/EX模块只是简单地将译码阶段的结果在时钟周期的上升沿传递到执行阶段。执行阶段将依据这些值进行运算。
4.2.6 执行阶段的实现
现在,指令已经进入流水线的执行阶段了,在此阶段将依据译码阶段的结果,对源操作数1、源操作数2,进行指定的运算。执行阶段包括EX、EX/MEM两个模块。
1、EX模块
观察图4-5中ID/EX与EX模块的端口连接关系可知,EX模块会从ID/EX模块得到运算类型alusel_i、运算子类型aluop_i、源操作数reg1_i、源操作数reg2_i、要写的目的寄存器地址wd_i。EX模块会依据这些数据进行运算,其接口描述如表4-7所示。
EX模块对应的代码文件为ex.v,其内容如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module ex( input wire rst, // 译码阶段送到执行阶段的信息 input wire[`AluOpBus] aluop_i, input wire[`AluSelBus] alusel_i, input wire[`RegBus] reg1_i, input wire[`RegBus] reg2_i, input wire[`RegAddrBus] wd_i, input wire wreg_i, // 执行的结果 output reg[`RegAddrBus] wd_o, output reg wreg_o, output reg[`RegBus] wdata_o ); // 保存逻辑运算的结果 reg[`RegBus] logicout; /****************************************************************** ** 第一段:依据aluop_i指示的运算子类型进行运算,此处只有逻辑“或”运算 ** *******************************************************************/ always @ (*) begin if(rst == `RstEnable) begin logicout <= `ZeroWord; end else begin case (aluop_i) `EXE_OR_OP: begin logicout <= reg1_i | reg2_i; end default: begin logicout <= `ZeroWord; end endcase end //if end //always /**************************************************************** ** 第二段:依据alusel_i指示的运算类型,选择一个运算结果作为最终结果 ** ** 此处只有逻辑运算结果 ** *****************************************************************/ always @ (*) begin wd_o <= wd_i; // wd_o等于wd_i,要写的目的寄存器地址 wreg_o <= wreg_i; // wreg_o等于wreg_i,表示是否要写目的寄存器 case ( alusel_i ) `EXE_RES_LOGIC: begin wdata_o <= logicout; // wdata_o中存放运算结果 end default: begin wdata_o <= `ZeroWord; end endcase end endmodule
EX模块中都是组合逻辑电路,上述代码可以分为两段理解。
(1)第一段依据输入的运算子类型进行运算,这里只有一种,就是逻辑“或”运算,运算结果保存在logicout中,这个变量专门用来保存逻辑操作的结果,以后还会添加算术运算、移位运算等,届时,会定义一些新的变量保存对应的运算结果。
(2)第二段给出最终的运算结果,包括:是否要写目的寄存器wreg_o、要写的目的寄存器地址wd_o、要写入的数据wdata_o。其中wreg_o、wd_o的值都直接来自译码阶段,不需要改变,wdata_o的值要依据运算类型进行选择,如果是逻辑运算,那么将logicout的值赋给wdata_o。此处实际上是为以后扩展做准备,当添加其它类型的指令时,只需要修改这里的case情况即可。
2、EX/MEM模块
参考图4-5可知,EX模块的输出连接到EX/MEM模块,后者的作用是将执行阶段取得的运算结果,在下一个时钟传递到流水线访存阶段。其接口描述如表4-8所示。
EX/MEM模块对应的代码文件是ex_mem.v,内容如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module ex_mem( input wire clk, input wire rst, // 来自执行阶段的信息 input wire[`RegAddrBus] ex_wd, input wire ex_wreg, input wire[`RegBus] ex_wdata, // 送到访存阶段的信息 output reg[`RegAddrBus] mem_wd, output reg mem_wreg, output reg[`RegBus] mem_wdata ); always @ (posedge clk) begin if(rst == `RstEnable) begin mem_wd <= `NOPRegAddr; mem_wreg <= `WriteDisable; mem_wdata <= `ZeroWord; end else begin mem_wd <= ex_wd; mem_wreg <= ex_wreg; mem_wdata <= ex_wdata; end end endmodule
十分简单,其中只有一个时序逻辑电路,在时钟上升沿,将执行阶段的结果传递到访存阶段。
4.2.7 访存阶段的实现
现在,ori指令进入访存阶段了,但是由于ori指令不需要访问数据存储器,所以在访存阶段,不做任何事,只是简单的将执行阶段的结果向回写阶段传递即可。
流水线访存阶段包括MEM、MEM/WB两个模块。
1、MEM模块
MEM模块的接口描述如表4-9所示。
MEM模块的代码位于文件mem.v,内容如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module mem( input wire rst, // 来自执行阶段的信息 input wire[`RegAddrBus] wd_i, input wire wreg_i, input wire[`RegBus] wdata_i, // 访存阶段的结果 output reg[`RegAddrBus] wd_o, output reg wreg_o, output reg[`RegBus] wdata_o ); always @ (*) begin if(rst == `RstEnable) begin wd_o <= `NOPRegAddr; wreg_o <= `WriteDisable; wdata_o <= `ZeroWord; end else begin wd_o <= wd_i; wreg_o <= wreg_i; wdata_o <= wdata_i; end end endmodule
MEM模块中只有一个组合逻辑电路,将输入的执行阶段的结果直接作为输出,参考图4-5可知,MEM模块的输出连接到MEM/WB模块。
2、MEM/WB模块
MEM/WB模块的作用是将访存阶段的运算结果,在下一个时钟传递到回写阶段。其接口描述如表4-10所示。
MEM/WB模块的代码位于mem_wb.v文件,其主要内容如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module mem_wb( input wire clk, input wire rst, // 访存阶段的结果 input wire[`RegAddrBus] mem_wd, input wire mem_wreg, input wire[`RegBus] mem_wdata, // 送到回写阶段的信息 output reg[`RegAddrBus] wb_wd, output reg wb_wreg, output reg[`RegBus] wb_wdata ); always @ (posedge clk) begin if(rst == `RstEnable) begin wb_wd <= `NOPRegAddr; wb_wreg <= `WriteDisable; wb_wdata <= `ZeroWord; end else begin wb_wd <= mem_wd; wb_wreg <= mem_wreg; wb_wdata <= mem_wdata; end end endmodule
MEM/WB的代码与MEM模块的代码十分相似,都是将输入信号传递到对应输出端口,但是MEM/WB模块中的是时序逻辑电路,即在时钟上升沿才发生信号传递,而MEM模块中的是组合逻辑电路。MEM/WB模块将访存阶段指令是否要写目的寄存器mem_wreg、要写的目的寄存器地址mem_wd、要写入的数据mem_wdata等信息传递到回写阶段对应的接口wb_wreg、wb_wd、wb_wdata。
4.2.8 回写阶段的实现
经过上面的传递,ori指令的运算结果已经进入回写阶段了,这个阶段实际是在Regfile模块中实现的,从图4-5可知,MEM/WB模块的输出wb_wreg、wb_wd、wb_wdata连接到Regfile模块,分别连接到写使能端口we、写操作目的寄存器端口waddr、写入数据端口wdata,所以会将指令的运算结果写入目的寄存器。具体代码可以参考Regfile模块。
4.2.9 顶层模块OpenMIPS的实现
顶层模块OpenMIPS在文件openmips.v中实现,主要内容就是对上面实现的流水线各个阶段的模块进行例化、连接,连接关系就如图4-5所示。在本章实现的OpenMIPS的接口如图4-6所示,还是采用左边是输入接口,右边是输出接口的方式绘制,便于理解,各接口的说明如表4-11所示。可见与第3章的系统蓝图还有较大差距,很多接口都没有,在后续章节随着OpenMIPS实现指令的增多,会逐步完善,最终实现第3章的系统蓝图。
代码如下,可以在本书附带光盘的Code\Chapter4\目录下找到源文件。
module openmips( input wire clk, input wire rst, input wire[`RegBus] rom_data_i, output wire[`RegBus] rom_addr_o, output wire rom_ce_o ); // 连接IF/ID模块与译码阶段ID模块的变量 wire[`InstAddrBus] pc; wire[`InstAddrBus] id_pc_i; wire[`InstBus] id_inst_i; // 连接译码阶段ID模块输出与ID/EX模块的输入的变量 wire[`AluOpBus] id_aluop_o; wire[`AluSelBus] id_alusel_o; wire[`RegBus] id_reg1_o; wire[`RegBus] id_reg2_o; wire id_wreg_o; wire[`RegAddrBus] id_wd_o; // 连接ID/EX模块输出与执行阶段EX模块的输入的变量 wire[`AluOpBus] ex_aluop_i; wire[`AluSelBus] ex_alusel_i; wire[`RegBus] ex_reg1_i; wire[`RegBus] ex_reg2_i; wire ex_wreg_i; wire[`RegAddrBus] ex_wd_i; // 连接执行阶段EX模块的输出与EX/MEM模块的输入的变量 wire ex_wreg_o; wire[`RegAddrBus] ex_wd_o; wire[`RegBus] ex_wdata_o; // 连接EX/MEM模块的输出与访存阶段MEM模块的输入的变量 wire mem_wreg_i; wire[`RegAddrBus] mem_wd_i; wire[`RegBus] mem_wdata_i; // 连接访存阶段MEM模块的输出与MEM/WB模块的输入的变量 wire mem_wreg_o; wire[`RegAddrBus] mem_wd_o; wire[`RegBus] mem_wdata_o; // 连接MEM/WB模块的输出与回写阶段的输入的变量 wire wb_wreg_i; wire[`RegAddrBus] wb_wd_i; wire[`RegBus] wb_wdata_i; // 连接译码阶段ID模块与通用寄存器Regfile模块的变量 wire reg1_read; wire reg2_read; wire[`RegBus] reg1_data; wire[`RegBus] reg2_data; wire[`RegAddrBus] reg1_addr; wire[`RegAddrBus] reg2_addr; // pc_reg例化 pc_reg pc_reg0( .clk(clk), .rst(rst), .pc(pc), .ce(rom_ce_o) ); assign rom_addr_o = pc; // 指令存储器的输入地址就是pc的值 // IF/ID模块例化 if_id if_id0( .clk(clk), .rst(rst), .if_pc(pc), .if_inst(rom_data_i), .id_pc(id_pc_i), .id_inst(id_inst_i) ); // 译码阶段ID模块例化 id id0( .rst(rst), .pc_i(id_pc_i), .inst_i(id_inst_i), // 来自Regfile模块的输入 .reg1_data_i(reg1_data), .reg2_data_i(reg2_data), // 送到regfile模块的信息 .reg1_read_o(reg1_read), .reg2_read_o(reg2_read), .reg1_addr_o(reg1_addr), .reg2_addr_o(reg2_addr), // 送到ID/EX模块的信息 .aluop_o(id_aluop_o), .alusel_o(id_alusel_o), .reg1_o(id_reg1_o), .reg2_o(id_reg2_o), .wd_o(id_wd_o), .wreg_o(id_wreg_o) ); // 通用寄存器Regfile模块例化 regfile regfile1( .clk (clk), .rst (rst), .we(wb_wreg_i), .waddr(wb_wd_i), .wdata(wb_wdata_i), .re1(reg1_read), .raddr1(reg1_addr), .rdata1(reg1_data), .re2(reg2_read), .raddr2(reg2_addr), .rdata2(reg2_data) ); // ID/EX模块例化 id_ex id_ex0( .clk(clk), .rst(rst), // 从译码阶段ID模块传递过来的信息 .id_aluop(id_aluop_o), .id_alusel(id_alusel_o), .id_reg1(id_reg1_o), .id_reg2(id_reg2_o), .id_wd(id_wd_o), .id_wreg(id_wreg_o), // 传递到执行阶段EX模块的信息 .ex_aluop(ex_aluop_i), .ex_alusel(ex_alusel_i), .ex_reg1(ex_reg1_i), .ex_reg2(ex_reg2_i), .ex_wd(ex_wd_i), .ex_wreg(ex_wreg_i) ); // EX模块例化 ex ex0( .rst(rst), // 从ID/EX模块传递过来的的信息 .aluop_i(ex_aluop_i), .alusel_i(ex_alusel_i), .reg1_i(ex_reg1_i), .reg2_i(ex_reg2_i), .wd_i(ex_wd_i), .wreg_i(ex_wreg_i), //输出到EX/MEM模块的信息 .wd_o(ex_wd_o), .wreg_o(ex_wreg_o), .wdata_o(ex_wdata_o) ); // EX/MEM模块例化 ex_mem ex_mem0( .clk(clk), .rst(rst), // 来自执行阶段EX模块的信息 .ex_wd(ex_wd_o), .ex_wreg(ex_wreg_o), .ex_wdata(ex_wdata_o), // 送到访存阶段MEM模块的信息 .mem_wd(mem_wd_i), .mem_wreg(mem_wreg_i), .mem_wdata(mem_wdata_i) ); // MEM模块例化 mem mem0( .rst(rst), // 来自EX/MEM模块的信息 .wd_i(mem_wd_i), .wreg_i(mem_wreg_i), .wdata_i(mem_wdata_i), // 送到MEM/WB模块的信息 .wd_o(mem_wd_o), .wreg_o(mem_wreg_o), .wdata_o(mem_wdata_o) ); // MEM/WB模块例化 mem_wb mem_wb0( .clk(clk), .rst(rst), // 来自访存阶段MEM模块的信息 .mem_wd(mem_wd_o), .mem_wreg(mem_wreg_o), .mem_wdata(mem_wdata_o), // 送到回写阶段的信息 .wb_wd(wb_wd_i), .wb_wreg(wb_wreg_i), .wb_wdata(wb_wdata_i) ); endmodule
至此,ori指令的流水线之旅已经结束了,一个原始而简单的五级流水线结构也已经建立了,有读者可能会怀疑区区百十行代码就实现了流水线,是不是太简单了?有这样的怀疑是正常的,的确很简单,但是简单并不代表简陋,不代表错误,流水线实际并没有大家想的那么复杂,下一节,将验证本节实现的流水线能不能正确工作,能不能正确执行ori指令。
好了,第一条指令就实现了,五级流水线也初步建立了,下一次将进行仿真验证,未完待续!
自己动手写处理器之第四阶段(1)——第一条指令ori的实现