流水线CPU设计方案
概述
没想到P6意料之外的很轻松,只要加指令就行了,而这一届课程组给的指令巨好写,只有一个乘除模块需要花点时间。
指令说明
本次在P5包含的指令基础上增加了十八条指令(这也太少了,感动)。
R型计算型指令
and
or
slt
sltu
改一改ALU和控制信号就解决了,巨方便。
I型计算型指令
andi
addi
同上。
B型指令
bne
跳转可以出不知道多少复杂的题,课程组却选择了最简单的一个。只要加一个控制信号就行了。
访存指令
lh
lb
sh
sb
本来这个应该算是个需要思考的点的,结果课程组把IM,DM分出来之后只要确定字节使能信号和输入的数据就可以了。
乘除指令
mult
multu
div
divu
mfhi
mflo
mthi
mtlo
最需要动脑子的一集,不过看了教程也不难。
功能模块设计
绝大部分地方跟P5一样,所以只选新增的展示。
MDU(乘除模块)
这是一个时序逻辑模块,根据课程组要求,为了模拟乘除法执行时间长的现象,我们要在五(十)个周期后才能完成操作,不过要注意的是,因为E级流水线寄存器不会等着乘除模块(甚至阻塞时也不会,因为E级是清空),所以必须在指令刚到(即start == 1
时)的时候就把需要用到输入的运算全部做完。我的处理是在内部设置HI_tmp
和LO_tmp
两个寄存器,计算时直接把值存到这两个寄存器里,计数器数到0时再移到正式的HI
和LO
寄存器。
信号名 | 方向 | 描述 | 位宽 |
---|---|---|---|
clk | input | 时钟信号 | 1 |
reset | input | 复位信号 | 1 |
start | input | 开始信号,触发运算 | 1 |
MDUOp | input | 乘除单元操作码 | 4 |
A | input | 第一个操作数 | 32 |
B | input | 第二个操作数 | 32 |
out | output | 输出结果,根据操作码选择的寄存器值 | 32 |
busy | output | 繁忙信号,指示MDU是否正在工作 | 1 |
DataExt
为lh
和lb
量身定做的模块,
信号名 | 方向 | 描述 | 位宽 |
---|---|---|---|
A | input | 访存地址最后两位,用于选择字节或半字位置 | 2 |
Din | input | 输入数据 | 32 |
Op | input | 数据扩展操作码 | 3 |
Dout | output | 输出扩展后的数据 | 32 |
根据Op决定是哪个指令,再根据A决定读到的数据是什么。
搭建转发数据通路
为了方便,我直接在E级把乘除模块的输出和ALU的输出用选择器拼在了一起,所以二者完全共享相同的数据通路,不需要考虑任何转发的问题。
阻塞信号的生成思路
因为乘除法的执行必须通过mfhi
和mflo
才能影响其他指令,同时课程组保证乘除后一定会接这两个指令,所以我们可以简单粗暴的把乘除法相关的阻塞信号写成:
|
|
然后再把这个信号和其他阻塞信号或起来就行了。
测试方案
依然是手写了若干测试代码,把新增的指令都测到了,其中最上面那一块是我测出来的bug,mtlo错误地把输入口B而非A存入了LO
寄存器。
|
|
思考题
- 为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器? 答:乘除法的时间消耗远大于加减等运算,为了保证性能,我们选择将其独立出来,用多个时钟周期计算。独立的HI、LO寄存器作为乘除模块与其他部分的桥梁,符合高内聚低耦合的设计原理,如果不这样做,乘除模块的多执行周期很可能导致大量冒险的出现。
- 真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。 答:将乘除法分成多个步骤,用多周期去实现,或者分散到流水线各级逐步实现。
- 请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的? 答:如果当前Start或Busy为1同时D级当前指令为乘除指令则阻塞。
- 请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑) 答:直接用信号的形式展现出了当前操作作用的内存部分,一目了然。同时抽象出了不同访存指令的根本不同,形式统一。
- 请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢? 答:不是,事实上我们只是把一字节从读写的字中拆出来。当内存访问局部性差,数据小且存放位置离散时,字节读写效率可能更高。
- 为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助? 答:对于指令太多的问题,我使用了分类的方法,将功能和执行过程相近的指令放在一起,使得生成控制信号的时候一目了然。此外,我用多路选择器将不同的写入数据合在一起,共用转发数据通路,大幅减少了处理冒险时的复杂性。
- 在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
答:
mfhi
和mflo
与读取寄存器的指令产生的数据冒险,使用转发解决。还有乘除相关指令彼此之间的数据冒险,用阻塞解决。测试样例如下。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
ori $21,$0,0xabcd mtlo $21 nop nop mfhi $3 andi $8,$19,0x2f4 mflo $1 lui $s0,0xf19a ori $s0,$s0,0x0233 lui $s1,0x2da3 ori $s1,$s1,0x4d14 mult $s0,$s1 mflo $s0 mfhi $s0 mthi $0 mfhi $s0 multu $s0,$s1 mflo $s0 mfhi $s0 mtlo $0 mflo $s0 div $s0,$s1 mflo $s0 mfhi $s0 mthi $0 mfhi $s0 divu $s0,$s1 mflo $s0 mfhi $s0
- 如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。 答:首先保证所有指令都在测试范围中,随后要重点考虑边界条件和不同指令之间可能冲突的地方。例如对于乘除指令要测试multu和divu对于负数的运算,还有后面接mf指令的阻塞情况。