目录
一 数据相关问题
1.1.什么是数据相关问题
数据相关问题有很多种,这里我们只关心RAW(read after write)相关,即读操作在写操作之前。顾名思义就是在新的数据还未写入寄存器时就去读取了寄存器的值,这样导致读取的值是原来的旧值而不是最新的值。
下面看一个例子,考虑有如下代码,可以看到第一条指令的结果对1号寄存器进行了赋值,第二条指令去读了一号寄存器的值。
这样为什么会造成RAW相关问题呢?主要还是因为cpu的五级流水线结构,如下图可以看到,第一条指令的结果要在回写阶段才能写入一号寄存器,而第二条指令在译码阶段就已经开始读取一号寄存器的值了,这时并没有读到最新更新的值。
类似的,当读写操作隔了一个指令时也会发生类似的情况:
最大的相隔情况是相隔两个指令:
1.2 如何解决数据相关问题
1.2.1相隔两条指令
我们可以看到相隔两条指令的时候,读和写操作位于同一个时钟周期,这时我们可以通过寄存器模块的代码来实现读取最新的数据。那就是当寄存器模块被同时读写同一个地址时,我们就将写入的值直接赋值给读取的值,这样就可以保证读到的值是回写过来的最新数据。之前我们在寄存器模块中也写过:
//读操作
always @(*) begin
if (!rst && re1) begin
rdata1 = regs[raddr1];
end
else if (!rst && re1 && (raddr1 == 0)) begin//0号寄存器恒为0
rdata1 = 0;
end
//读写同一个地址,把写地址直接赋值,保证写优先,这样才能读到最新的数据,这也是为什么先写写操作的代码
else if (!rst && re1 && we && (raddr1 == waddr)) begin
rdata1 = wdata;
end
else begin
rdata1 = 0;
end
end
always @(*) begin
if (!rst && re2) begin
rdata2 = regs[raddr2];
end
else if (!rst && re2 && (raddr2 == 0)) begin//0号寄存器恒为0
rdata2 = 0;
end
else if (!rst && re2 && we && (raddr2 == waddr)) begin
rdata2 = wdata;
end
else begin
rdata2 = 0;
end
end
1.2.2相隔0条和一条指令
这里介绍一个常用的解决RAW相关方法——数据前推
在相隔0条指令的情况下,虽然第一条指令的结果要在回写阶段才写入寄存器,但是其结果早在执行阶段就已经计算出来,如果我们将执行阶段的结果直接给到译码阶段不就可以解决这个问题了。同样的相隔一条指令时,把执行阶段的结果传递到访存阶段然后赋值到第三条指令的译码阶段就可以啦。
接下来思考如何具体实现:
执行阶段的结果需要传递给译码阶段的有哪些信号?
条件:需要回写到寄存器的指令(即写目的寄存器使能信号wreg_o为1时)
信号:除了写使能信号,肯定还需要把写入的地址和数据也传递过来即wd_o,wdata_o,这本来也是执行阶段的输出信号
当然这些信号也需要传递给访存阶段,再由访存阶段传输到译码阶段,修改的模块图如下:
来看看译码模块的代码是怎么实现的,其实和之前寄存器模块处理相隔两条执行的相关问题的方法一样,如果传递过来的数据就是译码阶段要读的数据(即目的寄存器地址和读取寄存器地址一样),就直接把传递过来的值给到译码阶段的输出。
//给reg1_。赋值的过程增加了两种情况:
//1.如果Regfile模块读端口1要读取的寄存器就是执行阶段要写的目的寄存器,
//那么直接把执行阶段的结果ex_ wdata i 作为reg1。的值;
//2.如果Regfile模块读端口1要读取的寄存器就是访存阶段要写的目的寄存器,
/1那么 直接把访存阶段的结果mem wdata i作为reg1。的值;
always @ (*) begin
if(rst == `RstEnable) begin
reg1_o <= `ZeroWord;
end else if((reg1_read_o == 1'b1) && (ex_wreg_i == 1'b1)
&& (ex_wd_i == reg1_addr_o)) begin
reg1_o <= ex_wdata_i;
end else if((reg1_read_o == 1'b1) && (mem_wreg_i == 1'b1)
&& (mem_wd_i == reg1_addr_o)) begin
reg1_o <= mem_wdata_i;
end else if(reg1_read_o == 1'b1) begin
reg1_o <= reg1_data_i;
end else if(reg1_read_o == 1'b0) begin
reg1_o <= imm;
end else begin
reg1_o <= `ZeroWord;
end
end
//给reg2_。赋值的过程增加了两种情况:
//1.如果Regfile模块读端口2要读取的寄存器就是执行阶段要写的目的寄存器,
//那么 直接把执行阶段的结果ex_wdata_ i作为 reg2_ 。的值:
//2.如果Regfile模块读端口2要读取的寄存器就是访存阶段要写的目的寄存器,
//那么 直接把访存阶段的结果mem_ wdata_ i作为reg2_。的值;
always @ (*) begin
if(rst == `RstEnable) begin
reg2_o <= `ZeroWord;
end else if((reg2_read_o == 1'b1) && (ex_wreg_i == 1'b1)
&& (ex_wd_i == reg2_addr_o)) begin
reg2_o <= ex_wdata_i;
end else if((reg2_read_o == 1'b1) && (mem_wreg_i == 1'b1)
&& (mem_wd_i == reg2_addr_o)) begin
reg2_o <= mem_wdata_i;
end else if(reg2_read_o == 1'b1) begin
reg2_o <= reg2_data_i;
end else if(reg2_read_o == 1'b0) begin
reg2_o <= imm;
end else begin
reg2_o <= `ZeroWord;
end
end
二 逻辑,移位操作和空指令
2.1指令说明
对于译码模块id,只需要在case语句里对不同的指令进行区分。
我们先来根据指令类型来整理一下这些指令对应的输出:
2.2 确定执行类型
指令码op不为0时可以直接判断,当指令码为0时也就是SPECIAL类型,需要判断op3。
这里不知道为什么还要额外判断,其实之分两种就可以,作者应该是按照id模块输出情况来分类的
2.3 修改执行模块
还记得执行模块都进行了什么操作吗,主要是根据操作类型对指令进行逻辑和移位操作
逻辑运算很简单,来看看移位运算是如何实现的:
case (aluop_i)
`EXE_SLL_OP: begin
shiftres <= reg2_i << reg1_i[4:0] ;
end
`EXE_SRL_OP: begin
shiftres <= reg2_i >> reg1_i[4:0];
end
`EXE_SRA_OP: begin
shiftres <= ({32{reg2_i[31]}} << (6'd32-{1'b0, reg1_i[4:0]}))
| reg2_i >> reg1_i[4:0];
en
这里[4:0]是因为对于sll指令是根据sa的值,对于sllv是根据寄存器rs的前四位,都是四位。
算数位移也可以使用更简单的>>>来实现:
Result = ($signed(operandB)) >>> operandA; //更正后
这里我有个疑问,对于不同的指令比如xor和xori为何送去执行阶段处理的操作alu_op_o一样?
后来仔细看了译码阶段的代码才知道,对于xor指令输出rs和rd寄存器的值送过去做xor运算,而对于xori只读了一个寄存器那么没读的那个操作数就把立即数赋值给他,代码如下:
always @ (*) begin
if(rst == `RstEnable) begin
reg1_o <= `ZeroWord;
end else if((reg1_read_o == 1'b1) && (ex_wreg_i == 1'b1)
&& (ex_wd_i == reg1_addr_o)) begin
reg1_o <= ex_wdata_i;
end else if((reg1_read_o == 1'b1) && (mem_wreg_i == 1'b1)
&& (mem_wd_i == reg1_addr_o)) begin
reg1_o <= mem_wdata_i;
end else if(reg1_read_o == 1'b1) begin
reg1_o <= reg1_data_i;
end else if(reg1_read_o == 1'b0) begin
reg1_o <= imm;
end else begin
reg1_o <= `ZeroWord;
end
en
所以看似在执行阶段操作是这样
`EXE_XOR_OP: begin
logicout <= reg1_i ^ reg2_i
其实对于不同的指令在译码阶段已经把输出的reg1/2更改了