RooKie_Z P6 Verilog流水线CPU设计文档
写在前面
这是RooKie_Z的P6流水线CPU设计文档,在课上测试中本CPU取得了 满分💯的成绩。
总体设计概述
本次要求实现的指令集为add, sub, and, or, slt, sltu, lui,addi, andi, ori,lb, lh, lw, sb, sh, sw,mult, multu, div, divu, mfhi, mflo, mthi, mtlo,beq, bne, jal, jr
,可以看到,除了一部分位移指令和异常相关的指令,指令集内各类型指令均有覆盖。由于P5架构较为完备,大部分指令已于P5课下实现,故本次设计仅添加乘除单元MDU,并结合课程组所给tb对数据通路稍作修改,并未进行大的重构。
数据通路
添加转发后的数据通路
关键模块介绍
总体而言,在总体架构不作大的修改的前提下,修改了一部分控制信号,同时添加了流水线各级寄存器和虚拟的阻塞控制单元,对于每一级流水线都设计了相应的译码器。
命名规则
- 对于各模块文件,均采用对于元件的文件命名,均为
流水线层级_元件英文简称
,例如D_GRF.v
,E_ALU.v
等,实例化时命名为大写首字母小写英文名
,例如Alu
,Grf
等 - 对于流水线寄存器文件命名为
两边的流水线层级_REG
,例如FD_REG.v
,DE_REG.v
,实例化时命名为大写英文层级
,例如FD
- 每一级的控制信号和临时的
wire
均以本级的名称开头,如E_ALUOp
,M_DMOp
等 - 在流水线中参与流水的信息遵从以下约定(以D级为例)
PC
和Instr
命名以流水线层级开头,如D_PC
,D_Instr
- 寄存器地址分别为
D_rs_addr
,D_rt_addr
,读出数据为D_rs
,D_rt
- 转发得到的寄存器数据(直接读取也视为一种转发)记作
D_FWD_rs_data
,D_FWD_rt_data
- 即将写入的寄存器地址为
E_A3
,即将写入的数据记作E_WD
,选择信号为E_WDSel
F级(取指)
PC(程序计数器)
信号名称 | 方向 | 功能描述 |
---|---|---|
NPC[31:0] | I | 待写入PC的指令地址 |
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
PC_WE | I | PC的写使能 |
PC | O | 当前指令地址 |
然后与mips_txt.v
交互获得当前指令
1 | F_PC _pc( |
FD_REG(F/D级流水线寄存器)
信号名称 | 方向 | 功能描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
flush | I | 寄存器刷新信号(阻塞时使用) |
F_PC | I | F级PC的指令地址 |
F_Instr[31:0] | I | 时钟信号 |
D_PC | O | D级PC的指令地址 |
D_Instr[31:0] | O | 32位的指令值 |
D级(译码)
- 本级需要处理来自E, M, W级的转发,其中W级为寄存器内部转发,另外两个分别是
D_FWD_rs,D_FWD_rt
,在CMP
和NPC
中需要用 - 本级的输入是来自F级的
PC
和Instr
,输出是D_rs
,D_rt
,D_Ext_Out
,D_PC
和D_Instr
,这些参与流水,还有输出到F级的NPC
- 本级元件较多,是最复杂的一级
D_GRF
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
A1[4:0] | I | 5位地址输入信号,将其储存的数据读出到RD1 |
A2[4:0] | I | 5位地址输入信号,将其储存的数据读出到RD2 |
A3[4:0] | I | 5位地址输入信号,将其作为写入数据的目标寄存器 |
RD1[31:0] | O | 输出A1指定的寄存器中的32位数据 |
RD2[31:0] | O | 输出A2指定的寄存器中的32位数据 |
WD[31:0] | I | 32位数据输入信号 |
clk | I | 时钟信号 |
reset | I | 同步复位信号,将32个寄存器中的数据清零;1:复位;0:无效 |
- 这次删去了
WE
写使能信号,因为如果我们不写寄存器,可以把A3
设为0,就相当于不写寄存器了
Ps :这是我灵光一现想到的办法,受了P4课上访存指令的启发,如果有指令要在读出之后针对数据判断是否写入GRF,那么WE信号不仅要一直参与流水,还要用于控制每级的转发与阻塞,利用0寄存器的特点就可以规避这一切的麻烦!!!
控制信号说明
1. D_A3
直接给出待写入寄存器的地址,弃用了在P4中利用A3Sel
进行选择的设计,这是因为P5采用分布式译码,每一级都需要A3
的信息,因此在CTRL
里面直接集成了
2. D_WDSel
控制信号值 | 功能 |
---|---|
WDSel_DMout |
选择写入寄存器的数据来自DM |
WDSel_ALUout |
选择写入寄存器的数据来自ALU运算结果 |
WDSel_PC8 |
选择写入寄存器的数据为当前流水线层级中的PC+8 |
D_EXT
将16位二进制数进行零扩展或符号扩展到32位
控制信号说明
控制信号值 | 功能 |
---|---|
EXT_unsigned |
零扩展 |
EXT_signed |
符号扩展 |
D_CMP(分支比较)
参考P3中CPU内BranchDefine子电路生成的CMP模块,来生成jump信号,判断分支是否跳转,link是否写入
目前实现了beq, bne, bgtz, bltz
等指令
代码实现:
1 |
|
端口说明
端口名称 | 方向 | 功能描述 |
---|---|---|
rs[31:0] | I | 转发后$rs 寄存器的值 |
rt[31:0] | I | 转发后$rt 寄存器的值 |
CMPOp[2:0] | I | 控制信号 |
jump | O | 指示分支是否跳转 |
D_NPC(次地址计算)
有了CMP之后,NPC的功能也更加简洁,只需根据NPCOp和jump信号输出NPC信号的值就行
实际上NPC
横跨了F级和D级两级,因为同时会输入F_PC
和D_PC
,前者正常跳转F_PC+4
用,后者则用于流水PC
值,后面转发PC+8
的时候用
这次我们弃用了P4中直接输出PC+4
的设计,转而让PC
信号参与流水,在需要转发时计算PC+8
端口说明
端口名称 | 方向 | 功能描述 |
---|---|---|
F_PC[31:0] | I | 32位输入当前F级地址 |
D_PC[31:0] | I | 32位输入当前D级地址 |
imm[31:0] | I | 32位立即数 |
jump | I | 指示b类型指令是否跳转 |
NPCOp[2:0] | I | 控制信号 |
RD1_rs[31:0] | I | $ra 寄存器保存的32位地址 |
NPC[31:0] | O | 32位输出次地址 |
控制信号说明
控制信号值 | 功能 |
---|---|
NPC_pc4 |
NPC = F_PC+4 |
NPC_b |
执行beq 等b类指令 |
NPC_jal |
执行j ,jal 指令 |
NPC_jalr |
执行jalr ,jr 指令 |
DE_REG(D/E级流水线寄存器)
输入
D_PC,D_Instr,D_Ext_Out
,此外上一级的$rs
和$rt
的值也要参与流水,即D_FWD_rs,D_FWD_rt
需要参与流水,这是由于指令序列sw, nop, add
的存在,sw
在M级需要使用$rt
的数据,但是在E级不会再进行转发(因为在D级已经转发过了),因此需要让正确的$rt
值参与流水输出
E_PC,E_Instr,E_Ext_Out,E_rs,E_rt
,ALU
需要这些信息
信号名称 | 方向 | 功能描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
flush | I | 寄存器刷新信号(阻塞时使用) |
D_PC[31:0] | I | D级PC的指令地址 |
D_Instr[31:0] | I | 32位的指令值 |
D_Ext_Out[31:0] | I | 16位立即数经EXT 扩展的结果 |
D_rs[31:0] | I | 32位的寄存器数据 |
D_rt[31:0] | I | 32位的寄存器数据 |
E_PC[31:0] | O | E级PC的指令地址 |
E_Instr[31:0] | O | 32位的指令值 |
E_Ext_Out[31:0] | O | 16位立即数经EXT 扩展的结果 |
E_rs[31:0] | O | 32位的寄存器数据 |
E_rt[31:0] | O | 32位的寄存器数据 |
E_ALU
- 相比于P4,ALU做了很大的变动,添加了
ALUSrcA
信号选择A运算数的来源,这是为了便于扩展sll
和sllv
类指令的原因取消了shamt
信号,shamt
信号从ALUSrcB
中选择进入ALU
中
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
A[31:0] | I | 32位输入运算数A |
B[31:0] | I | 32位输入运算数B |
ALUOp[4:0] | I | 控制信号 |
ALUOut[31:0] | O | 32位输出运算结果 |
控制信号说明
1. ALUOp
具体见代码,已实现绝大多数计算功能。
控制信号值 | 功能 |
---|---|
ALU_add |
执行加法运算 |
ALU_sub |
执行减法运算 |
ALU_or |
执行逻辑或运算 |
ALU_lui |
执行lui 指令 |
2. ALUSrcA
控制信号值 | 功能 |
---|---|
SrcA_rt |
对于sll 和sllv 等移位指令,选择$rt 的值 |
SrcA_rs |
对于其他大部分运算指令,采用 $rs 的值 |
3. ALUSrcB
控制信号值 | 功能 |
---|---|
SrcB_rt |
选择处理完转发后$rt 寄存器中的值进行运算 |
SrcB_imm |
选择立即数进行运算 |
SrcB_shamt |
使用{27'b0, E_ALUshamt} 得到32为扩展移位数 |
SrcB_rs |
考虑到sllv 指令要求可变的位移数,这里可以选择{27'b0, E_FWD_rs_data[4:0]} ,即$rs 寄存器中的数据作为移位数 |
E_MDU(乘除单元)
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 复位信号 |
MDUOp[2:0] | I | 控制信号 |
D1[31:0] | I | 32位输入运算数A |
D1[31:0] | I | 32位输入运算数B |
Start | I | 开始运算的指示信号 |
Busy | O | 是否处于运算过程中 |
HI[31:0] | O | 32位HI寄存器值结果 |
LO[31:0] | O | 32位LO寄存器值结果 |
控制信号说明
1. MDUOp
控制信号值 | 功能 |
---|---|
MDU_mult |
乘法运算 |
MDU_div |
除法运算 |
MDU_multu |
无符号乘法运算 |
MDU_divu |
无符号除法运算 |
MDU_mfhi |
mfhi 指令 |
MDU_mflo |
mflo 指令 |
MDU_mthi |
mthi 指令,把D1的值赋给HI寄存器中 |
MDU_mtlo |
mtlo 指令,把D1的值赋给LO寄存器中 |
EM_REG(E/M级流水线寄存器)
信号名称 | 方向 | 功能描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
flush | I | 寄存器刷新信号(阻塞时使用) |
E_PC[31:0] | I | E级PC的指令地址 |
E_Instr[31:0] | I | 32位的指令值 |
E_Ext_Out[31:0] | I | 16位立即数经EXT 扩展的结果 |
E_rt[31:0] | I | 32位的寄存器数据 |
E_ALU_Out[31:0] | I | 32位的ALU运算结果 |
E_MDU_Out[31:0] | I | 32位的MDU运算结果 |
M_PC[31:0] | O | M级PC的指令地址 |
M_Instr[31:0] | O | 32位的指令值 |
M_Ext_Out[31:0] | O | 16位立即数经EXT 扩展的结果 |
M_ALU_Out[31:0] | O | 32位的ALU运算结果 |
M_MDU_Out[31:0] | O | 32位的MDU运算结果 |
M_rt[31:0] | O | 32位的寄存器数据 |
M级(储存)
- 输入
E_PC,E_Instr
,此外上一级的ALUOut
参与流水,即E_ALU_Out,E_Ext_Out
需要参与流水,这是因为ALUOut
可能是待写入或读取的内存地址,另外,上一级的rt值需要参与流水,因此还需要输入E_FWD_rt
,这是因为sw
指令会向内存中写入$rt
的数据 - 输出
M_PC,M_Instr,M_ALU_Out,M_DM_Out,M_MDU_Out
M_DM(数据储存器)
DM
已经不需要自行实现,调用mips_txt.v
中的接口即可利用BE模块处理待写入数据,使其支持按半字、字节、字储存
利用DE模块处理DM返回的数据,使其可以按照不同要求存入寄存器
M_BE
信号名称 | 方向 | 功能描述 |
---|---|---|
BEOp[1:0] | 输入 | 控制信号 |
Addr[31:0] | 输入 | 地址信息,用于处理半字、字节 |
rt_data[31:0] | 输入 | 读取的寄存器数据,待处理 |
DMWr | 输入 | 写使能 |
m_data_byteen[3:0] | 输出 | 控制写入半字、字节的位置位置 |
m_data_wdata[31:0] | 输出 | 待写入数据 |
M_DE
信号名称 | 方向 | 功能描述 |
---|---|---|
DEOp[1:0] | 输入 | 控制信号 |
Addr[31:0] | 输入 | 地址信息,用于处理半字、字节 |
m_data_rdata[31:0] | 输入 | mips_txt.v 返回的DM中的数据 |
DMout[31:0] | 输出 | 处理之后的正确的读取数据 |
与接口进行交互
1 | // 与DM交互 |
W级(回写)
MW_REG(M/W级流水线寄存器)
信号名称 | 方向 | 功能描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
flush | I | 寄存器刷新信号(阻塞时使用) |
M_PC[31:0] | I | M级PC的指令地址 |
M_Instr[31:0] | I | 32位的指令值 |
M_DM_Out[31:0] | I | 从内存中读取的值 |
M_ALU_Out[31:0] | I | 32位的ALU运算结果 |
W_PC[31:0] | O | W级PC的指令地址 |
W_Instr[31:0] | O | 32位的指令值 |
W_DM_Out[31:0] | O | 从内存中读取的值 |
W_ALU_Out[31:0] | O | 32位的ALU运算结果 |
数据通路分析
指令 | opcode | funct | NPCOp | A3Sel | WDSel | EXTOp | GRFWE | ALUSRCB | ALUOp | DMWr | DMOp |
---|---|---|---|---|---|---|---|---|---|---|---|
add | 000000 | 100000 | NPC_PC4 |
A3Sel_rd |
WDSel_ALUout |
X | 1 | SrcB_rt |
ALU_add |
0 | X |
sub | 000000 | 100010 | NPC_PC4 |
A3Sel_rd |
WDSel_ALUout |
X | 1 | SrcB_rt |
ALU_sub |
0 | X |
ori | 001101 | X | NPC_PC4 |
A3Sel_rt |
WDSel_ALUout |
EXT_unsigned |
1 | SrcB_imm |
ALU_or |
0 | X |
lw | 100011 | X | NPC_PC4 |
A3Sel_rt |
WDSel_DMout |
EXT_signed |
1 | SrcB_imm |
ALU_add |
0 | DM_w |
sw | 101011 | X | NPC_PC4 |
X | WDSel_DMout |
EXT_signed |
0 | SrcB_imm |
ALU_add |
1 | DM_w |
beq | 000100 | X | NPC_branch |
X | X | X | 0 | X | X | 0 | X |
lui | 001111 | X | NPC_PC4 |
A3Sel_rt |
WDSel_ALUout |
X | 1 | SrcB_imm |
ALU_lui |
0 | X |
jal | 000011 | X | NPC_jal |
A3Sel_ra |
WDSel_PC8 |
X | 1 | X | X | 0 | X |
jr | 000000 | 001000 | NPC_jalr |
X | X | X | 0 | X | X | X | X |
本CPU采用分布式译码,在每一级均设有译码器,解码出各级所需信息
冲突处理方法
转发(Forwarding)
对于转发,我们直接采用 AT 法 + 暴力转发,首先要搞明白转发到哪,转发什么。 对于每一个流水线层级,我们要能够确定当前这一级正在执行的指令要写什么数据,向哪里写,因此就要维护 GRFWD (解决转发啥)和 GRFA3 (解决转发到哪)这两个值,我们转发需要去关注的也就是这两个数据,这些信号都可以从 Control 里面译码读出来 简单来说就是,我们需要在每一级都知道本级需要从哪读数据,要写到哪,要写啥,现在不知道没事,总之在这条指令从流水线消失之前,我们肯定知道,并且可以根据这些再经过判断做转发。
在每一个需要用转发数据的地方,我们去比较要用的数据的 GPR 地址和前面正在维护的要写的 GRFA3 的地址,如果相同,那就意味着我们要写的寄存器已经被用了,但是这时前面获得的值显然是错误的,这时候直接转发过去就好了
这里还要考虑优先级的问题,流水线寄存器生成的WD越靠近这条指令,得到的数据就越新,我们就越倾向于优先使用这些数据
利用 AT 法,如果不阻塞就意味着一定能够在使用该寄存器的值之前获得正确的值,如果我们要用的时候,这个正确的值还没有算出来,那肯定不行,这时候我们就阻塞,如果能算出来,那么之前转发的错误的值不用去管它,最后总能得到一个正确的值去覆盖原先错误的值
如果我们还不知道要写的值是啥,那这个时候 GRFA3
就给正确的地址, GRFWD
就给 32'bz
,这时还不能做转发,但是如果写的阻塞模块正确,这个值就不可能被转发,因为这种情况如果出现就已经被阻塞在 D 级了
综合考量各种指令序列,我们得到了转发的旁路:
- D级需求: E->D(如序列
jal-add
), M->D(如序列jal-nop-add
)(W->D隐藏于GRF的内部转发中); - E级需求: M->E(如序列
add-add
), W->E(如序列add-nop-add
) - M级需求: W->M(如序列
add-sw
)
具体设计见前文图片
代码实现
W to D
:
1 | // 寄存器内部转发 |
D级转发处理
:
1 | //D级写回数据 |
E级转发处理
:
1 | //转发 |
M级转发处理
:
1 | //转发 |
阻塞(Stall)
对于阻塞的处理,直接采用教程中的AT方法,设计一个 Stall
模块,专门负责处理阻塞时流水线寄存器的 flush
和 WE
信号就行
只在 D 级进行阻塞,阻塞控制器接受当前 D,E,M 级的指令输入,处理分析指令类别,算出当前D的Tuse,和E、M的Tnew,再进行相应的计算
代码实现:
1 | //阻塞逻辑 |
测试方案
指令集:add
, sub
, and
, or
, slt
, sltu
, lui
, addi
, andi
, ori
, lb
, lh
, lw
, sb
, sh
, sw
, mult
, multu
, div
, divu
, mfhi
, mflo
, mthi
, mtlo
, beq
, bne
, jal
, jr
测试目标
- 单计算指令行为
- 单存取指令行为
- 单跳转指令行为
- 计算/存取指令数据冲突阻塞转发行为
- 跳转指令与计算/存取指令数据冲突阻塞转发行为
- 新增存储指令行为
- 单指令乘除指令行为以及
Busy
信号表现 - 乘除指令阻塞行为
- 计算指令/乘除指令的数据冲突阻塞转发行为
具体实现分为两部分
功能性测试
采用课下提供的P6_L0_weak.txt
测试每条指令的功能,对于未进行测试的指令,进行手动测试
首先在P5的测试工具基础上进行简单的修改,由于指令类型相似,因此仅在P5的基础上添加了mfhi
,mflo
,mthi
,mtlo
,div
,divu
,mult
,multu
指令然后进行大范围的随机生成测试
我的测试方法是利用随机生成数据进行大范围测试,对于随机数据无法覆盖的点,通过手动构造特殊样例进行测试
对于更多情况,手动构造数据处理
单条指令功能测试:
ori测试:
1 | ori $t0, $t0, 1 |
add测试:
1 | ori $t0, $t0, 2 |
sub测试:
1 | ori $t0, $t0, 2 |
lui测试:
1 | lui $t0, 0x7fff |
beq测试:
1 | #三次跳转 |
j,jal,jr测试:
1 | jal A |
sw,lw测试:
1 | ori $t0, $t0, 4 #t0 = 4 |
之后对于各种边界情况和转发阻塞情况,进行覆盖测试:
提供一组超强测样例:
1 |
|
对拍输出一致,或许对吧……
思考题
为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?
因为乘法除法需要在多个周期内执行,且只用HI,LO寄存器。所以用单独的HI,LO寄存器可以减少阻塞周期,提高效率,因为在乘除法部件工作时ALU模块还可以进行其他指令的工作,做到高效率执行。
并且HI,LO寄存器不属于通用寄存器,和其他通用寄存器的用法不一致,不能通过非乘除法指令修改和访问,因此不需要置于GRF中,内置在MDU中即可。
真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。
乘法实现:
首先CPU会初始化三个通用寄存器用来存放被乘数,乘数,部分积。
部分积寄存器初始化为0。
判断乘数寄存器的低位是0|1,如果为0则将乘数寄存器右移一位,同时将部分积寄存器也右移一位。
在位移时遵循计算机位移规则,乘数寄存器低位溢出的一位丢弃,部分积寄存器低位溢出的一位填充到乘数寄存器的高位。
同时部分积寄存器高位补0。如果为1则将部分积寄存器加上被乘数寄存器,再进行移位操作。
当所有乘数位处理完成后部分积寄存器做高位,乘数寄存器做低位就是最终乘法结果。还有另一种乘法的方式:
只需两个寄存器,A[31:0],B[63:0],A初始化为被乘数,B初始化为乘数。
每一次取B的最低位,为1则将A[31:0]+B[63:32] -> B[63:32],为0则不操作。
每次将B >> 1,然后高位补0。除法实现:
与乘法的操作基本相反,首先CPU会初始化三个寄存器,用来存放被除数,除数,部分商。余数(被除数与除数比较的结果)放到被除数的有效高位上。CPU做除法时和做除法时是相反的,乘法是右移,除法是左移,乘法做的是加法,除法做的是减法。首先CPU会把被除数bit位与除数bit位对齐,然后再让对齐的被除数与除数比较(双符号位判断)。比如01-10=11(前面的1是符号位) 1-2=-1 计算机通过符号位和后一位的bit位来判断大于和小于,那么01-10=11 就说明01小于10,如果得数为01就代表大于,如果得数为00代表等于。如果得数大于或等于则将比较的结果放到被除数的有效高位上然后再商寄存器上商:1 并向后多看一位(上商就是将商的最低位左移1位腾出商寄存器最低位上新的商)如果得数小于则上商:0 并向后多看一位然后循环做以上操作当所有的被除数都处理完后,商做结果被除数里面的值就是余数。
请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的?
对于stall信号,我增加了如果Start或者Busy信号有效,且D级时mfhi,mflo,mthi,mtlo时进行阻塞。
请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑)
清晰性方面,按字节使能相当于onehot编码,1为写入,0为不写入,直观清楚。
统一性方面,对于各种处理内存指令只需设置好字节使能信号就可以控制数据的写入,不用通过取出该字的数据后再次拼接,对各种指令的操作表现的一样。
请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?
在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是同一节,因为读写所使用的地址都来自上一级的ALUresult,但是对应的字节不能保证一样,因为写入的和读出的有特定的模块控制。
在有sb,sh这种不是对于完整字节操作的指令来说, 按字节的读写效率会更高,如果按字处理,需要先从内存中读出该字节的所存储的数据,接下来根据需要进行拼接,最后组成完整的字节存入地址中,从内存中读出所消耗的组合逻辑延迟会降低效率,以字节读写就会消除这方面的影响。
为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?
主要采用了指令分类的方法,P6 完全沿用了 P5 的分类方法,新增的指令对应的特点都没有脱离这些分类,因此对于每条指令而言,只需译码后将其加入对应的分类,数据通路部分和 P5 完全类似,转发部分完全不用改,暂停部分只需添加一个因乘除块而导致的暂停。
在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
冲突方面,主要是数据冒险和控制冒险,分别通过暂停转发以及比较前移+延迟槽解决。
具体而言:
R类型Rd(写),后续指令Rs或Rt(读)
使用旁路转发,以add为例
1
2
3add $t0,$t1,$t2 add $a0,$t0,$t1 M到E级转发
add $t0,$t1,$t2 nop add $a0,$t0,$t1 M到D级转发
add $t0,$t1,$t2 nop nop add $a0,$t0,$t1 WB向D级转发load指令Rt(写),隔条指令Rs或Rt(读)
1
2
3lw $t0,0($0) add $t0,$t0,$t0 stall一个周期,WB向E级转发
lw $t0,0($0) nop add $t0,$t0,$t0 WB向E级转发
lw $t0,0($0) nop nop add $t0,$t0,$t0 WB向D级转发mult、multu、div、divu和mfhi、mflo、mthi、mtlo
如果E级的Busy或Start信号有效,如果是其他非乘除法相关指令,不会阻塞,使用ALU即可,如果是乘除法相关指令,如果是mfhi、mflo、mthi、mtlo,将它们阻塞在D级,直到乘除法结束;如果是mult、multu、div、divu,清空乘除法模块中的操作,将D级流水到E级。
1
2mult $t1,$t2 mflo $a0 会阻塞直到乘除法结束
mult $t1,$t2 div $a0,$a1 mult指令被div指令覆盖如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。
数据生成器采用了特殊策略:单组数据中除了 0 和 31 号寄存器外,至多涉及 3 个寄存器。一方面,这样产生的代码中,邻近的指令几乎全部都存在数据冒险,可以充分测试转发和暂停;另一方面,当测试数据的组数一定多,几乎涉及了每个寄存器,避免了只测试部分寄存器。此外,所有跳转指令都是特殊构造的,不会进入死循环的同时如果跳转出错可以输出中体现。
对于一些会产生异常的指令,为防止 MARS 报错,进行了一定的规避,比如除法不会去生成有关0号寄存器的除法,lw,sw保证是4的倍数等等。
RooKie_Z P6 Verilog流水线CPU设计文档