P6_L0_document
P6_L0_document
设计草稿
设计说明
支持如下指令集:
add, sub, and(op==0, func==100100), or(func == 100101), slt(func == 101010), sltu(func == 101011), lui ,addi(op ==001000), andi(op==001100), ori ,lb(op == 100000), lh(op==100001), lw, sb(op == 101000), sh(op == 101001), sw, mult(func == 011000), multu(func==011001), div(func == 011010), divu(func == 011011), mfhi(func == 010000), mflo(func == 010010), mthi(func == 010001), **mtlo(func == 010011) ** ,beq, bne(op == 000101), jal, jr
的五级流水线CPU,且不考虑溢出异常。
设计要求
IM
和DM
模块外置,不用实现。需要实现单独的乘除法模块和数据扩展模块。
mips.v顶层模块的端口发生了改变:
信号名 | 方向 | 描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
i_inst_rdata[31:0] | I | F级对应的32位指令 |
m_data_rdata[31:0] | I | M级从数据存储器的读出相应数据 |
i_inst_addr[31:0] | O | 需要进行取指操作的流水级PC(一般为F级) |
m_data_addr[31:0] | O | 待写入/读出的数据存储器(DM)的相应地址 |
m_data_wdata[31:0] | O | M级待写入数据存储器DM的相应数据 |
m_data_byteen[3:0] | O | 四位字节使能 |
m_inst_addr[31:0] | O | M级PC |
w_grf_we | O | W级GRF写使能信号 |
w_grf_addr[4:0] | O | W级GRF待写入寄存器编号 |
w_grf_wdata[31:0] | O | W级GRF中待写入数据 |
w_inst_addr[31:0] | O | W级PC |
新增模块
乘除模块
为了支持mult
、multu
、div
、divu
、mfhi
、mflo
、mthi
、mtlo
等乘除法相关的指令,需要在E级独立设计乘除功能部件。
MulDiv
信号名 | 方向 | 描述 |
---|---|---|
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
start | I | 乘除法开始信号 |
Ope1[31:0] | I | 操作数一 |
Ope2[31:0] | I | 操作数二 |
Opeop[1:0] | I | 操作的类型 |
Hiout[31:0] | O | hi寄存器的值 |
Loout[31:0] | O | lo寄存器的值 |
busy | O | 处理相应乘除法的延迟信号 |
在乘除模块中为了实现要求的延迟的效果,采用此前的状态机的设计,即在乘除模块内置一个状态量cnt
用来记录还需要延时的周期数,当cnt
为0时,假如start_E
为1,那么此时将开始进行乘除运算,注意:此处要用一个临时的reg
变量存储当前乘除运算的结果,不可等到向hi
和lo
寄存器写入值得周期再计算(因为那时Rs
和Rt
早已面目全非了),并将busy
置1,将cnt
置相应的延时周期数(乘法5,除法10)。当cnt
为1时这时候需要结束相应的延时了,将临时变量中的值写入hi
和lo
中,并且将busy
置0,cnt
置0,重新等待下一个乘除运算。
字节使能模块(BE)
端口定义:
信号名 | 方向 | 描述 |
---|---|---|
LTwoAW_M[1:0] | I | 写入地址的最低两位 |
MemWrite_M | I | 写使能信号 |
BEOp_M[2:0] | I | 控制写入的方式:000:sw;001:sh;010:sb |
WD_M_final[31:0] | I | 需要处理的写入数据存储器中的数据 |
m_data_byteen[3:0] | O | 控制写入数据存储器中数据的位置 |
m_data_wdata[31:0] | O | 进行处理后的写入数据 |
数据扩展模块(DE)
进行将从数据存储器读出的数据进行符号扩展,例如lb
,数据扩展模块输入数据寄存器的32位数据,根据ALU计算出来的地址最低2位从中取出特定的字节,并以该字节的最高位为符号位做符号扩展。
端口定义:
信号名 | 方向 | 描述 |
---|---|---|
LTwoAR_M[1:0] | I | 读出地址的最低两位 |
Din[31:0] | I | 输入的32位从数据存储器中读出的数据 |
DEOp_M[2:0] | I | 数据扩展控制码:000:无扩展(lw);001:无符号字节数据扩展(lbu);010:符号字节数据扩展(lb);011:无符号半字数据扩展(lhu);100:符号半字数据扩展(lh) |
Dout[31:0] | O | 扩展后的32位数据 |
流水线寄存器堆
相应的由于我采用的是集中式译码,因此新增的乘除槽、字节使能模块和数据扩展模块所需要的选择信号等需要进行相应的流水,即在E级流水线寄存器堆需要增加start、Opeop、selMvfr、BEOp和DEOp信号的流水,而M级流水线寄存器堆需要增加BEOp、DEOp信号的流水。
阻塞和转发
对于新增的and
、or
、slt
、sltu
直接并入P5的NorR
类指令即可,并在ALU模块中对应相应的选择信号增加相应的运算;addi
(注意:该指令的立即数扩展是符号扩展和其他的I类指令的0扩展不同)、andi
并I类指令;sb
、sh
并入Save
类指令;lb
、lh
并入Load
类指令;bne
并入Bjump
类指令。这些类指令的阻塞和转发在P5中都已经得到了相应的实现,而对于乘除法相关的指令mult
、multu
、div
、divu
、mfhi
、mflo
、mthi
、mtlo
等指令将按照对于相应的寄存器的存取特性,分为MulDiv
、Mvfr
、Mvto
三类,根据AT模型做好相应的Tuse
和Tnew
分析后即可得到正确的转发,但是由于乘除槽的特性,假如乘除槽正在工作即start_E
或busy_E
任意一个为1时,那么此时如果此时在D级的指令是同乘除槽相关的(MulDiv
、Mvfr
、Mvto
),便需要将该指令阻塞在D级,因此阻塞信号还需要再添加一种乘除槽导致的情况(stall_MulDiv
)。
测试方案
典型测试样例
计算、访存类指令测试
先通过对除了分支、跳转之外的计算访存指令进行测试,该部分测试由python代码自动生成,大致生成思路是,连续枚举4条连续指令,将NorR
、I
、Save
、Load
、MulDiv
、Mvfr
、Mvto
、LUI
等8种类型(由于lui指令与I类型指令类似,于是将其同I类型指令合并)的指令进行排列组合(每一个类型随机取出一条指令),总共有2401种排列。为了增加冲突发生的概率,将被读写的寄存器范围调整到有限的5个,当测试样例足够多时,基本上可以覆盖所有的冲突情况。
生成代码:
1 |
|
分支、跳转指令测试代码:
1 |
|
思考题
1.为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?
1.乘除法在实际的实现中的延迟比ALU模块大得多,假若整合进ALU,那么CPU的整体周期数将大幅提升。
2.独立的HI、LO寄存器可以使得乘除法指令同其它的指令并行运行,提高了CPU的运行效率。
2.真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。
1.乘法通常利用若干个较小的组合逻辑的乘法单元组成,这些乘法单元你内部通过若干个全加器构成,然后每个周期计算特定的几位,依次利用全加器累加起来,便能在几个周期后得到正确的最终结果。
2.除法通常使用试商法,使用组合逻辑在一个周期内计算出4位左右的商,经过8个周期正好可以计算结束。
3.请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的?
将乘除槽产生的busy
信号和当前处于E级的指令的start
信号传入产生阻塞信号的模块中,当乘除槽正在工作即start
或busy
任意一个为1时,那么此时如果此时在D级的指令是同乘除槽相关的(MulDiv
、Mvfr
、Mvto
),便需要将该指令阻塞在D级,因此阻塞信号还需要再添加一种乘除槽导致的情况(stall_MulDiv
)。
4.请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑)
1.清晰性:使用字节使能信号使sw
、sh
和sb
指令的实现更加清晰,例如sw
指令全写入,即对应的4个字节的使能信号值均置1,相应的sh
和sb
指令也是类似的形式。
2.统一性:使用字节使能信号便于将新增加的sh
和sb
以及上机过程中可能遇到的奇葩写入指令同原有的sw
指令的数据通路相统一,并且将决定写入位置与否提前在数据存储模块外决定好也体现了模块功能的统一性。
5.请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?
1.不是。从DM获得的数据是计算地址所在字的数据,向DM写入的数据是对应需要写的字节改动,不需要写入的字节是保持原状,因此相当于是写入一个针对原有地址对应数据的只改变对应字节的数据。
2.在一些语言中存在一些只占一个字节的数据类型如,C语言中的char
,对于这些数据的处理按字节访问内存相较于按字访问就会更有优势。
6.为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?
1.将指令按照不同行为分为NorR
、I
、LUI
、Bjump
、Jjump
、Rjump
、Save
、Load
、MulDiv
、Mvfr
、Mvto
等类型,将不同指令的控制信号的产生和数据冲突的处理抽象为这些类型指令的统一行为,进而简化了相应的控制信号的产生以及阻塞和转发的实现,并且对于一种大类型下可能的小类型指令的不同控制信号则通过控制信号的指定输出以及宏定义定义的选择来清楚明了的反映不同指令的行为。
2.这样一来,上机时除非是指令的行为极其特殊不然都可以归入已有的指令大类型中,只需要对于特有的控制信号的输出以及运算进行修改就可以了,省去了同类型指令的阻塞和转发排查,使编码具有高内聚低耦合性,同时宏定义使代码的可读性提高,便于bug的排查。
7.在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
控制模块输出写入内存信号RegToMem
忘记了将新增的sh
、sb
指令添加上,导致出现写入地址的错误以及写入数据的错误,通过编写相关的测试样例如下:
1 |
|
定位到可能出现问题的地方并进行相应的修改。
由于新增的乘除槽的特殊性,需要特别关注多个乘除法相关指令的连续出现的情况下阻塞和转发的实现是否正确,构造出如下的样例:
1 |
|
找到了乘除槽模块内进行运算选择时出现了宏定义错乱的问题。
8.如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。
我采用的是半随机生成半人工生成的方式,利用随机生成合法的不含有分支和跳转指令的样例(如存取指令的地址合理性、除法运算中除数为0的处理等),对于分支和跳转指令,采用人工手动生成的方式,在P5手搓的测试样例的基础上,将原来的对于R、I类型指令的运算更改为新增的乘除指令和按字节存取和按半字存取的指令。
随机生成的数据仍存在覆盖率不足的情况,在同一种排列方式下,寄存器冲突的排放方式仍存在可能的潜在冲突,除此之外,对于同一类型的微弱差别也不一定能够在一次随机生成的数据中完全覆盖到,但是对于多次随机生成的数据可以覆盖尽可能大的范围。
9.[P5、P6 选做] 请评估我们给出的覆盖率分析模型的合理性,如有更好的方案,可一并提出。
由于是选做,所以你懂的。