逻辑,移位操作与空指令的添加
综述
文章引于自己动手写CPU之第五阶段(3)——MIPS指令集中的逻辑、移位与空指令
指令描述参考与博文[BOOK],感谢博主的分享!!!
由于那个指令具有不同的指令格式,但一共就只有三种,因此这里将其进行区分,进行译码以及执行相关代码需要进行修改。这里笔者直接加入的指令,具体的指令作用在书中或者博文中都很明确,有需要可以自行翻阅查看。
照例笔者会将自己对于代码的理解以及相关的必要知识记录总结在对应的目录下
ID模块的修改
首先是要确定指令的类型,这里对对应的指令进行区分,使用的方式是首先判断op,然后若是op下有对应的指令,依次同理进行判断。注意这里有一个例外情况就是使用inst_i[31:21]
进行判断,这里是单独分开进行判断的。具体见代码。由于具体的指令,使用的寄存器数量不同,对于每一个数据的处理也是不同的,具体可以在书中找到对应的操作,然后对应代码中查看。
`timescale 1ns / 1ps
`include "defines.v"
module id(
input wire rst ,
input wire [`InstAddrBus] pc_i ,
input wire [`InstBus] inst_i ,
//读取regfile的值
input wire [`InstBus] reg1_data_i ,
input wire [`InstBus] 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 ,
//处于执行阶段的指令运行的运算结果
input wire [`RegAddrBus] ex_wd_i , //执行阶段的目的寄存器地址
input wire ex_wreg_i , //是否要写入数据标志
input wire [`RegBus] ex_wdata_i , //执行阶段送到访存的数据
//处于访存阶段的指令运行的运算结果
input wire [`RegAddrBus] mem_wd_i ,
input wire mem_wreg_i ,
input wire [`RegBus] mem_wdata_i
);
//取得指令的指令码,功能码
//用于ori指令只需要判断21-31 bit的值,即可判断是否是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_SPECIAL_INST :
begin
case(op2)
5'b0000_0:
begin
case(op3)
`EXE_OR:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_OR_OP;
alusel_o <= `EXE_RES_LOGIC;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_AND:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_AND_OP;
alusel_o <= `EXE_RES_LOGIC;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_XOR:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_XOR_OP;
alusel_o <= `EXE_RES_LOGIC;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_NOR:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_NOR_OP;
alusel_o <= `EXE_RES_LOGIC;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_SLLV:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_SLL_OP;
alusel_o <= `EXE_RES_SHIFT;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_SRLV:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_SRL_OP;
alusel_o <= `EXE_RES_SHIFT;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_SRAV:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_SRA_OP;
alusel_o <= `EXE_RES_SHIFT;
reg1_read_o <= 1'b1;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
`EXE_SYNC:
begin
wreg_o <= `WriteEnable;
aluop_o <= `EXE_NOP_OP;
alusel_o <= `EXE_RES_SHIFT;
reg1_read_o <= 1'b0;
reg2_read_o <= 1'b1;
instvalid <= `InstValid;
end
default:
begin
end
endcase
end
default:
begin
end
endcase
end
`EXE_ORI: //判断op的值是进行opi指令
begin
wreg_o <= `WriteEnable ; //ori 指令需要将结果写入目的寄存器,所以wreg_o 为 WriteEnable
aluop_o <= `EXE_OR_OP ; //运算的子类型是逻辑“或”运算
alusel_o <= `EXE_RES_LOGIC ; //运算类型是逻辑运算
reg1_read_o <= 1'b1 ; //需要通过Regfile的读端口1读取寄存器
reg2_read_o <= 1'b0 ; //不需要通过Regfile的读端口2读取寄存器
imm <= {16'h0,inst_i[15:0]} ; //指令执行需要的立即数
wd_o <= inst_i[20:16] ; //执行指令要写入的目的寄存器地址
instvalid <= `InstValid ; //ori指令是有效的
end
`EXE_ANDI:
begin
wreg_o <= `WriteEnable ;
aluop_o <= `EXE_AND_OP ;
alusel_o <= `EXE_RES_LOGIC ;
reg1_read_o <= 1'b1 ;
reg2_read_o <= 1'b0 ;
imm <= {16'h0,inst_i[15:0]} ;
wd_o <= inst_i[20:16] ;
instvalid <= `InstValid ;
end
`EXE_XORI:
begin
wreg_o <= `WriteEnable ;
aluop_o <= `EXE_XOR_OP ;
alusel_o <= `EXE_RES_LOGIC ;
reg1_read_o <= 1'b1 ;
reg2_read_o <= 1'b0 ;
imm <= {16'h0,inst_i[15:0]} ;
wd_o <= inst_i[20:16] ;
instvalid <= `InstValid ;
end
`EXE_LUI:
begin
wreg_o <= `WriteEnable ;
aluop_o <= `EXE_OR_OP ;
alusel_o <= `EXE_RES_LOGIC ;
reg1_read_o <= 1'b1 ;
reg2_read_o <= 1'b0 ;
imm <= {inst_i[15:0],16'h0 } ;
wd_o <= inst_i[20:16] ;
instvalid <= `InstValid ;
end
`EXE_PREF:
begin
wreg_o <= `WriteDisable ;
aluop_o <= `EXE_NOP_OP ;
alusel_o <= `EXE_RES_NOP ;
reg1_read_o <= 1'b0 ;
reg2_read_o <= 1'b0 ;
instvalid <= `InstValid ;
end
default:
begin
end
endcase
if(inst_i[31:21] == 11'b0000_0000_000)
begin
if(op3 == `EXE_SLL)
begin
wreg_o <= `WriteEnable ;
aluop_o <= `EXE_SLL_OP ;
alusel_o <= `EXE_RES_SHIFT ;
reg1_read_o <= 1'b0 ;
reg2_read_o <= 1'b1 ;
imm[4:0] <= inst_i[10:6] ;
wd_o <= inst_i[15:11] ;
instvalid <= `InstValid ;
end
else
if(op3 == `EXE_SRL)
begin
wreg_o <= `WriteEnable ;
aluop_o <= `EXE_SRL_OP ;
alusel_o <= `EXE_RES_SHIFT ;
reg1_read_o <= 1'b0 ;
reg2_read_o <= 1'b1 ;
imm[4:0] <= inst_i[10:6] ;
wd_o <= inst_i[15:11] ;
instvalid <= `InstValid ;
end
else
if(op3 == `EXE_SRA)
begin
wreg_o <= `WriteEnable ;
aluop_o <= `EXE_SRA_OP ;
alusel_o <= `EXE_RES_SHIFT ;
reg1_read_o <= 1'b0 ;
reg2_read_o <= 1'b1 ;
imm[4:0] <= inst_i[10:6] ;
wd_o <= inst_i[15:11] ;
instvalid <= `InstValid ;
end
end
end
end
/*********************************************************************************
*************************** 第二阶段:确定进行运算的源操作数1***************************
*********************************************************************************/
//regfile读端口1的输出值
always @ (*)
begin
if(rst == `RstEnable)
reg1_o <= `ZeroWord;
else
// 对于源操作数,若是目前端口的读取得寄存器数据地址是 执行阶段的要写入的目的寄存器 ,那么直接将执行的结果作为reg1_o的值。
//这个数相当于是要写入的数据
if((reg1_read_o == 1'b1) && (ex_wreg_i == 1'b1) && (ex_wd_i == reg1_addr_o) )
reg1_o <= ex_wdata_i;
else
//对于要是目的寄存器,我们要读取的寄存器其实是最终访存要写入的寄存器,那么访存的数据就直接作为源操作数进行处理
if((reg1_read_o == 1'b1) && (mem_wreg_i == 1'b1) && (mem_wd_i == reg1_addr_o) )
reg1_o <= mem_wdata_i ;
else
if(reg1_read_o == 1'b1)
reg1_o <= reg1_data_i;
else
if(reg1_read_o == 1'b0)
reg1_o <= imm; //立即数
else
reg1_o <= `ZeroWord;
end
/*********************************************************************************
*************************** 第三阶段:确定进行运算的源操作数2***************************
*********************************************************************************/
//regfile读端口2的输出值
always @ (*)
begin
if(rst == `RstEnable)
reg2_o <= `ZeroWord;
else
// 对于源操作数,若是目前端口的读取得寄存器数据地址是 执行阶段的要写入的目的寄存器 ,那么直接将执行的结果作为reg2_o的值。
//这个数相当于是要写入的数据
if((reg2_read_o == 1'b1) && (ex_wreg_i == 1'b1) && (ex_wd_i == reg2_addr_o) )
reg2_o <= ex_wdata_i;
else
//对于要是目的寄存器,我们要读取的寄存器其实是最终访存要写入的寄存器,那么访存的数据就直接作为源操作数进行处理
if((reg2_read_o == 1'b1) && (mem_wreg_i == 1'b1) && (mem_wd_i == reg2_addr_o) )
reg2_o <= mem_wdata_i ;
else
if(reg2_read_o == 1'b1)
reg2_o <= reg2_data_i;
else
if(reg2_read_o == 1'b0)
reg2_o <= imm; //立即数
else
reg2_o <= `ZeroWord;
end
endmodule
EX模块的修改
执行阶段对于不同数,以及不同的信息进行处理。新建寄存器用于保存移位寄存器的结果,根据指令类型对象对相应的数据进行输出。
`timescale 1ns / 1ps
`include "defines.v"
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 ;
//保存位移运算结果
reg [`RegBus] shiftres ;
// 移动操作的结果
reg [`RegBus] moveres ;
/*********************************************************************************
*************** 依据aluop_i指示的运算子类型进行运算,
*********************************************************************************/
always @ (*)
begin
if( rst == `RstEnable)
logicout <= `ZeroWord;
else
begin
case(aluop_i)
`EXE_OR_OP:
begin
logicout <= reg1_i | reg2_i;
end
`EXE_AND_OP:
begin
logicout <= reg1_i & reg2_i;
end
`EXE_NOR_OP: //逻辑或与非
begin
logicout <= ~(reg1_i | reg2_i);
end
`EXE_XOR_OP:
begin
logicout <= reg1_i ^ reg2_i;
end
default:logicout <= `ZeroWord;
endcase;
end
end
always @ (*)
begin
if(rst == `RstEnable)
shiftres <= `ZeroWord;
else
case(aluop_i)
`EXE_SLL_OP: //逻辑左移
shiftres <= reg2_i << reg1_i[4:0];
`EXE_SRL_OP:
shiftres <= reg2_i >> reg1_i[4:0];
`EXE_SRA_OP:
shiftres <= ( {32{ reg2_i[31]} } << (6'd32 - {1'b0,reg1_i[4:0] } ) ) | reg2_i >> reg1_i[4:0];
default:
begin
shiftres <= `ZeroWord;
end
endcase
end
/*********************************************************************************
*************** 第二阶段:依据alusel_i指示的运算类型,确定wdata_o的值
*********************************************************************************/
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
`EXE_RES_SHIFT:
begin
wdata_o <= shiftres; //wdata_o中存放位移运算运算结果
end
`EXE_RES_MOVE:
begin
wdata_o <= moveres; //指令为EXE_RES_MOVE
end
default:
wdata_o <= `ZeroWord;
endcase
end
endmodule
仿真验证
测试逻辑操作
首先分析测试数据。OpenMIPS将指令转换为ori指令来执行。lui指令使用立即数(扩展后的立即数)做或运算。得到0x01010000
之后进行两次或运算,将数据分别写入寄存器1与寄存器2中。
接下来是使用寄存器中的数据进行或运算,将结果送入1寄存器。
然后使用立即数与寄存器中的数据进行与运算,结果送入寄存器3,在使用寄存器1与寄存器3中的数据相与结果送寄存器1,然后使用寄寄存器1中的数据与立即数相异或,之后分别使用寄存器中的数据进行两次运算即可。
I-型指令
lui
lui: 把立即数加载到寄存器高位。
ori
逐位逻辑操作指令
andi
逐位逻辑操作指令
xori
逐位逻辑操作指令
异或运算指令使用方法为:xori rt, rs, immediate
指令作用为:rt <- rs XOR zero_extended(immediate)。将地址为rs的通用寄存器的值。与指令中马上数进行零扩展后的值进行逻辑“异或”运算,运算结果保存到地址为rt的通用寄存器中。
xor&nor
逐位逻辑操作指令
R-型指令
or
逐位逻辑操作指令
and
逐位逻辑操作指令
得到的数据与书中预期一致。
测试移位操作与空指令
这里笔者不一一分析,这里指出一个问题,是在第六行的空指令,加上会多一个时钟周期,不加会少一个时钟周期。自己看仿真图即可理解。这里依据给出的源文件,期间分析应该有五个时钟周期的延时。
简单分析,在复位后,第五个时钟上升沿,寄存器2开始有数据,然后更新,之后的四个时钟周期,对寄存器1,5,8进行操作,因此加上其本身,一个维持五个时钟周期。书中是维持了四个时钟周期,是不对的。
不加时候的仿真截图
加上之后的
这里书中给出的是四个时钟周期的延时。这里上边已经进行了分析。
移位类指令
sll
当功能码是6’b000000,表示是sll指令,逻辑左移
指令使用方法为:sll rd, rt, sa
指令作用为:rd <- rt << sa (logic),将地址为rt的通用寄存器的值,向左移sa位。空出来的位置使用0填充。结果保存到地址为rd的通用寄存器中。
srl
当功能码是6’b000010,表示是srl指令。逻辑右移
指令使用方法为:srl rd, rt, sa
指令作用为:rd <- rt >> sa (logic),将地址为rt的通用寄存器的值,向右移sa位,空出来的位置使用0填充,结果保存到地址为rd的通用寄存器中。
sra
当功能码是6’b000011。表示是sra指令,算术右移
指令使用方法为:sra rd, rt, sa
指令作用为:rd <- rt >> sa (arithmetic),将地址为rt的通用寄存器的值,向右移sa位。空出来的位置使用rt[31]的值填充,结果保存到地址为rd的通用寄存器中。
sllv
当功能码是6’b000100。表示是sllv指令,逻辑左移
指令使用方法为:sllv rd, rt, rs
指令作用为:rd <- rt << rs[4:0](logic)。将地址为rt的通用寄存器的值。向左移位,空出来的位置使用0填充,结果保存到地址为rd的通用寄存器中。移位位数由地址为rs的寄存器值的0-4bit确定。
srlv
当功能码是6’b000110,表示是srlv指令。逻辑右移
指令使用方法为:srlv rd, rt, rs
指令作用为:rd <- rt >> rs[4:0](logic),将地址为rt的通用寄存器的值,向右移位,空出来的位置使用0填充。结果保存到地址为rd的通用寄存器中。
移位位数由地址为rs的寄存器值的0-4bit确定。
srav
当功能码是6’b000111,表示是srav指令,算术右移
指令使用方法为:srav rd, rt, rs
指令作用为:rd <- rt >> rs[4:0](arithmetic),将地址为rt的通用寄存器的值,向右移位。空出来的位置使用rt[31]填充,结果保存到地址为rd的通用寄存器中。
移位位数由地址为rs的寄存器值的0-4bit确定。
总结来说。这六条移位操作指令能够分为两种情况:sllv、srav、srlv这3条指令的助记符最后有“v”。表示移位位数是通过寄存器的值确定的,sll、sra、srl这3条指令的助记符最后没有“v”,表示移位位数就是指令中6-10bit的sa的值。
ssnop、nop
空操作:
nop:相当于 sll zero,zero,o,
ssnop: equals sll zero,zero,1. 这个指令不得与其它指令同时发送,这样就保证了其运行要花费至少一个时钟周期。这在简单的流水线的CPU上无关紧要,但在复杂些的实现上对于实现强制的延时很有用。
另外,MIPS32指令集架构中还定义了sync、pref这2条指令,当中sync指令用于保证载入、存储操作的顺序,对于OpenMIPS而言,是严格依照指令顺序运行的,载入、存储操作也是依照顺序进行的,所以能够将sync指令当作nop指令处理,在这里将其归纳为空指令。pref指令用于缓存预取,OpenMIPS没有实现缓存,所以也能够将pref指令当作nop指令处理,此处也将其归纳为空指令。