将陆续上传本人写的新书《自己动手写处理器》(尚未出版),今天是第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所看到的。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpc2hhbmd3ZW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
在流水线结构中。信号在寄存器之间传递,每传递到一级都会引起对应的组合逻辑电路变化。对这样的模型进行抽象描写叙述就是寄存器传输级(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程序文件名称。
本节接下来将分别实现图中各个模块。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpc2hhbmd3ZW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
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所看到的。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpc2hhbmd3ZW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
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, // 写port
input wire we,
input wire[`RegAddrBus] waddr,
input wire[`RegBus] wdata, // 读port1
input wire re1,
input wire[`RegAddrBus] raddr1,
output reg[`RegBus] rdata1, // 读port2
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 /****************************************************************
*********** 第三段:读port1的读操作 *********
*****************************************************************/ 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 /****************************************************************
*********** 第四段:读port2的读操作 *********
*****************************************************************/ 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)第三段:实现了第一个读寄存器port,分下面几步依次推断。
- 当复位信号有效时。第一个读寄存器port的输出始终为0
- 当复位信号无效时,假设读取的是$0,那么直接给出0
- 假设第一个读寄存器port要读取的目标寄存器与要写入的目的寄存器是同一个寄存器。那么直接将要写入的值作为第一个读寄存器port的输出
- 上述情况都不满足。那么给出第一个读寄存器port要读取的目标寄存器地址相应寄存器的值
- 第一个读寄存器port没有使能时,直接输出0
(4)第四段:实现了第二个读寄存器port,详细过程与第三段是相似的,不再反复解释。
注意一点:读寄存器操作是组合逻辑电路,也就是一旦输入的要读取的寄存器地址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读port1读取的寄存器地址
reg2_addr_o <= inst_i[20:16]; // 默认通过Regfile读port2读取的寄存器地址
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的读port1读取寄存器
reg1_read_o <= 1'b1; // 不须要通过Regfile的读port2读取寄存器
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读port1的输出值
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读port2的输出值
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读port1读取的寄存器地址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读port2读取寄存器,这里暗含使用马上数作为运算的操作数。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模块读port1读取的寄存器的值作为源操作数1,假设reg1_read_o为0,那么就将马上数作为源操作数1,对于ori而言,此处选择从Regfile模块读port1读取的寄存器的值作为源操作数1。该值将通过reg1_oport被传递到运行阶段。
(3)第三段:给出參与运算的源操作数2的值,假设reg2_read_o为1,那么就将从Regfile模块读port2读取的寄存器的值作为源操作数2,假设reg2_read_o为0,那么就将马上数作为源操作数2。对于ori而言,此处选择马上数imm作为源操作数2。
该值将通过reg2_oport被传递到运行阶段。
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模块的port连接关系可知,EX模块会从ID/EX模块得到运算类型alusel_i、运算子类型aluop_i、源操作数reg1_i、源操作数reg2_i、要写的目的寄存器地址wd_i。EX模块会根据这些数据进行运算,其接口描写叙述如表4-7所看到的。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpc2hhbmd3ZW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
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所看到的。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpc2hhbmd3ZW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
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模块的代码十分相似,都是将输入信号传递到相应输出port,可是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模块,分别连接到写使能portwe、写操作目的寄存器portwaddr、写入数据portwdata,所以会将指令的运算结果写入目的寄存器。详细代码能够參考Regfile模块。
4.2.9 顶层模块OpenMIPS的实现
顶层模块OpenMIPS在文件openmips.v中实现,主要内容就是对上面实现的流水线各个阶段的模块进行例化、连接。连接关系就如图4-5所看到的。在本章实现的OpenMIPS的接口如图4-6所看到的,还是採用左边是输入接口。右边是输出接口的方式绘制,便于理解,各接口的说明如表4-11所看到的。
可见与第3章的系统蓝图还有较大差距。非常多接口都没有,在兴许章节随着OpenMIPS实现指令的增多,会逐步完好,终于实现第3章的系统蓝图。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGVpc2hhbmd3ZW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">
代码例如以下,能够在本书附带光盘的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指令的流水线之旅已经结束了,一个原始而简单的五级流水线结构也已经建立了,有读者可能会怀疑区区百十行代码就实现了流水线。是不是太简单了?有这种怀疑是正常的,的确非常easy,可是简单并不代表简陋,不代表错误,流水线实际并没有大家想的那么复杂,下一节。将验证本节实现的流水线能不能正确工作。能不能正确运行ori指令。
好了,第一条指令就实现了。五级流水线也初步建立了,下一次将进行仿真验证。未完待续!