自己动手写处理器之第四阶段(1)——第一条指令ori的实现

将陆续上传本人写的新书《自己动手写处理器》(尚未出版),今天是第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的实现

时间: 2024-10-01 06:36:20

自己动手写处理器之第四阶段(1)——第一条指令ori的实现的相关文章

自己动手写CPU之第四阶段(2)——验证第一条指令ori的实现效果

将陆续上传本人写的新书<自己动手写CPU>(尚未出版),今天是第12篇,我尽量每周四篇 书名又之前的<自己动手写处理器>改为<自己动手写CPU> 4.3 验证OpenMIPS实现效果 4.3.1指令存储器ROM的实现 本节将验证我们的OpenMIPS是否实现正确,包含:流水线是否正确.ori指令是否实现正确.在验证之前,需要首先实现指令存储器,以便OpenMIPS从中读取指令. 指令存储器模块是只读的,其接口如图4-7所示,还是采用左边是输入接口,右边是输出接口的方式绘

自己动手写CPU之第四阶段(4)——Makefile文件建立

将陆续上传本人写的新书<自己动手写CPU>(尚未出版),今天是第14篇,我尽量每周四篇 4.4.6 编写Makefile文件 为了得到指令存储器初始化文件,我们需要输入4条命令,有点麻烦,最好只输入一条命令就可以了,这需要使用到Makefile文件.在汇编程序inst_rom.S所在目录下新建一个Document,文件名为Makefile,内容如下. ifndef CROSS_COMPILE CROSS_COMPILE = mips-sde-elf- endif CC = $(CROSS_CO

自己动手写处理器之第三阶段——教学版OpenMIPS处理器蓝图

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第十篇,我尽量每周四篇 从本章开始将一步一步地实现教学版OpenMIPS处理器.本章给出了教学版OpenMIPS的系统蓝图,首先介绍了系统的设计目标,其中详细说明了OpenMIPS处理器计划实现的5级流水线.3.2节给出了OpenMIPS处理器的接口示意图,及各个接口的作用.3.3节简单解释了各个源代码文件的作用.最后描述了OpenMIPS处理器的实现方法,读者将发现本书给出的实现方法与现有书籍的方法完全不同,更加易于理解.便于实践

自己动手写CPU之第四阶段(3)——MIPS编译环境的建立

将陆续上传本人写的新书<自己动手写CPU>(尚未出版).今天是第13篇.我尽量每周四篇 4.4 MIPS编译环境的建立 OpenMIPS处理器在设计的时候就计划与MIPS32指令集架构兼容,所以能够使用MIPS32架构下已有的GNU开发工具链.本节将说明怎样安装使用GNU开发工具链以及怎样制作Makefile文件.从而以更加方便.快捷.自己主动的方式对測试程序进行编译.并得到指令存储器ROM的初始化文件inst_rom.data. 4.4.1 VisualBox的安装与设置 GNU工具链要安装

自己动手写处理器之第一阶段(3)——MIPS32指令集架构简介

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第四篇,我尽量每周四篇 1.4 MIPS32指令集架构简介 本书设计的处理器遵循MIPS32 Release 1架构,所以本节介绍的MIPS32指令集架构指的就是MIPS32 Release 1. 1.4.1 数据类型 指令的主要任务就是对操作数进行运算,操作数有不同的类型和长度,MIPS32提供的基本数据类型如下. 位(b):长度是1bit. 字节(Byte):长度是8bit. 半字(Half Word):长度是16bit. 字(

自己动手写处理器之第一阶段(2)——MIPS指令集架构的演变

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第三篇,我尽量每周四篇 MIPS指令集架构自上世纪80年代出现后,一直在进行着更新换代,从最初的MIPS I到MIPS V,发展到可支持扩展模块的MIPS32.MIPS64系列,再到集成代码压缩技术的microMIPS32.microMIPS64.每个MIPS ISA都是其前一个的超集,没有任何遗漏,只有增加新的功能.       1.MIPS Ⅰ 提供加载/存储.计算.跳转.分支.协处理及其它特殊指令.该指令集架构用于最初的MIP

自己动手写处理器之第一阶段(1)——计算机的简单模型、架构、指令集

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第二篇,我尽量每周四篇 第1章 处理器与MIPS 时间开始了! --胡风 · 1949 让我们以一句诗意的话,开始本书的阅读. 时间从1971年11月15日开始,那一天,Intel发布了世界上第一款单芯片微处理器4004. 1.1 计算机的简单模型 计算机很复杂,可以听歌.看电影.上网.玩游戏,内部是怎么工作的,这个问题太可怕了,太复杂了. 计算机很简单,只有加.减.乘.除.逻辑.移位.转移.存储.加载等几类可以做的操作,太简单了.

自己动手写处理器之第二阶段(1)——可编程逻辑器件与PLD电路设计流程

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第五篇,我尽量每周四篇 通过上一章的介绍,读者应该知道CPU内部有一些基本的电路,比如:译码电路.运算电路.控制电路,此外还有一些寄存器等.这些电路怎么实现呢?当然可以通过一大堆分立的元器件实现,实际上在2008年,美国加州的游戏开发人士Steve Chamberlin就自己制造了一款8位CPU,耗时18个月,花费1000美元,总共使用了1253条线缆,如图2-1所示,Steve Chamberlin为它起了一个十分贴切的名字--B

自己动手写处理器之第二阶段(3)——Verilog HDL行为语句

将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第七篇,我尽量每周四篇 2.6 Verilog HDL行为语句 2.6.1 过程语句 Verilog定义的模块一般包括有过程语句,过程语句有两种:initial.always.其中initial常用于仿真中的初始化,其中的语句只执行一次,而always中语句则是不断重复执行的.此外,always过程语句是可综合的,initial过程语句是不可综合的.       1.always过程语句 always过程语句的格式如图2-10所示.