自己动手写CPU(7)转移指令的实现
分支延迟槽
在MIPS五级流水线中,一条指令被分成了五个阶段:取指、译指、执行、仿存、回写。当第一条指令的执行阶段结束时,第二条指令的译指阶段也已经结束了。
那么如果第一条指令是分支跳转指令,那么在执行阶段才会知道要不要跳转以及跳转的目标指令地址是多少。而此时第二条指令已结束译指阶段,第三条指令已结束取指阶段。如果这个时候CPU直接跳转到目标指令地址去执行,那么就需要清空现有流水线,从新的指令地址开始取指、译指。这是因为分支跳转后面的指令不能被执行啊,程序已经跳转了,也就相当于原先第二条指令的取指和译指操作,第三条指令的取指操作,这些CPU已经做过的工作,都白做了。因为CPU此时是从的新的地址重新开始的!这叫流水线“冒泡”。
但做上述的工作也是需要耗费CPU时间的,MIPS设计者们觉得十分浪费,所以发明了“分支延迟槽”的东西。
但是,即使引入延迟槽,在转移发生时仍然会导致已经进入取指阶段的指令无效,也就是说,仍浪费一个时钟周期,要解决这个问题,可以在译码阶段进行转移判断,这样就可以避免浪费时钟周期。OpenMIPS处理器就设计为在译码阶段进行转移判断。
需注意的几件事情:
- “分支延迟槽”里面的指令,在目标跳转指令前面执行,所以**“分支延迟槽”里面的指令不能修改目标跳转指令会用到的寄存器或者变量的内容**,否则程序很容易搞错。
- “分支延迟槽”里面的指令,通常可以被加以利用会做一些比较意义的事情,例如清零内存之类的。
指令介绍
- 跳转指令:jr、jalr、j、jar
- 分支指令b、bal、beq、bgez、bgezal、bgtz、blez、bltz、bltzal、bne
跳转指令
指令格式
-
jr(功能码为6’b001000):用法:jr rs,作用:pc <- rs 将地址为rs的通用寄存器的值赋给寄存器PC,作为新的指令地址
-
jalr(功能码为6’b001001):用法:jalr rs 或者 jalr rd,rs 作用:rd <- return_address,pc <- rs,将地址为rs的通用寄存器的值赋给寄存器PC,作为新的指令地址,同时将跳转指令后面第2条指令的地址作为返回地址保存到地址为rd的通用寄存器,如果没有在指令中指明rd,那么默认将返回地址保存到寄存器$31
-
j(指令码为6’b000010):用法:j target,作用:pc <- (pc+4)[31,28]||target||‘00’,转移到新的指令地址,其中新地址的低28位是target左移两位后的值,新指令地址高4位是后一指令的高四位
因为处理器按照字节寻址,二指令存储器每个地址是一个32bit字,所以要给指令中的立即数乘4,即左移两位
-
jal(指令码为6’b000011):用法:jal target,作用:pc <- (pc+4)[31,28]||target||‘00’,转移到新的指令地址,其中新地址的低28位是target左移两位后的值,新指令地址高4位是后一指令的高四位,jal指令要将跳转指令后面的一条指令地址(pc+4)写入$31寄存器
分支指令
指令格式
-
由指令格式可以看出:
beq、b、bgtz、blez、bne这5条指令可以直接依据指令中的指令码进行判断是哪一条指令,bltz、bltzal、bgez、bgezal、bal这5条指令指令码相同,依据指令中16~20bit的值进一步判断是哪一条指令
所有分支指令的第0~15bit存储的都是offset,如果发生转移,那么将offset左移2位,并符号扩展至32位
转移目标地址 = (signed_extend)(offset||‘00’)+(pc+4)
-
beq(指令码为6’b000100):用法:beq rs,rt,offset,作用:if rs = rt then branch,将地址为rs的通用寄存器的值与地址为rt的通用寄存器的值进行比较,如果相等,则发生转移
-
b(指令码为6’b000100,且16~25bit为0):用法:b offset,作用:无条件转移,即beq指令的rs,rt都为0时的情况,实现时不需要特意实现b指令,只需要实现beq即可
-
bgtz(指令码为6’b000111):用法:bgtz rs,offset,作用:if rs > 0 then branch
-
blez(指令码6’b000110):用法:blez rs,offset,作用:if rs <= 0 then branch
-
bne(指令码6’b000101):用法:bne rs,rt,offset,作用:if rs != rt then branch
-
bltz(指令码为REGIMM,且第16~20bit为5’b00000):用法:bltz rs,offset,作用:if rs < 0 then branch
-
bltzal(指令码为REGIMM,且第16~20bit为5’b10000):用法:bltzal rs,offset,作用:if rs < 0 then branch,并且将指令后面的指令地址作为返回地址,保存到通用寄存器$31
-
bgez(指令码为REGIMM,且第16~20bit为5’b00001):用法:bgez rs,offset,作用:if rs >= 0 then branch
-
bgezal(指令码为REGIMM,且第16~20bit为5’b10001):用法:bgezal rs,offset,作用:if rs >= 0 then branch,并且将指令后面的指令地址作为返回地址,保存到通用寄存器$31
-
bal(指令码为REGIMM,且第2125bit为0,第1620bit为5’b10001):用法:bal offset,作用:无条件转移,并且将指令后面的指令地址作为返回地址,保存到通用寄存器$31,bal是bgezal指令的特殊情况,即bgezal指令的rs为0,不用特意实现这个指令
调整系统结构
- 如果处于译码阶段的指令是转移指令,并且满足转移条件,那么ID模块设置转移发生标志 branch_flag_o为 Branch,同时通过branch_target_address_o接口给出转移目的地址,送到PC模块,后者据此修改取指地址。
- 如果处于译码阶段的指令是转移指令,并且满足转移条件,那么ID模块还会设置next_inst_in_delayslot_o为 InDelaySlot,表示下一条指令是延迟槽指令,其中 InDelaySlot是一个宏定义。next_inst _in_delayslot_o信号会送入ID/EX模块,并在下一个时钟周期通过ID/EX模块的is_in_delayslot_o接口送回到ID模块,ID模块可以据此判断当前处于译码阶段的指令是否是延迟槽指令。
- 如果转移指令需要保存返回地址,那么ID模块还要计算返回地址,并通过link_addr_o接口输出,该值最终会传递到EX模块,作为要写入目的寄存器的值。
代码修改
1、增加宏定义
`define Branch 1'b1 //发生转移
`define NotBranch 1'b0 //不发生转移
`define InDelaySlot 1'b1 //是延迟槽指令
`define NotInDelaySlot 1'b0 //不是延迟槽指令
`define EXE_J 6'b000010 //指令J的功能码
`define EXE_JAL 6'b000011 //指令JAL的功能码
`define EXE_JALR 6'b001001 //指令JALR的功能码
`define EXE_JR 6'b001000 //指令JR的功能码
`define EXE_BEQ 6'b000100 //指令BEQ的指令码
`define EXE_BGEZ 5'b00001 //指令BGEZ第16~20bit
`define EXE_BGEZAL 5'b10001 //指令BGEZAL第16~20bit
`define EXE_BGTZ 6'b000111 //指令BGTZ的指令码
`define EXE_BLEZ 6'b000110 //指令BLEZ的指令码
`define EXE_BLTZ 5'b00000 //指令BLTZ第16~20bit
`define EXE_BLTZAL 5'b10000 //指令BLTZAL第16~20bit
`define EXE_BNE 6'b000101 //指令BNE的指令码
`define EXE_REGIMM_INST 6'b000001 //REGIMM类的指令码
`define EXE_J_OP 8'b01001111
`define EXE_JAL_OP 8'b01010000
`define EXE_JALR_OP 8'b00001001
`define EXE_JR_OP 8'b00001000
`define EXE_BEQ_OP 8'b01010001
`define EXE_BGEZ_OP 8'b01000001
`define EXE_BGEZAL_OP 8'b01001011
`define EXE_BGTZ_OP 8'b01010100
`define EXE_BLEZ_OP 8'b01010011
`define EXE_BLTZ_OP 8'b01000000
`define EXE_BLTZAL_OP 8'b01001010
`define EXE_BNE_OP 8'b01010010
`define EXE_RES_JUMP_BRANCH 3'b110
2、修改取指阶段的PC模块
修改取指阶段的PC模块如下,主要修改一点:如果 branch_flag _i为 Branch,那么设置新的PC值为 branch_target _address_i。
always @ (posedge clk) begin
if (ce == `ChipDisable) begin
pc <= 32'h00000000;
end else if(stall[0] == `NoStop) begin
if(branch_flag_i == `Branch) begin
pc <= branch_target_address_i;
end else begin
pc <= pc + 4'h4;
end
end
end
3、修改译码阶段ID模块
根据指令的指令码和功能码,以及指令有关bit位的特点来判断是哪一条指令
4、修改执行阶段EX模块
如果alusel_o
为EXE_RES_JUMP_BRANCH
,那么就将返回地址link_address_i
作为要写入目的寄存器的值赋给wdata_o
。
测试
JUMP指令
BRANCH指令