简介
p6和P5的整体思路一样,区别在于P6将IM和DM模块放在了外部(课程组写好了代码),从而使得CPU更加纯粹,贴合实际。同时P6新增了乘除槽指令,由于乘除槽指令执行周期较长,因此我们在阻塞等方面要加以注意。废话不多说,我们开始吧。
IFU -> PC
在P6实验中,IM外置,因此在P5的基础上,我们要删掉这部分,保留PC的同时在顶层模块mips新增接口与外置DM信息交换:input [31:0] i_inst_rdata, //F级PC对应的32位指令output [31:0] i_inst_addr, //F级PC
D_CMP
P6新增指令BNE,需要在D_CMP模块完成比较。
assign Beq_jump = (CMPOp == `CMP_BEQ && RS_Data == RT_Data) ? 1'b1 : |
D_GRF
P6不需要$display("%d@%h: $%d <= %h",$time,PC,A3,WD);
def
这个模块其实本来没什么好说的,只要新增指令需要用到的变量我们在这里定义即可。在这里值得说明的是,虽然P6指令极多,但是笔者权衡之后依然采用每条指令独立考量的方式,没有像主流思路一样对指令进行分类。两种方式各有利弊,这里只能仁者见仁,智者见智了,举个例子。
assign GRFA3Sel = (cal_r | jalr | mf) ? `GRFA3rd : |
assign GRFA3Sel = (Add|Sub|Sll|Jalr|_And|_Or|Slt|Sltu|Mfhi|Mflo) ? `GRFA3rd : |
显然,第二段代码要比第一段代码冗杂的多,这是我采用的写法的缺点。但是你也很容易发现,第一种写法将分类和不分类杂糅了起来,导致在coding时极度混乱,容易出错;而第二段代码因为是每条指令独立考虑的,也就不存在这个问题了。个人不太喜欢这种易错感觉,当然大家如何架构还是看自己心意🫡。
E_ALU
P6对ALU的改动就是增加了新的指令的计算方式and,slt,sltu。
E_MDU
乘除槽的实现是P6的重点和难点。我们要实现8条乘除法相关指令:mult multu div divu mthi mtlo mfhi mflo,前4条指令是计算乘除法,并将结果存入HI,LO两个寄存器中,后4条指令是对HI和LO的读写。
我们把MDU模块当作特殊的ALU,对于前四条指令,MDU要执行很多个周期并且不需要输出计算结果,只要在时钟上升沿时将计算结果存入HI,LO寄存器即可。对于mfhi,mflo则需要正常输出。对于mthi,mtlo,在时钟上升沿时写入对应寄存器即可,只需要执行一个周期。
代码如下:
always @(posedge clk) begin |
注:以下提到的乘除法计算均为mult,multu,div,divu。
关于阻塞,之前我们提到了乘除法的计算周期较长,为了不因为乘除法计算的存在而拉低CPU的最短时钟周期,我们引入了MDU。因此当MDU模块正在计算时,所有不进入MDU的指令都可以正常流水,这也就导致了MDU的阻塞与自身息息相关。当MDU执行乘除法计算时,
- 出现了
mfhi,mflo,将指令阻塞在D级。 - 出现了
mult,multu,div,divu,直接终止运算因为接下来要覆盖 - 出现了
mthi,mtlo,将指令阻塞在D级。
当然课程组为了方便我们设计给了我们一些提示:
- 当E级指令是
mult multu div divu时,CTRL模块需要输出1个周期的1位start信号,表示乘除法计算开始; - 当start信号结束后,如果之前的计算指令是
mult multu指令,MDU模块需要连续输出5个周期的busy信号,表示正在计算HI寄存器和LO寄存器的值;如果之前的计算指令是div divu指令,则需要连续输出10个周期的busy信号; - 当start信号或busy信号为1时,不进入MDU模块的指令正常流水,要进入MDU模块的所有指令均阻塞在D级;
- 为了降低实现难度,保证MDU模块在进行乘除法计算时只会出现
mfhi mflo两种乘除法指令。
也就是说,对于乘除法计算指令涉及的阻塞,我们只要无脑阻塞在D级即可。
//CTRL |
//E_MDU |
//Stall |
这就完了吗?当然不是!观察一下mfhi mflo,他们读取hi和lo的值是要写入rd寄存器的,这就不可避免地带来了数据冲突,因此E_Tnew = (E_Mfhi || E_Mflo) ? 1 : 0;
如此,我们就顺利滑过了E_MDU,庆祝一下吧。
M_BE
我们知道,P6实现了DM外置,所以在CPU内部要实现和外置DM的数据交互。M_BE位于DM之前,处理store类指令,这也很好理解,毕竟store是向DM写入数据;相应地,M_DE位于DM之后处理load类指令,因为load是从DM取出数据存入GRF中的。
先来看M_BE。重要变量
output reg [ 3:0] M_Data_Byteen, //字节使能 |
四位M_Data_Byteen中的某一位为1代表对应的8位要写入数据,这里只要看仔细sw,sh,sb的Rtl语言正常翻译就行。
case (BEOp) |
M_DE
case (DEOp) |
Attention:[(16 * Addr[1] + 15) -: 16]这里是减,不要写成加法形式。千万别写成加法形式!!!
总结
剩下的就是在CTRL里面进行小修小补,在Stall里面完善一下T_new,T_use,在mips里面添加新增的对外交互接口并找到他们对应的连线。此外,所有新添加的指令,除了乘除类,都在P5有过类似的指令,照着之前的添加即可。
思考题
- 为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?
答:因为乘除法的效率比ALU其他运输运算的效率要低很多(5/10),不单独设计的话会让CPU整体效率降低,使用单独的HI,LO寄存器是因为乘除法需要两个寄存器来保存结果,和其他指令需要一个不一样。
- 真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。
答:在真实的流水线 CPU 中,乘法通常有若干个较小的乘法单元组成(组合逻辑),然后每个周期计算特定的几位,依次累加起来,于是会在几个周期后得到正确的最终结果;除法通常使用试商法,通常也是使用组合逻辑在一个周期内计算 4 位左右的商,经过 8 个周期正好可以计算结束。
- 请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的?
答:当Busy或Start信号为1并且D级为乘除法指令的时候阻塞。
- 请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑)
答:可以在sh和sb的时候只传入写使能信号和数据,不需要为他们单独设计电路,实现高内聚低耦合。
- 请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?
答:不是,是4字节。处理单字节 / 非连续字节数据时。
- 为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?
答:应当按照cal_r,cal_i,load,store,mult/div,branch,jump来分类指令,这样可以将同类型的指令在生成控制信号的时候统一处理。但是我没这么干,感觉这种分类标准不统一,在实现的时候反而要思前想后,相比于单个指令的实现,我还是觉得两者的复杂度相差不大。
- 在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
答:
mult $1, $2 |
将mfhi和mflo阻塞在D级
- 如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。
答:在手动构造数据的时候,在P5的基础上,加入连续操作乘除法的指令,并且在乘除法之间的5-10周期内加入其他指令,来检测新增的指令是否有错误。
Lyrics Sharing
爱是愚人的国度 |