CO_P5
P5总结
(一).控制和冒险简述
-
对于控制冒险,本实验要求大家实现比较过程前移至 D 级,并采用延迟槽。
-
对于数据冒险,两大策略及其应用:
假设当前我需要的数据,其实已经计算出来,只是还没有进入寄存器堆,那么我们可以用转发( Forwarding )来解决,即不引用寄存器堆的值,而是直接从后面的流水级的供给者把计算结果发送到前面流水级的需求者来引用。如果我们需要的数据还没有算出来。则我们就只能暂停( Stall ),让流水线停止工作,等到我们需要的数据计算完毕,再开始下面的工作。
(二).冒险处理
冒险处理我们均通过“A_T”法实现——
转发(forward)
当前面的指令要写寄存器但还未写入,而后面的指令需要用到没有被写入的值时,这时候会产生数据冒险,我们首先考虑进行转发。我们假设所有的数据冒险均可通过转发解决。也就是说,当某一指令前进到必须使用某一寄存器的值的流水阶段时,这个寄存器的值一定已经产生,并存储于后续某个流水线寄存器中。
在这一阶段,我们不管需要的值有没由计算出,都要进行转发,即暴力转发。为实现这一机制,我们要清楚哪些模块需要转发后的数据(需求者)和保存着写入值的流水寄存器(供应者)
- 供应者及其产生的数据
流水级 | 产生数据 | MUX名&选择信号名 | MUX输出名 |
---|---|---|---|
E | E_imm32, E_PC8 |
直接流水线传递 | 直接流水线传递 |
M | M_ALUout, M_PC8 |
直接流水线传递 | 直接流水线传递 |
W | w_res, w_RD, w_imm32, W_PC8 |
w_WhichtoReg | WD |
注:当M级指令为读hi和lo的指令时, M_AO中的结果是从上一周期在乘除槽中读取的hi或lo的值;如果是其他指令,M_AO是上一周期ALU的计算结果。
- 需求者及其产生的数据
接收端口 | 选择数据 | HMUX名&选择信号名 | MUX输出名 |
---|---|---|---|
B_transfer_D1 | D_V1, M_out, E_out |
SelB_D1 | d_b_transfer1 |
B_transfer_D2 | d_RD2, m_res, e_res |
SelB_D2 | d_b_transfer2 |
ALU_A | e_RD1, WD, m_res |
SelALU_A | e_A |
ALU_B | e_RD2, WD, m_res |
SelALU_B | e_B |
DM_WD | m_RD2, WD |
SelDM | M_WD_f |
NPC_ra | D_V1_f , E_PC8 , M_PC8 |
SelJr | ra |
从上表可以看出,W级中的数据没有转发到D级,原因是我们在GRF内实现了内部转发机制,将GRF输入端的数据(还未写入)及时反映到RD1或这RD2,判断条件为A3 == A2
或者A3 == A1
。
此时为了生成HMUX的选择信号,我们需要向HCU(冒险控制器)输入”A”数据,然后进行选择信号的计算,执行转发的条件为——
暂停(stall)
接下来,我们来处理通过转发不能处理的数据冒险。在这种情况下,新的数据还未来得及产生。我们只能暂停流水线,等待新的数据产生。为了方便处理,我们仅仅为D级的指令进行暂停处理。
我们把Tuse和Tnew作为暂停的判断依据——
- Tuse:指令进入 D 级后,其后的某个功能部件再经过多少时钟周期就必须要使用寄存器值。对于有两个操作数的指令,其每个操作数的 Tuse 值可能不等(如 store 型指令 rs、rt 的 Tuse 分别为 1 和 2 )。
- Tnew:位于 E 级及其后各级的指令,再经过多少周期就能够产生要写入寄存器的结果。在我们目前的 CPU 中,W 级的指令Tnew 恒为 0;对于同一条指令,Tnew@M = max(Tnew@E - 1, 0)、
在这一阶段,我们找到D级生成的Tuse_rs和Tuse_rt和在E,M,W级寄存器中流水的Tnew_D,Tnew_M,Tnew_W,如下表所示
- Tuse表和计算表达式
指令类型 | Tuse_rs | Tuse_rt |
---|---|---|
calc_R | 1 | 1 |
calc_I | 1 | X |
shift | X | 1 |
shiftv | 1 | 1 |
load | 1 | X |
store | 1 | 2 |
md | 1 | 1 |
mt | 1 | X |
mf | X | X |
branch | 0 | 0 |
j / jr | X | X |
jal / jalr | 0 | X |
lui | X | X |
- Tnew表和计算表达式
指令类型 | Tnew_D | Tnew_E | Tnew_M | Tnew_W |
---|---|---|---|---|
calc_R | 2 | 1 | 0 | 0 |
calc_I | 2 | 1 | 0 | 0 |
shift | 2 | 1 | 0 | 0 |
shiftv | 2 | 1 | 0 | 0 |
load | 3 | 2 | 1 | 0 |
store | X | X | X | X |
md | X | X | X | X |
mt | X | X | X | X |
mf | 2 | 1 | 0 | 0 |
branch | X | X | X | X |
jal / jalr | 0 | 0 | 0 | 0 |
j / jr | X | X | X | X |
lui | 1 | 0 | 0 | 0 |
然后我们Tnew和Tuse传入HCU(冒险控制器中),然后进行stall信号的计算。如果满足以下条件则stall有效——
-
Tnew > Tuse
-
前位点的读取寄存器地址和某转发输入来源的写入寄存器地址相等且不为 0
-
写使能信号有效
-
当E级延迟槽在进行运算(
start | busy
)时,D级为md、mt、mf指令 -
阻塞的构造(D级)
(三).其他要点
根据流水线的思想,我们将指令的整个执行过程分割为五个部分,各个部分独立完成不同指令相应过程的工作。然而会存在某些寄存器的值虽然被算出来了但是还没来得及写回去便被下一条指令读入的情况。这时,便需要我们修一条数据通路将算出来的值连接到读入的地方对错误的值进行修正。
根据上面的理解,我们可以得出如下结论:
- 转发是针对GRF的行为。(由于lw类指令要到M级才会读到数据,即使上一条紧跟着sw指令。sw类指令会在M级将数据写到DM中,此时lw类指令还在E级是完全来得及的)
- 已经算出来值的寄存器可以被转发。
- 即将被写回的寄存器和被读入的寄存器是相同的。
自然,我们需要关注的是:
-
读入寄存器(rs、rt都要考虑)、写回寄存器(上面的WriteReg)是什么?(决定了要不要转发、同时要注意$zero)
-
要转发的值是什么?(决定了MUX的选择信号是什么)
-
写回寄存器的值在哪一级被算出来?(决定了在哪一级提供转发)
如果写回寄存器在某一级被算出来,则这个值在后面的流水级都可能需要被转发。
-
这一级需不需要用到读寄存器的值?(决定了在哪一级接收转发)
事实上、只有D级和E级需要接收转发,因为在M级ALUOut都有了。
-
这一级对应的指令改不改变寄存器的值?(决定了要不要转发)
这里我还是对无脑转发机制感觉不是很放心,所以多加了一个判断
同时强烈建议大家把pc和instr沿着流水线传下去、方便debug也方便之后加指令
-
在E级有关写寄存器的信号:
SignImm_E 、PC_E
-
在M级有关写寄存器的信号:
SignImm_M 、PC_M、ALUOut_M
-
在W级有关写寄存器的信号:
Result_W ( 之前的 SignImm、PC和 ALUOut 都被集成到这里面去了 )
想好了上面几个问题之后,我们就可以正式考虑转发辣!(这里分D级、E级两种情况考虑)
1.D级
首先,由于beq、jr这两条指令,我们是需要读寄存器的值并且立即使用的 ( 即T_use = 0 ) 。
由于jal、lui这两条指令在D级就被算出来了,因此这两个值 ( SignImm 和 PC ) 都应该可以作为被转发的值。这两个值在E级( SignImm_E 、PC_E )和M级(SignImm_M、PC_M )对应的值应该被记录下来作为可能被转发的值。( 为什么没有W级? )
由于GRF内部转发的机制,如果写回-读入寄存器相同的话(且不为$zero),读取写入的值
而在E级ALUOut被算出来了,所以要记录 ALUOut_M ( 为什么不是ALU_E)
转发的数据来源必须是某级流水线寄存器、不允许对功能部件的输出直接进行转发。(可能是为了维护时序的稳定?求大佬解答)
所以、以RD1为例我们可以如下定义D级的转发信号(ForwardAD)
1 | assign ForwardAD = ((Rs_D != 0) && (Rs_D == WriteReg_E) && RegWrite_E && WriteSel_E == 2'b01) ? 3'b101 : |
2.E级
由于我们的ALU需要 SrcA 和 SrcB 这两个需要 “读” 的值、也需要接收转发 ( T_use = 1 )
和D级一样,我们需要M级的 SignImm_M 、PC_M、ALUOut_M 、同时还需要 W 级的 Result_W 作为转发的来源
所以、以SrcA为例我们可以如下定义E级的转发信号 (ForwardAE)
1 | assign ForwardAE = ((Rs_E != 0) && (Rs_E == WriteReg_M) && RegWrite_M===1'b1 && WriteSel_M == 2'b00) ? 3'b100 : |
这样的话、所有的转发控制我们就都考虑好啦。
除此之外,由于某些不可抗力的因素,只用转发有些问题我们是解决不了的。比如有一些指令写入的操作太慢了( 如lw类 ) ,还有一些指令需要用的时候太早了 ( 如 beq 类,含 jr ) 。这时我们就需要用阻塞去解决。
根据上面的分析,我们大概可以将阻塞的类型分为两种:
lw 类型阻塞(定义lwstall端口)
那我们是在什么时候发现需要用lw类型阻塞的呢?
事实上,在E级我们就已经接收到了D级的CU翻译出来的控制信号了,而MemtoReg==1则代表了像 lw 这样从DM中读出数据给寄存器的指令。如果这个信号为1,则可能需要阻塞。同时记得条件还有这时要读出的寄存器 ( Rs或Rt ) 和 lw 读取的寄存器 ( Rt ) 是同一个寄存器。
这样我们就可以给出 lwstall 控制信号,可以思考为什么是D级的Rs,Rt 和 E级的 Rt ?
1 | assign lwstall = ((Rs_D == Rt_E) || (Rt_D == Rt_E)) && MemtoReg_E; |
Rs_D 、 Rt_D 对应D级读出的寄存器、而Rt_E则对应了 lw 类指令要写回的寄存器
beq 类型的阻塞 ( 定义 branchstall端口 )
由于 beq 类指令比较特殊(T_use = 0),需要考虑和ALU计算类型指令的冲突 (T_new = 1) , 还有和 lw 类型指令的冲突 (T_new = 2 )。
对于ALU计算类型、可以分析出当写回的寄存器与 Rs 、Rt 相同时需要阻塞 (同时这个指令需要写回,也就是 RegWrite = 1)。
对于lw类型、可以分析出当读取的寄存器与 Rs、Rt 相同时需要阻塞 ( lw类 对应MemtoReg_M)
这样,我们就可以给出 branchstall 控制信号,可以思考以下为什么是E级的 RegWrite 和M级的 MemtoReg ?
1 | assign branchstall = (Branch_D && RegWrite_E && (WriteReg_E == Rs_D || WriteReg_E == Rt_D)) || |
E级的 RegWrite 对应的是 ALU 计算类指令要写回的寄存器,应和 beq 指令需要的寄存器进行比较。
事实上,在 lw 指令后面紧跟 beq 指令时,是需要阻塞两周期的。当 lw 指令在E级、beq指令在D级时满足 lwstall 条件,此时阻塞一周期。lw 指令到M级而 beq 指令还在D级。所以这里需要判断的是M级的MemtoReg。如果满足条件的话由 branchstall 指令给出第二次阻塞。
jr 类型的阻塞(定义jrstall端口)
和beq类型的阻塞相似.
(四).p5上机技巧
增添指令一般步骤
明确指令RTL
- 该步骤需要结合题目弄懂指令行为,包括明确指令类型(R型?I型?J型?即明确读和写的目标)、opcode和funct域数据、执行功能(计算?跳转?访存?)
- 最好可以先用MARS模拟下,以免对指令行为理解不到位。
明确非转发数据通路
- 在该步骤中,可以在单周期中思考新指令的行为,构思出该新指令的数据通路,然后修改控制器中的相关控制信号——包括使能信号(P5中的DM和GRF的写使能、P6中的GRF写使能和新加的byteen)、功能MUX的选择信号(GRF写入地址的选择、ALU的B端口数据的选择、GRF写入数据的选择等等)和模块功能的选择信号(ALU功能选择、NPC功能选择)。
考虑转发
- 考虑新指令作为提供者:
- 首先考虑它是否及时将已经计算出来的将要被写入GRF的数据转发;
- 例如,计算类指令在E级ALU会生成GRF写入数据,那么需要在后面的M级、W级的流水寄存器设置接口这个数据转发
- 例如,跳转并链接类指令在D级就可以生成GRF写入数据
pc+8
,那么需要E级、M级和W级的流水寄存器设置接口将这个数据转发
- 然后考虑GRF的5位写入地址是否正确。
- 一般在第2步已经调整完毕,但是像
lwer
、lhso
等条件存储类指令只有在M级从DM中取出数据后才能明确写入地址,需要在M级将GRF写入地址再次修改
- 一般在第2步已经调整完毕,但是像
- 首先考虑它是否及时将已经计算出来的将要被写入GRF的数据转发;
- 考虑新指令作为接受者:明确需要使用
GPR[rs]
、GPR[rt]
的功能部件和相应接口(CMP的D1和D2?ALU的A和B?乘除模块的A和B?DM的WD?),课上可能需要设置新的接口。
考虑暂停
- 明确新指令的
Tuse_rs
、Tuse_rt
以及在各级流水的Tnew
,直接在主控制器中修改即可(千万不要忘记)。
课上测试题型分析
注:笔者课下cpu设计采用的是集中式译码和暴力转发。
计算类
P5中一般只需要增加ALU的功能,但一定要看清楚新指令的计算行为,最好在MARS里先模拟一下。
-
一般来说新指令的计算行为会稍微复杂一点,用
always @(*)
写会比较简单,用assign
的话可以定义一个function
。 -
一般情况下,
Tnew
和Tuse
与calc_R型指令保持一致即可。 -
循环移位可以采用以下写法——
1
2
3//以循环左移为例
if(B[4:0] == 5'd0) out = A;
else out = A << B[4:0] | A >> (5'd31 - B[4:0] + 5'd1);
P6的计算会涉及到乘除模块,也相对比较简单。需要注意madd、maddu、msub、msubu等指令(roife博客[1] 有讲到)。
-
以madd为例(将两个数有符号相乘,计算结果与之前的HI、LO寄存器中的值相加,而不是覆盖),如果是以下写法会出现问题
1
2
3
4//错误写法1
{HI_temp, LO_temp} <= {HI, LO} + $signed(A) * $signed(B);
//错误写法2
{HI_temp, LO_temp} <= {HI, LO} + $signed($signed(A) * $signed(B));- 错误写法1出现问题的原因是: 位拼接
{HI, LO}
默认被当做无符号数, 无符号性传递到$signed(A) * $signed(B)
,因此即使使用了$signed()
还是会被当成无符号数进行乘法运算 - 错误写法2出现问题的原因是:虽然使用了
$signed()
屏蔽了外界符号性的传入,但是也屏蔽了位宽信息的传入,所以$signed($signed(A) * $signed(B))
的结果实际上是32位(因为$signed(A)
和$signed(B)
都是32位,又没有外界位宽信息的传入,因此结果被强制规定为32位),即高32位的数据被截去,在参与后续运算时自然会出现问题。
- 错误写法1出现问题的原因是: 位拼接
-
为了避免上述情况,我们需要在错误写法2的最外层$signed()中人为传入64位位宽信息,学长博客的写法如下——
1
2
3
4//正确写法1
{HI_temp, LO_temp} <= {HI, LO} + $signed($signed(64'd0) + $signed(A) * $signed(B));
//正确写法2
{HI_temp, LO_temp} <= {HI, LO} + $signed($signed({{32{A[31]}}, A[31]}) * $signed({{32{B[31]}}, B[31]}));我认为还可以对错误写法1进行修改——
1
2//正确写法3
{HI_temp, LO_temp} <= $sigend({HI, LO}) + $signed(A) * $signed(B);
条件跳转类
一般跳转类指令有以下几种要求——
- 条件跳转+无条件链接
- 条件跳转+条件链接
- 条件跳转+条件(无条件)链接+不跳转时清空延迟槽
-
条件跳转比较好做,一般只需增加CMP模块中的判断功能即可。
-
如果是无条件链接的话也比较简单,可以直接在D级将
RFWrite
(GRF写入使能)置1并让它流水,并更改一下A3
(GRF写入地址,一般是要链接到31号寄存器),最后在W级将GRF写入数据选择成PC+8即可。 -
如果是条件链接,则需要在D级根据CMP模块的输出结果判断RFWrite是否有效,写法如下——
1 | //为了确定当前指令是新指令,我们设置一个check信号随新指令一起流水,check有效则表示当前指令是新指令 |
-
如果题目要求不跳转时清空延迟槽,则需要根据当前CMP模块输出结果判断是否清空D级流水寄存器。
需要注意的是,如果当前正在处于stall状态时,不能清空延迟槽(stall说明前面指令的Tnew大于新指令的Tuse,即需要传入CMP模块的两个值的最新值还没有计算出来,因此还无法转发到CMP中)。写法如下——
1
wire D_Reg_clr = check_D & ~D_CMP_out & ~stall;
条件存储类
条件存储,也就是从DM取出值之后,根据这个值是否满足某个condition,再判断要往哪个寄存器写。 和前两种题型相比更复杂,但是总结下来也就只有以下三种类型——
- condition成立: 将DM中的值写入A号寄存器
condition不成立: 写入B号寄存器 - condition成立: 将DM中的值写入A号寄存器
condition不成立: 不写入 - 写入目标完全取决于DM的读取值(如将DM读取值的低5位作为写入目标)
对于第二种不写入的情况,我们可以将写入地址设置为0号寄存器。因此这三种类型本质上是一种。
对于条件存储类指令,我们只有到M级才知道写入目标是什么,这对会我们的转发和暂停造成影响。我们需要对 stall
信号的生成逻辑进行修改,引用学长的话说就是——“如果 D 级的指令要读寄存器,而且后面的新指令 可能 要写这个寄存器,那么就 stall
”。代码如下——
1 | //笔者采用的命名方法是——A1和A2表示该流水级指令的GRF读地址,A3表示指令的GRF写地址 |
此外我们还需要在M级根据DM取出的值修改A3
(GRF写入地址),代码如下——
1 | //第一种题型(eg:condition满足向rt号写,否则写31号) |
这样一来,我们在M级就将可以将正确的GRF写入地址修改,然后再传入下一级流水寄存器(W_Reg)和冒险控制器(HCU)即可。
1 | W_Reg u_W_Reg (//input |
如果你是到W级才修改写入地址(也就是说,M_A3_new
只传入了W_Reg而没有传入HCU,HCU的输入端仍然是M_A3
), 这样会有一定问题。通过下面的例子说明——
1 | lhso $s1, 1024($0) |
当lhso
在M级的时候,M级写入地址还没有被及时更新。因此这时候冒险控制器中lhso
的写地址($s1
)和sw的读地址($s1
)还是相等的,所以会向处于E级的sw指令转发一个数据(即教程中采用的暴力转发)。下一时钟上升沿来临时,lhso
进入W级,如果这时候lhso的写入地址不再是$s1
, 而是根据condition修改成了31
号寄存器,那么我们在上一周期向sw指令转发的值就是一个错误值。
为了避免这种情况,我们需要对转发信号做一些调整,策略是:既然我们在lhso
进入W级之前不知道要往哪个寄存器写值,那么我们就不向前转发。转发信号的调整如下——
1 | assign FwdCMPD1 = ((D_A1 != 5'd0) & (D_A1 == E_A3) & (RFWrite_E) & ~check_E) ? 2'd2 : |
这样对于新指令,我们就不再是暴力转发,而是条件转发。我个人还是建议采用第2种方法,我认为这种方法的正确性比第一种更容易证明。另外,建议在课下可以提前设置好一个check信号并让它流水,这样在课上会节省很多时间。
实际上,我们也可以把所有指令的转发都更改为条件转发——即所有的指令只有得到要写入寄存器的结果后才会向前转发,这样只需要将Tnew == 0
加入判断条件即可。
1 | //Tnew == 0 表示当前指令已经产生要写入寄存器的结果 |
Q:条件转发会有问题吗?
A:不会。 因为有暂停机制把关,保证了指令获得要写入寄存器的值之前,前面的正常执行的(即不被stall的)指令都不会用到相关寄存器的值,或恰好将要使用,即Tnew <= Tuse,因此不会带来新的问题。
- Title: CO_P5
- Author: Charles
- Created at : 2022-12-26 20:56:01
- Updated at : 2023-11-05 21:36:02
- Link: https://charles2530.github.io/2022/12/26/co-p5/
- License: This work is licensed under CC BY-NC-SA 4.0.