五级流水线冲突的解决以及算法在流水线上的运用

  • 实验目的
    1理解五级流水线中冲突的产生条件以及类型模块,分析冲突对于流水线正常操作的影响以及它们可能会带来的错误执行结果,同时寻求解决方案,并改进原有代码使得流水线可以正常运行。
    2通过理解各种算法的运用,即解析它们的流程需要的指令,在本实验中包括最小公倍数、最大公因数、64位加法、冒泡排序等算法的解析,然后在五级流水线上进行跑指令运算,类似于上一次对于init_test的指令测试集,然后在板子上进行验证。
    3、理解这3次项目来的整体思想,加深对于工程搭建以及五级流水线的理解,同时总结整一个项目下来的感想。
  • 原理分析
  1. Hazard冲突的理解

对于之前的五级流水线的工程构建,我们一直都没有谈到冲突这么一回事,那么为什么会突然冒出这个东西出来呢?或者说它的存在是累赘吗?或者只是单单为了增强流水线的某方面性能呢?显然答案是冲突是个非常严重的问题,之前我们之所以没有谈到这个问题,同时它也仿佛没有给我们带来什么困扰,是因为我们的指令代码几乎每一行的后面都加3了个Nop指令,这是解决冲突的方法,但显然太笨了我们不可能对于一般的指令都去刻意地添加Nop使得程序得以正常运行,同时如果我们在上一次写代码的时候有留意的话,也会发现其实一些指令的后面加了Nop与没加是一样,但有一些确实一定要加的,其实当时我们可能可以大致推测到只要后面的两三条指令与前面指令的寄存器没有关系,那么就没有必要添加Nop,都能想到这一步了,那么问题也就开始接近我们的主题了,这其实就是我们遇到的一个常见的冲突,前面的指令对于某一个寄存器进行了赋值,但是下一条指令立即就要求使用该寄存器的值,那么这时候就会使用到之前留给该寄存器的值,而不是我们想要的,因为我们上一条指令的值还没有等到它赋给该寄存器的时间周期,我们就已经对它进行取值了,这就是一种冲突,只是举个例子,下面我们进行详细的讲解。

         流水线的冲突对于具体的流水线来说,其实就是由于某些相关的存在使得指令流的下一条或者几条指令不能在指定的时钟准确执行。流水线的冲突通常包括了两种类型,第一是我们刚刚提到的数据冲突,我们可以理解为在指令执行时,我们的指令需要取到前一条指令还没有最终写到的寄存器的数值,这时就会取到错误的数值,专业一点的说法就是我们在安排指令时,当有关的指令(这里我们假定他们会使用到前面的指令处理的寄存器),靠得足够近时,它们在流水线中的重叠执行或者重新排序会改变指令读写操作数的顺序,使之不同于它们非流水实现时的顺序。我们可以简单借用一下老师上课时讲过的例子来看,左边是指令,右边是它们的一个执行周期列表,NG表示该指令出现了错误,而OK则表示可以正常运行,如图:

   

        可以看到在第一条指令在第一个时钟周期执行之后,第二个时钟周期到来时,我们开始执行第二条指令,因为它的加法指令用到了3号寄存器,而3号寄存器的值需要刚好前一条指令加法执行后的结果,所以当在第二条指令译码时,取到的3号寄存器的值不是前一条指令执行后的值,因为时间还没到!,同样对于第三条指令,我们对于3号寄存器的需求也刚好在第一条指令得到结果之前,以此类推,只有当第五条指令到来时我们才能准确读到3号寄存器的值,(前提是在此之前,有有对3号寄存器做出什么赋值操作,否则我们的值又会错下去!!),当然对于第四条指令,可能有参考一些其他流水线的设计会发现,为什么也是错误的,答案是因为我们无法保证读到的刚好是在赋值之后,可能我们在设计的时候可以弄成前半周期写完操作,后半周期读完操作,这样就可以完美解决问题了,也使得第四条指令可以成功执行,之前我们也就可以少写一个Nop。

         那么针对这个问题,我们的就解决方案其实大家肯定也大致了解,那就是重新定向数据流向,也就是说,我们在一些特定的操作上通过将存储器访问阶段或写回阶段的结果重新定向或者经过旁路到执行阶段来化解,可以说就是在产生出我们想要的计算结果之前,其他指令并不真正立即需要该计算结果,如果我们能够将这个计算结果从它产生的地方直接送到其他指令需要它的地方就好了,继续引用老师上课讲过的图片:

   

        可以看到我们对于第二条指令其实可以从第一条指令的执行周期的运算单元直接读到我们想要的值,那么对于第三条指令呢,我们是可以通过对第一条指令它的reg_C来直接读到需要的值,为什么不是从运算单元呢?额,因为时钟周期一直在跑,那个值也在跟着前进到下一级的,所以我们可以得到的结论就是,我们在判断需要的值对应于前面指令的哪一个地方取值时,是看指令周期的运算码是否与当前指令运算码一致的,一旦一样,就意味着那个运算值已经到达那个执行周期,我们就可以从它的寄存器中读到数值,这是我们取值的一个大重点!!!

          因此我们可以看到对于各种运算码我们需要进行分类,例如,对于在r1=r1+{val2,val3}类型的运算码:BN、BC等等会产生这种结果的情况下,我们根据判断操作码是与某个执行周期的操作码一致,就取到那个阶段的寄存器的值,判断可以看到如下:

    其实就是对于mem、ex、wb的判断操作码然后进行赋值,当然我们会注意到其中还有一些处理可能这里还没讲,因为会涉及其他的冲突没所以我们放在下面的那个详细讲一下。

         但是我们还会有例如r1=r2#r3 or r1=r2#val等等的类型,其实就是找到它们的操作码可能会产生的下一条指令对于这条指令的需求。同时要注意下它们分别在译码阶段给到reg_B以及reg_A的是哪个值,进而能够判断出是要检查哪个位置相等才需要对数据进行定向转移。下图是对于reg_B的处理:

          大致讲了数据冲突(大致讲完,其实还有很多细节我们会去处理,例如我们的各种操作码的分类等等的问题),但是我们如果足够细心的话,会发现,其实还有一个问题,因为我们一直是取到能够提前在运算单元取到的值,如果没有能够在合适的执行周期取到合适的值呢?那么问题就来了,例如我们的load指令,可以看到下图:

      因为load指令只有到达存储器级后才能读取到数据,因此结果不能重定向到下一条指令的执行阶段,也就是说它有两个周期延迟,只有过了两个周期延迟才能使用到正确的结果,对于这个问题我们需要的方案解决是阻塞化解,也就是说我们先把操作挂起阿里,直到数据有效,其实理解起来就是相当于加了Nop指令了,当然我们知道通过禁止某一级的流水线寄存器实现阻塞流水线,此时寄存器的内容应该不发生改变,当某一级流水线阻塞是,前面的各级也应该都阻塞,这样后续的指令才不会丢失。可以看到我们对于它的解决方法:

   

      接下来呢,我们还要来看一下另外一种冲突,是什么呢?当然是控制冲突了,其实对于它来说,我们主要就是在跳转方面会遇到这个问题,因为我们在此时会遇到流水线碰到分支指令或者说其他会改变PC值的指令,对于执行分支指令的结果我们可以分为两种情况,第一是分支成功,那么此时PC的值发生改变,变为分支转移的目标地址,在条件判定和转移地址计算都完成后,才改变PC值;但是一旦不成功或者说失败的或,我们的PC值需要保持正常递增,同时指向顺序的下一条指令。关于处理分支指令最简单的方法就是冻结或者排空流水线,可以看到我们对于它的处理在PC的值以及指令处理如下:

     

     

等等这些操作其实都是在冲掉因为控制冲突刷新的指令,当我们处在取址阶段,Branch指令及其标志位为true则Flush掉一条指令;跳转指令则直接跳转并处理pc值延后一个周期的情况,其余情况直接读取下一条指令,而对于译码阶段,如果为Branch指令且标志位为true则需要Flush掉当前内容;否则如果冲突出现,则需要根据不同的运算指令从不同的地方取出内容。

讲到这里大致对于我们的冲突也就讲完了,接下来是对于整体代码的改动,我觉得这一次的话是在上一次代码的基础上修改的,因此ip核都能用,只是到时不同算法需要改进一下指令代码的初始文件就可以了,其他的就是我们对于PCPU的主文件修改,大体就是按着我们的思路来修改,首先考虑数据定向移动,然后在这样一个整体的趋势下去注意load的影响,注意进行补充、阻塞。然后是控制冲突的解决,主要是对于PC端的影响;改动不大,主要精力是放在译码阶段的A、B寄存器的值的重定向赋值比较难而已。

  1. 实现程序的功能介绍

对于本次的指令集测试,我觉得在算法本身这里就不用多言了,因为要实现的代码算法我们如果要说的话大家都是比较熟悉的,之前的程序结构课程也就是这几种算法不断地运用而已,所以在本次讲解中我就不涉及算法的精妙之处,直接把它转化为二进制或者十六进制初始文件然后逐一进行读入i_mem的初始文件里面,然后运用我们上次项目的板极验证方法同样运行一次后查看最后各个地址的数值是否与提供的一致就行啦。注意因为我们解决了hazard问题所以我们在使用指令时就不用使用Nop进行隔断啦!

 

三、实验步骤

  1. 转化指令集的操作码,然后进行初始化文件的更新。

对于我们修改之后的hazard代码的验证,我们当然是通过不同的算法指令集来验证,那么在本次项目中,我们主要是对指令存储器以及数据存储器的IP核的初始化文件进行修改就行了,同时注意的是我们的指令集不再需要是加NOP来缓解冲突,所以我们只需要把操作直接转化为二进制或者十六进制进行初始化就行了,这类操作我们在上一次的实验项目报告中也已经详细说明了一下。所以可以看到我们对于五个复杂指令集的初始化文件(局部)如下图,教给大家一个小技巧,轻松获取我们的初始文件十六进制码,可能之前助教提供了那个文件也可以直接复制那些有效的操作码前面的数字,当然我们也可以把操作码保留,然后如果你还记得上一次的那个数据存储器的仿真文件输出的话,就可以相似地用这种方法输出指令代码,这时输出的都是十六进制的码数,你可以直接复制用上啦!

下面是我们的初始文件(包括指令集存储器的初始化以及数据存储器的初始化),当然对应于每一个算法它们两者的初始化都是需要同时进行改变的,具体看下面图:

图一:对应于64位加法的指令初始化(局部)

图二:对应于64位加法的数据初始化

图三:对应于冒泡排序的局部指令初始化

       因为指令集翻译其实还算是一种简单暴力的操作,所以我们不多说啦,直接给几张图大致参考一下就可以啦,就是根据复杂指令进行数值转化嘛,经过一系列的堆叠我们就把初始化都写完啦,接下来就是我们的仿真喽。

2、进行仿真,通过观察运行过程中的各种执行码的正确性以及寄存器的变化,同时读出最终的数据寄存器的地址对应的值进行比对。

3、进行板极验证,观察结果。详细见下面结果的分析。

 

三、实验结果

• Lab3:

  1. 综合的RTL电路图

      

 

                                        图一:流水线RTL图

      如图一所示,综合出的RTL图显示出CPU的通信关系,没毛病。

  1. 仿真结果

仿真这一次因为要验证的指令集较多,我就是按照之前验证init_test的方法生成最终的数据寄存器的文件然后与实验的结果文件进行对照,如果一致就说明仿真成功。

可以看到我们的init_test的结果:

同时我们的add64位加法:

而我们的gcm:

还有冒泡排序:

以及sort:

经过对照仿真全部正确,可以烧板啦!

4、开发板的显示效果图
Init_test:
正如之前项目报告那个功能简介已经说了,我们把一号波动开关设为地址输入的有效信号,具体的
地址信号在右边那一行的拨动开关(注意是那一排小的),然后二号拨动开关是 reset 信号,三号拨
动开关是 enable 信号,而按钮键是 start。所以实验开始时,我们先让 reset 置零,看到如下:
- 9 -
因为我们一开始的 pc 和地址都是 0,可以看到数据是 fffd。
接下来我们点击 start,然后通过拨动 enable 让程序停下来,记录数据后拨动它变成有效,再点击
start 使它继续执行,可以与上面的仿真数据相比较是对应着的。接下来我们验证执行后的数据存
储器的数据正确性,首先是将第一位拨动开关调至有效,然后点击 start,运行后然后拨动开关选
择地址,查看数据。(此时 pc 一直为 00)
通过查看数据我们可以看到当调到 00000010 时,即是选择 2 号地址输出 0005;而调到 5(00000101)时
输出 0041 合理!
当我们调到 1f 时显示 00c3,同时调到 2f 时显示 0069,都与上次实验最后的输出结果一致,得到验证。
对于我们的 64 位加法来说,我们同样经过运行 start 之后,通过查询地址,验证数值是否正确来评判板
极验证的正确性,这一点在下面几个指令集都是一样(也就是说,下面我们也是直接通过改 IP 核的数据
以及指令寄存器的值然后运用到上次写板极验证的代码烧写到板子上,然后通过在板子运行然后检验地
址对应的数据是否正确即可):
- 10 -
64 位加法:
对应于地址 00 的数据是 fffe 对应于地址 04 的数据是 ffff
对应于地址 08 的数据是 fffd 对应于地址 07 的数据是 0000
对于 gcm 的算法:
GCM:
对应于地址 03 的数据是 0008 对应于地址 04 的数据是 0060
- 11 -
对于 bubble sort:
对应于 05 地址的数据是 2369 对应于地址 0a 的数据是 0004
对于 sort:
对应于 02 地址的数据是 0001 对应于地址 09 的数据是 0011
经过验证板极验证时正确的。奈斯!

四、附录报告

对于功率报告我们由于没有改动所以遇上一次一样如下:

通过功率报告我们可以看到该实验项目中总共的功率0.273w以及它的一个分配,整体我觉得不算高耗能,还行。主要消耗在时钟管理上。同时IO口的输入输出也占据了比较大的部分,但是我们可以大概猜到时钟管理相比那些其他的逻辑门(它们甚至数量更多)耗能更大,它占到的比例更高,可能是影响实验运行的一个重要因素,不管是速度还是频率接受。

对于利用指数报告:

我们也可以看到占用较多的是MMCM时钟管理这一部分,包括前面的那个功率消耗也是它相对较高。应该说对于实验来说这个时钟管理显得极为占空间和功率。

而此次对于频率没有过分要求,也就按正常的100Mhz来运作,得到正常的时序报告,如下:

 

五、实验感想

对于本次实验我其实也想把它写成一篇博客,可供其他人参考,所以对于各个细节都写了一下,但可能还是有一些不足,只能不断努力去补足,其实把源代码改成hazard,最重要还是对于冲突的深入理解,而后才是对于指令集的验证,个人觉得只要结构写出来了,什么指令集不都是轻易代上去就可以解决了吗?所以本次项目就是为了让我们深入理解冲突的产生并思考怎么解决冲突而设计的,我觉得这个项目对于自己对于流水线的理解确确实实从表面达到了精华部分,但还是要不断学习,才能汲取更多有用的知识。

猜你喜欢

转载自blog.csdn.net/qq_41566923/article/details/81632025
今日推荐