$\mathscr {Hugo}$ $\mathbb{CPU}$ 五级流水线$\mathscr{Design}$
实现五级流水线CPU
- 命名统一采用hugo命名法(借鉴了匈牙利命名法和下划线命名)
- 主体为mips.v模块,不再像P4一样加入DataPath模块,统一在mips.v中完成连线等大部分操作。
- 控制部分分为
Ctrl_Unit
和Hazard_Ctrl
两个部分,处理冲突为在能够使用旁路转换的情况下尽可能的使用旁路转换 - 流水线阶段分为IF,ID,EX,MEM,WB五个部分
- F:
NPC
,PC
,IM
- D:
GRF
,EXT
,CMP
- E:
ALU
- M:
DM
- W:
GRF
- F:
- 其中用到大部分的宏定义在
def.v
中定义
CPU流水线的实现
实现指令说明
将本CPU实现的指令分为以下几类:
classify | 指令set |
---|---|
load | lw//lh,lhu,lb,lbu |
store | sw//sh,sb |
calc_r | add,sub//addu, subu,and,or,nor,xor,slt,sltu |
calc_i | ori//addiu,addi,xori,slti,sltiu |
shift_s | sll//sra,srl |
shift_v | //sllv,srav,srlv |
b_type | beq//bne |
j | jal,j |
特殊 | jr,lui |
基本的数据通路
IF阶段的pc需要保留到后面继续使用
重点处理的在于ALU
命名规范
- 对于每一个模块依旧采用仅有文件英文名的办法,对于其实例化为_小写
- 对于每一条线采用层级+命名的方式(原本采用的是匈牙利命名法,也就是前面加上对应的类型,后发现全部都是wire,遂弃之)
- 寄存器文件采用两边的流水线层级加上_REG的方式。
数据通路DataPath
同P4,变量命名有稍微修改。
但没有单独使用一个DataPath的模块,显得比较多余。
对于一个数据路径,包括取指令(IF),译码(ID),执行(EX),访存(MEM),回写(WB)这几个方面,相应的有IFU,NPC,GRF,ALU,IM,DM这几个基本单元,单元之间通过Splitter和MUX等进行元件之间的数据交换和处理,这里需要在构建是需要留下几个控制信号的接口,以便于最后CU(控制器单元)单元的构建。
IFU取指令单元
该模块由PC(Programming Counter)模块和IM(Instruction Memory)模块组成。其中PC模块负责对每次新的指令状态进行转移,IM模块则从ROM中得到相应的指令。
这里考虑到之后可能需要将IM和DM放到一起,这里不再对PC和IM进行进一步的封装。
F_PC(程序计数器)
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_clk | I | 1 | 时钟信号 |
i_reset | I | 1 | 异步复位信号 |
i_en | I | 1 | 使能信号 |
i_npc | I | 32 | 通过计算得到的下一条指令的地址 |
or_pc | O | 32 | 状态转移后的地址,输出当前正在执行的地址 |
- 功能定义
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 复位 | 当Reset信号有效时,将PC寄存器中的值置为0x00003000 |
2 | 停止 | 当Stop信号有效时,PC寄存器忽略时钟输入,PC当前值保持不变 |
3 | 写 PC 寄存器 | 当 Stop 信号失效且时钟上升沿来临时,将下一条指令的地址(next PC)写入 PC 寄存器 |
F_IM(指令存储器)
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_pc | I | 32 | 当前正在执行的地址 |
o_instr | O | 32 | 输出当前正在执行的指令 |
- 功能定义
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 取指令 | 根据当前PC的值从IM中读出对应的指令 |
D_NPC(下一指令计算单元)
计算下一个指令,有三种方式,包括直接计算下一条指令,b型跳转指令,j型跳转指令。其中j型跳转指令包括跳转到寄存器的值和直接跳转两种。
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_pc | I | 32 | 当前指令地址 |
i_npcOp | I | 2 | NPC控制信号 |
i_imm16 | I | 16 | branch类型的16位立即数 |
i_imm26 | I | 26 | jump类型的26位立即数 |
i_jumpEn | I | 1 | 用于得到branch类型的跳转条件是否成立 |
i_ra_of_jr | I | 32 | 寄存器中存储的地址 |
o_npc | O | 32 | 下一条指令地址 |
选择信号类型 | 位宽 | 值 | 描述 |
---|---|---|---|
NPC_PC4 | 3 | 3‘b000 | pc+4 |
NPC_J | 3 | 3’b001 | 直接跳转,26位立即数拓展后的地址 |
NPC_B | 3 | 3’b010 | 条件跳转,满足条件跳转到16位立即数拓展后的地址 |
NPC_JR | 3 | 3’b011 | 跳转到寄存器存储的地址 |
- 三种跳转指令
b型跳转指令
均为判断后跳转到label(即Offset)
JR型跳转指令(jr,jalr)
跳转到寄存器中的存储的地址
J型跳转指令(j,jal)
跳转到target这个立即数对应的地址
其实也可以分为:
间接寻址(通过PC+4和Offset寻址)
直接寻址(直接跳转到立即数对应地址,或者寄存器中存储的地址)
D_GRF(通用寄存器组)
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_clk | I | 1 | 时钟信号 |
i_reset | I | 1 | 异步复位信号 1:复位信号有效 0:复位信号无效 |
i_writeEn | I | 1 | 写使能信号 1:写入有效 0:写入无效 |
i_A1 | I | 5 | 地址输入信号,指定 32 个寄存器中的一个,将其中的数据读出到 RD1 |
i_A2 | I | 5 | 地址输入信号,指定 32 个寄存器中的一个,将其中的数据读出到 RD2 |
i_A3 | I | 5 | 地址输入信号,指定 32 个寄存器中的一个,将其作为写入目标 |
i_WD | I | 32 | 数据输入信号 |
o_RD1 | O | 32 | 输出A1指定的寄存器中的 32 位数据 |
o_RD2 | O | 32 | 输出A2指定的寄存器中的 32 位数据 |
i_pc | I | 32 | 用于$display |
- 功能定义
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 复位 | Reset 信号有效时,所有寄存器中储存的值均被清零 |
2 | 读数据 | 读出 A1,A2 地址对应的寄存器中储存的数据,将其加载到 RD1 和 RD2 |
3 | 写数据 | 当 WE 信号有效且时钟上升沿来临时,将 WD 中的数据写入到 A3 地址对应的寄存器 |
D_EXT(拓展单元)
将16位立即数符号拓展为32位。这里为了提高可拓展性,添加了UnsignedExt
接口
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_imm16 | I | 16 | 16位立即数输入信号 |
i_unsigned_ext_Sel | I | 1 | 无符号拓展信号 1:无符号拓展(0拓展) 0:符号拓展 |
o_imm32 | O | 32 | 32位立即数输出信号 |
- 功能定义
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 符号拓展 | 将16位立即数进行符号拓展 |
D_CMP(B类指令比较单元)
用于生成Branch类跳转信号是否跳转的使能信号。该单元根据输入的branchOp信号对当前B指令的类型进行判断,进而对当前输入的数值进行比较,最后输出结果。
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_cmpA | I | 32 | 输入信号 |
i_cmpB | I | 32 | 输入信号 |
i_branchOp | I | 比较类型 | |
jumpOp | O | 1 | 是否满足跳转条件 |
- 功能定义
branchOP | 位宽 | 值 | 描述 |
---|---|---|---|
CMP_EQUAL | 4 | 4‘b0000 | 判断是否相等 |
E_ALU(逻辑运算单元)
该模块可实现加,减,按位与,按位或等 11 种运算,并根据 ALUOP 信号的值在这些功能中进行选择。除此之外,该模块还可以实现溢出判断
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
ALUOp | I | 4 | ALU 功能选择信号 |
src_A | I | 32 | 参与 ALU 计算的第一个值 |
src_B | I | 32 | 参与 ALU 计算的第二个值 S |
shamt | I | 5 | 移位数输入 |
out | O | 32 | 输出 ALU 计算结果 |
- 功能定义
ALUOp | 指令 | Opcode | Op |
---|---|---|---|
加法 | add | 00000 | ALURes = SrcA+SrcB |
减法 | sub | 00001 | ALURes = SrcA-SrcB |
乘法(low) | mul | 00010 | ALURes = SrcA*SrcB |
除法(商) | div | 00011 | ALURes = SrcA / SrcB |
与运算 | and | 00100 | ALURes = SrcA & SrcB |
或运算 | or | 00101 | ALURes = SrcA | SrcB |
异或运算 | xor | 00110 | ALURes = SrcA $\oplus$ SrcB |
或非运算 | nor | 00111 | ALURes = ~(SrcA | SrcB) |
逻辑左移 | sll | 01000 | ALURes = SrcB << Shift |
逻辑右移 | srl | 01001 | ALURes = SrcB >> Shift |
算数右移 | sra | 01010 | ALURes = SrcB >>> Shift |
M_DM(数据存储器)
- 端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
i_clk | I | 1 | 时钟信号 |
i_reset | I | 1 | 复位信号 |
i_Addr | I | 32 | 内存中的地址信号 |
i_dmOp | I | 2 | 选择信号 2’b00:word 2’b01:half_word 2’b10:byte |
i_WriteEn | I | 1 | 写使能信号 1:写入有效 0:写入无效 |
i_writeData | I | 32 | 在写入信号有效时,写入内存地址的数据 |
o_RD | O | 32 | 输出内存中对应地址的数据 |
- 功能定义
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 复位 | reset信号有效时,所有寄存器的中存储的值均被清零 |
2 | 读数据 | 读出A地址对应的存储单元的数据,将其加载到RD |
3 | 写数据 | 当WE信号有效且时钟上升沿来临时,将WD中的数据写入到A地址对应的存储单元 |
流水器寄存器模块定义
采用的是分布式译码,流水的是pc和instr,在每个阶段实例化CU模块,得到该阶段的对应控制信号。
FD_REF(IF/ID流水寄存器)
-
端口定义
方向 信号名 位宽 描述 输入来源 I i_clk 1 时钟信号 mips.v 中的 clk I i_reset 1 同步复位信号 mips.v 中的 reset I i_en 1 D 级寄存器使能信号 HCU 中 stall 信号取反 I i_flush 1 D 级寄存器清空信号 默认为 1‘b0 I F_instr 32 F 级 instr 输入 IFU_instr I F_pc 32 F 级 pc 输入 IFU_pc O D_instr 32 D 级 instr 输出 O D_pc 32 D 级 pc 输出
DE_Reg(ID/EX 流水寄存器)
-
端口定义
方向 信号名 位宽 描述 输入来源 I i_clk 1 时钟信号 mips.v中的clk I i_reset 1 同步复位信号 mips.v中的reset I i_en 1 使能信号 from HU I i_flush 1 清空信号 from HU I i_pc 32 I i_instr 32 O or_pc 32 O or_instr 32 I i_grf_RD1 32 D级产生的有效信号 from GRF I i_grf_RD2 32 D级产生的有效信号 from GRF I i_ext_result 32 D级产生的有效信号 from EXT O or_grf_RD1 32 传给E级的有效信号 O or_grf_RD2 32 传给E级的有效信号 O or_ext_result 32 传给E级的有效信号 -
运算功能
$Tnew_D = (Tnew_E > 0) ? (Tnew_D-1) : 0$
EM_Reg(EX/MEM 流水寄存器)
-
端口定义
方向 信号名 位宽 描述 输入来源 I i_clk 1 时钟信号 mips.v中的clk I i_reset 1 同步复位信号 mips.v中的reset I i_en 1 使能信号 from HU I i_flush 1 清空信号 from HU I i_pc 32 I i_instr 32 O or_pc 32 O or_instr 32 I i_alu_result 32 I i_mem_writeData 32 关于sw的来自GRF_RD2的信号 I i_ext_result 32 O or_alu_result 32 O or_mem_writeData 32 O or_ext_result 32 -
运算功能
我这里暂停部分在专门的一个模块HU中解决了
$Tnew_E = (Tnew_D > 0) ? (Tnew_E-1) : 0$
MW_Reg(MEM/WB 流水寄存器)
-
接口定义
方向 信号名 位宽 描述 输入来源 I i_clk 1 时钟信号 mips.v中的clk I i_reset 1 同步复位信号 mips.v中的reset I i_en 1 使能信号 from HU I i_flush 1 清空信号 from HU I i_pc 32 I i_instr 32 O or_pc 32 O or_instr 32 I i_dm_RD 32 来自dm的数据读出信号 from M_DM I i_alu_result 32 I i_ext_result 32 O or_dm_RD 32 O or_alu_result 32 O or_ext_result 32
控制单元_CU
输入改为instr,之前为opcode和func
指令 | Opcode[31:26] | [25:21] | [20:16] | [15:11] | [10:6] | [5:0] |
---|---|---|---|---|---|---|
add | 000000 | rs | rt | rd | 00000 | 100000 |
sub | 000000 | rs | rt | rd | 00000 | 100010 |
ori | 001101 | rs | rt | immediate | ~ | ~ |
lw | 100011 | base | rt | offset | ~ | ~ |
sw | 101011 | base | rt | offset | ~ | ~ |
beq | 000100 | rs | rt | offset | ~ | ~ |
lui | 001111 | 00000 | rt | immediate | ~ | ~ |
nop | 000000 | 0 | 0 |
分支转移实现
B 类指令
为了减少因控制冲突导致的暂停(stall),我们将 B 类指令的判断进行前置,单独使用 CMP 模块进行判断。当 B 类指令进入 D 级后(此时 F 级的指令为编译优化调度的指令),CMP 模块的判断结果进入 NPC,如过 CMP 结果为真(CMP_out = 1)而且 NPCOp 信号为 0x001(说明当前指令为 B 类指令),NPC 输出转移的地址 npc 并进入 IFU 的输入端,在下一时钟沿上升时进入 F 级,实现转移。
j 和 jal
当 j 或 jal 进入 D 级后(此时 F 级的指令为编译优化调度的指令),D_instr 中 imm26 域的数据进入 NPC 进行处理,如果当前 NPCOp 信号为 0x010(说明当前指令为 jal 或 j 指令),NPC 输出转移的地址 npc,并进入 IFU 的输入端,在下一时钟沿上升时进入 F 级,实现转移。
jal 指令在实现跳转的同时,还需要将下一条指令的地址存入 31 号寄存器中,因此我们需要在 IFU 中计算出改地址,并随着 jal 指令进行流水,最终在 W 级写入 GRF 的 31 号寄存器。由于存在延迟槽,pc+4 地址中的指令是编译优化机制调度过来的,因此我们要保存的地址应该为 pc+8。
jr
当 jr 进入 D 级后(此时 F 级的指令为编译优化调度的指令),D_V1_f(经过转发后的 D_V1 值)进入 NPC,如果当前 NPCOp 信号为 0x011(说明当前指令为 jr 指令),NPC 输出转移的地址 npc,并进入 IFU 的输入端,在下一时钟沿上升时进入 F 级,实现转移。
冒险处理
冒险处理我们均通过 “A_T” 法实现 ——
转发(forward)
无脑转发策略。
将所有在该层级后面的均进行转发。
比如ID级流水,需要用到D_rs和D_rt的对应寄存器的值。对于写入寄存器的值,可能为E级中的,E_alu_result, M_mem_writeData。
当前面的指令要写寄存器但还未写入,而后面的指令需要用到没有被写入的值时,这时候会产生数据冒险,我们首先考虑进行转发。我们假设所有的数据冒险均可通过转发解决。也就是说,当某一指令前进到必须使用某一寄存器的值的流水阶段时,这个寄存器的值一定已经产生,并存储于后续某个流水线寄存器中。
在这一阶段,我们不管需要的值有没由计算出,都要进行转发,即暴力转发。为实现这一机制,我们要清楚哪些模块需要转发后的数据(需求者)和保存着写入值的流水寄存器(供应者)
-
供应者及其产生的数据
流水级 产生数据 MUX 名 & 选择信号名 MUX 输出名 E E_E32,E_pc8 MUX_E_out & SelEMOut E_out M M_AO,M_pc8 MUX_M_out & SelEMOut M_out W W_AO,W_RD,W_pc8 MUX_W_out & SelWOut W_out -
需求者及其产生的数据
接收端口 选择数据 HMUX 名 & 选择信号名 MUX 输出名 CMP_D1/NPC_ra D_V1,M_out,E_out HMUX_CMP_D1 & FwdCMPD1 D_V1_f CMP_D2 D_v1,M_out,E_out HMUX_CMP_D2 & FwdCMPD2 D_V2_f ALU_A E_V1, W_out,M_out HMUX_ALU_A & FwdALUA E_V1_f ALU_B E_V2,W_out,M_out HMUX_ALU_B & FwdALUB E_V1_f DM_WD M_V2, W_out HMUX_DM & FwdDM M_V2_f
从上表可以看出,W 级中的数据没有转发到 D 级,原因是我们在 GRF 内实现了内部转发机制,将 GRF 输入端的数据(还未写入)及时反映到 RD1 或这 RD2,判断条件为 A3 == A2
或者 A3 == A1
。
此时为了生成 HMUX 的选择信号,我们需要向 HCU(冒险控制器)输入”A” 数据,然后进行选择信号的计算,执行转发的条件为 ——
- 前位点的读取寄存器地址和某转发输入来源的写入寄存器地址相等且不为 0
- 写使能信号有效
根据以上条件我们可以生成上面的 5 个 HMUX 选择信号,选择信号的输出值应遵循 “就近原则”,及最先产生的数据最先被转发。
暂停(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 branch 0 0 jump X X jr 0 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 branch X X X X jal 0 0 0 0 jr X X X X lui 1 0 0 0
然后我们 Tnew 和 Tuse 传入 HCU(冒险控制器中),然后进行 stall 信号的计算。如果 Tnew > TuseHCU 中的 stall 信号值为 1,此时执行以下操作 ——
- 冻结 PC 寄存器(IFU_en = ~stall = 0)
- 冻结 D 级寄存器(D_en = ~stall = 0)
- 清空 E 级寄存器(E_clr = stall = 1)
思考题
1、我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。
- 我们使用两个寄存器值的时间最早来到了 D 级,所以很可能会引发暂停.
- 如下面这种情况,若 beq 的结果在 E 级产生,则不需要暂停,可以转发,但是现在 beq 在 D 级的时候 lw 还在 M 级,没有产生结果,需要暂停。
lw $t0, 0($0)
nop
beq $t0, $t0, label
2、因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计?
- 因为延迟槽的存在,跳转指令的后一条必然会执行,所以需要把 PC+8 写入寄存器,不然 jr 时延迟槽内的指令会再执行一次
3、我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM 、 ALU ),请思考为什么?
- 因为流水寄存器中的储存的数据时前一级已经计算出来的数据,在当前周期内时稳定输出的。而功能部件的输出是有延迟的,如果让这些部件提供数据,有可能再其在回写数据生成前就写入了错误的数据,导致数据波动。
4、我们为什么要使用 GPR 内部转发?该如何实现?
如果你W级写入数据的寄存器与D级读寄存器的地址相同时,将会你导致出现错误的数值,因此需要通过内部转发来规避数据冒险。
// 内部转发
wire [31:0] D_FWD_RD1_D, D_FWD_RD2_D;
assign D_FWD_RD1_D = ( i_A1 == 5'h0 ) ? 32'h0:
((i_A1 == i_A3)) ? i_WD :
r_rf[i_A1];
assign D_FWD_RD2_D = ( i_A2 == 5'h0 ) ? 32'h0:
((i_A2 == i_A3)) ? i_WD :
r_rf[i_A2];
5、我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?
wire [31:0] D_FWD_rs_RD1 = (D_rs == 5'h0 ) ? 32'h0000_0000 :
(D_rs == E_grf_WA) ? E_grf_writeData :
(D_rs == M_grf_WA) ? M_grf_writeData :
D_grf_RD1;
wire [31:0] D_FWD_rt_RD2 = (D_rt == 5'h0 ) ? 32'h0000_0000 :
(D_rt == E_grf_WA) ? E_grf_writeData :
(D_rt == M_grf_WA) ? M_grf_writeData :
D_grf_RD2;
wire [31:0] E_FWD_rs_RD1 = (E_rs == 5'h0 ) ? 0 :
(E_rs == M_grf_WA) ? M_grf_writeData :
(E_rs == W_grf_WA) ? W_grf_writeData :
E_grf_RD1;
wire [31:0] E_FWD_rt_RD2 = (E_rt == 5'h0 ) ? 0 :
(E_rt == M_grf_WA) ? M_grf_writeData :
(E_rt == W_grf_WA) ? W_grf_writeData :
E_grf_RD2;
wire [31:0] M_FWD_writeData = (M_rt == 5'h0 ) ? 0 :
(M_rt == W_grf_WA) ? W_grf_writeData :
M_dm_writeData ;
6、在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。
- 高内聚低耦合原理:大多数只需要改变控制信号以及相应的功能模块增加功能,但不排除某些需要修改数据通路
- 对于计算类:首先改变 MCU,注意每个指令的控制信号的对应,再改 ALU 的结构,增加输出选择
- 对于访存类:改变 MCU + 修改 DM 增加相应的功能
- 对于跳转类:修改 MCU+NPC 相应功能修改
- 小技巧:寻找已有的指令中与新增的指令相似的指令(可能不止一条),然后顺着这几条指令改。
7、确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。
我采用的是分布式译码。我感觉分布式译码器确实从实际运用上讲会造成器件的浪费,但是单从code上来讲,确实会方便很多,其中把每个instr传进去也感觉十分方便。
但是可能不足在于修改了CU相应的部分,mips.v和HU.v都需要进行比较大的改动。
流水线冒险
-
在采用本节所述的控制冒险处理方式下,PC 的值应当如何被更新?请从数据通路和控制信号两方面进行说明。
A:当需要进行暂停时,IFU 的使能信号失效,PC 值不变。当不需要进行分支转移和跳转时,NPC 中将 F_pc 信号加 4 处理返回 IFU,下一时钟沿来临时更新为 F_pc+4。当执行分支指令时,NPC 将 D_pc +4 和符号扩展后的 imm16 相加,返回 IFU,下一时钟沿上升时更新。当执行 j/jal 指令时,NPC 将 imm26 进行扩展(前四位补 D_pc 的前四位,后两位补 0),返回 IFU,下一时钟沿上升时更新。当执行 jr 指令时,NPC 将从 GRF 的 RD1 端口(考虑转发)输出的值输出,返回 IFU 下一时钟沿上升时更新。
-
对于 jal 等需要将指令地址写入寄存器的指令,为什么需要回写 PC+8 ?
A:因为需要考虑编译优化,jal 的下一条指令是延迟槽中的指令,在 jal 执行前会被执行。如果回写 PC+4 的话,当出现 “jr $ra” 时,将会回到延迟槽,重复执行延迟槽中的指令。因此需要回写 PC+8。
数据冒险的分析
-
为什么所有的供给者都是存储了上一级传来的各种数据的流水级寄存器,而不是由 ALU 或者 DM 等部件来提供数据?
A:因为流水寄存器中的储存的数据时前一级已经计算出来的数据,在当前周期内时稳定输出的。而功能部件的输出是有延迟的,如果让这些部件提供数据,有可能再其在回写数据生成前就写入了错误的数据,导致数据波动。
AT 法处理流水线数据冒险
-
“转发(旁路)机制的构造” 中的 Thinking 1-4;
Thinking 1:如果不采用已经转发过的数据,而采用上一级中的原始数据,会出现怎样的问题?试列举指令序列说明这个问题。
A:例如:
addi $s0, $0, 4 andi $s1, $s0, 5 sw $s1, 4($s0)
Thinking 2:我们为什么要对 GPR 采用内部转发机制?如果不采用内部转发机制,我们要怎样才能解决这种情况下的转发需求呢?
A:因为需要使得 W 级保存的将要写入得数据及时反馈到 GRF 的输出端口,从而规避数据冒险。如果不采用内部转发,我们可以在 GRF 的输出端口加入多路选择器,将 W 级的数据进行转发。
Thinking 3:为什么 0 号寄存器需要特殊处理?
A:因为对 0 号寄存器的写入是无效的,如果不特殊处理,则会使得向 0 号寄存器写入的数据(无效输入)被错误转发,从而造成 BUG。
Thinking 4:什么是 “最新产生的数据”?
A: 距离当前需求者最近的流水寄存器中储存的数据。
-
在 AT 方法讨论转发条件的时候,只提到了 “供给者需求者的 A 相同,且不为 0”,但在 CPU 写入 GRF 的时候,是有一个 we 信号来控制是否要写入的。为何在 AT 方法中不需要特判 we 呢?为了用且仅用 A 和 T 完成转发,在翻译出 A 的时候,要结合 we 做什么操作呢?
A: 因为当 we 信号为 0 时我们就把 RF 写入地址置为 0,因此如果 “供给者的 A 不为 0”,就已经排除了 “写入信号为 0” 的情况。
在线测试相关说明
在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。
此思考题请同学们结合自己测试 CPU 使用的具体手段,按照自己的实际情况进行回答。
A:我是采用半随机生成半手动构造的方法。对于功能型指令,完全通过 python 脚本自动生成。并且为了提高数据冲突的概率,我们仅仅使用 0~7 号寄存器进行测试。对于跳转指令,我们先使用一定模板进行构建,然后为了增加数据冒险和控制冒险,我们又手动进行一定修改,使得测试样例尽可能更多的覆盖所有可能的情况