计算机组成原理P6

流水线CPU设计文档(多指令版)
构建思路
本次流水线CPU的搭建在上一次的基础上多增了很多指令,其中包括计算型指令,存储型指令,跳转型指令。对于新增指令,我们其实只需考虑增加这条指令后要修改原来电路的哪些东西,想明白这一点之后,我们对代码的修改便会变得得心应手起来。
数据通路图
再次附上数据通路图和logisim里的单周期CPU电路搭建图。
数据通路:
logisim单周期CPU电路:
通过这两张图,我们对整个架构更加清晰,增加指令的时候也可以对着图来看有哪些信号或者模块需要修改
修改思路
- 计算型指令(由于乘除相关指令要求我们模仿实际上的延时,因此乘除相关指令不放在此):对于这一类指令,通过数据通路我们可以发现,要修改的模块只有
alu
模块,我们只需对其进行相应的扩展并修改对应的控制信号即可。具体来说,我们可能需要对regwe
,regdst
,aluslt
,aluopt
,memreg
这些控制信号进行修改。 - 存取型指令:对于这一类指令,我们可能需要对
alu
模块进行一定的修改,因为存取的地址很可能是alu
的计算结果,其次由于这次的指令新增了sb
,sh
,lb
,lh
四条指令,所以我们需要用一个四位的MEM写信号来控制写入数据的相应位数的数据是否写入存储器。具体来说,我们可能需要对regwe
,regdst
,aluslt
,aluopt
,memreg
,memwe
,loadwe
这些控制信号进行修改,而其中memwe
需要扩展到四位,而如果后续(如课上)还要新增这一类指令的话,可能涉及到一些新增的算术运算,那我们还需要扩展alu
模块。 - 跳转型指令:在本次课下设计中新增的跳转型指令只有
bne
指令,而这条指令的实现与beq
几乎一样,因此课下对这类指令的新增没有什么好说的。但我们仍然要明白新增这一类指令基本的修改方向。这类指令既可能涉及到运算,又可能涉及到存储,还一定会涉及到跳转,因此我们要将alu
以及npc
模块的修改全部纳入考虑范围内,又因为我们设计的是流水线CPU,因此如果涉及运算模块,我们需要将其加到D级,同时还可能需要修改regwe
,regdst
,alust
,aluopt
,memreg
等控制信号。 - 乘除型指令:由于我们需要模拟真正乘除法部件执行乘除法的延迟,因此需要在E级新增一个模块,而
hi
和lo
两个寄存器也放在这个模块里。这次课下要求执行乘法的时间为5个时钟周期,执行除法的时间为10个时钟周期,我们只需要拿一个计数寄存器来记录当前是第几个周期,到达周期之后将相应的数据写入相应寄存器即可(针对纯粹的乘除类指令)。而针对写和读hi
或lo
指令来说,我们不需要模拟这个延迟,直接写入和读出并参与流水线的其他步骤即可,也因此需要考虑他们的控制信号的变化。
冒险
仍然采用A-T法来解决该问题,只需将新增指令的Tnew和Tuse算出即可。满足A-T的条件,我们就将相应数据进行转发,不满足我们就阻塞一周期。而相应的数据也不会太复杂,其实我们仔细思考可以发现,有写入功能的指令他们写入的数据无非就是四种,一种是mfhi
和mflo
指令,它会写入从hi
或lo
寄存器中读取的数据,一种是jal
,它会写入下一条指令的地址,一种是ALU计算型,他们回写入ALU的计算结果,一种是lw
,会写入MEM的读取值,况且再结合上阻塞会发现,如果我们当前指令是与lw
产生冒险,那一定会阻塞到lw流水到M级或W级,而那时便可将MEM读取值转发,因此我们注意将下一条指令的地址以及ALU的结果流水下去即可。
还有一点需要注意的是对于一些不用相关数据的指令例如jr,我们不应把他们的Tuse设为0,而是应该设为一个尽可能大的数,不然会产生无谓的阻塞,使得运行周期加长。
控制器
我们可以采用两种译码方式,一种是分布式译码,一种是集中式译码。集中式译码需要在某一级译码完成后将控制信号流水下去,这样流水的信号数量会很大,因此我采用的是分布式译码的方式,也就是在每一级对在该级的指令译码一次产生我所需要的控制信号即可,因此这样我会将控制器实例化4次。在控制器中除了单周期CPU里对应的控制信号以外,还需要加入转发的控制信号,我对Tuse和Tnew的计算也放进了控制器里。
控制信号
regwe=add|sub|ori|jal|lui|lw|lh|lb|and_|or_|slt|sltu|addi|andi|mfhi|mflo
regdst=(jal)?2:(add|sub|and_|or_|slt|sltu|mfhi|mflo)?1:0
aluslt=add|sub|beq|and_|or_|slt|sltu|bne|mult|multu|div|divu
1 | aluopt=(sltu)?6: |
1 | memreg=(mfhi|mflo)?3: |
1 | assign D_rs_Tuse=(phase==2&&add)?1: |
1 | assign D_rt_Tuse=(phase==2&&add)?1: |
1 | assign Tnew=(phase==3&&add)?1: |
测试方案
基本指令:
1 | ori $1,$0,3 |
跳转:
1 | ori $1,$0,0x1011 |
1 | ori $1,$0,0x301c |
思考题
为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?
- 因为我们要模仿真正乘除法部件执行乘除法的延迟,如果整合进了ALU模块,那么因为这个延迟可能会阻塞其他指令多个周期,导致运行的效率十分低,而设计单独的乘除法部件就可以不影响其他指令的执行。
- 如果不设置单独的HI、LO寄存器,那么因为乘除法的延迟,当乘除模块到达时间想要写入寄存器的时候,这时可能有其他指令也想要写入寄存器,造成矛盾,况且HI、LO寄存器内的值只供读出到某个寄存器里,而非读出来做运算,因此需要独立出来。
真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明
真实的流水线CPU中,乘法是由一级一级的门级电路搭建来的,因此是一个周期一个周期的传下去,每个周期计算特定的几位,最后走完整个乘法模块的时候得到最终结果。而除法使用试商法,通过组合逻辑在一个周期内计算4位左右的商,经过8个周期结束计算。
请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的
当D级指令为乘除模块相关指令(即mult,multu,div,divu,mfhi,mflo,mthi,mtlo)时且E级的Busy信号为1时,阻塞D级。
请问采用字节使能信号的方式处理写指令有什么好处
首先可以很清晰地看到具体是哪几位要写入,并且针对sw、sh、sb都使用同样的写使能信号,不用引进多的使能信号增加模块复杂性。
请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢
是同一字节,但不是一字节。在只需要对字节或半字访问时,按字节访问内存性能更由优势。
为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助
首先命名上规范,这样可以让自己的代码较为清晰,容易查错。除此之外,将控制信号按指令的类别来定义和分类,这样考虑某类指令的时候,不需要去考虑其他的控制信号,也不用每个控制信号都要囊括所有指令。
在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
- 存取和运算指令——转发+阻塞解决
1 | ori $1,$0,3 |
- 乘除和存取指令——转发+阻塞解决
1 | ori $1,$0,3 |
- 运算(存取)和跳转指令——阻塞+转发解决
1 | ori $1,$0,0x301c |
如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果
由于数据生成器生成的样例可能前后关联性不够强,因此我手动构造了一些样例。这些样例首先要保证针对每一条指令都将其的所有情况考虑完整,比如对于跳转指令就不能只考虑向后跳转还要考虑向前跳转。除此之外,由于流水线CPU很重要的就是解决冒险,因此还要考虑不同指令间的冒险,而这有可能发生在同类型指令,也可能发生在不同类型指令,因此两种情况我们都需要考虑到。在设计不同类型指令冒险的代码的时候,在保证前面的测试正确的情况下,可以只挑选一个类型里的典型指令出来,不必一个类型里的所有指令都拿出来设计一遍,因为同一类型下的转发和阻塞实现是相同的。
- Title: 计算机组成原理P6
- Author: Cdostan
- Created at : 2025-02-09 21:20:31
- Updated at : 2025-02-10 18:18:41
- Link: https://cdostan.github.io/2025/02/09/计组/计组P6/
- License: This work is licensed under CC BY-NC-SA 4.0.