Loading

P4 流水线CPU(logisim搭建)

一、写在前面

  • 首先,何为流水线CPU,流水线CPU和单周期CPU有什么差别?

    单周期CPU上所有指令都在一个时钟周期内完成,所以其时钟周期一般较长(能够完成最慢的指令),吞吐量不高。出于增大吞吐量的考虑,引入了流水线CPU,同一时刻有多条指令在其上运行,因此理论上五段流水CPU的吞吐量是单周期的五倍。——指导书

  • 那么,如何实现多条指令同时运行?

    五级流水线CPU按照指令运行的五个阶段对应地分成了五个顶层模块IF(instruction fetch)DEC(decode)EXE(execute)ME(memory)WB(write back)每个模块执行一条指令的一个阶段

    为了实现阶段的分割与传递性,需要在单周期CPU里添加4个寄存器IF/DECDEC/EXEEXE/MEMMEM/WB,给予统一的时钟与复位信号。每次时钟上升沿到来,寄存器便会将前一个阶段的信息传递给后一个阶段,即将所有指令均向后传递一个阶段,从而实现互不干扰,阶段推进。

    写回模块后面不需要流水线寄存器的原因是:其一,它运行的是指令执行的最后一个阶段,它的运行数据并不需要给对应指令的下一阶段使用;其二,写回部分是寄存器堆,本身可以看作“第五个流水线寄存器”。——指导书。

    第五阶段寄存器写回寄存器不需要再等待一个时钟上升沿,但读出的rs与rt寄存器的值却并不准确,这里考虑内部转发,后文介绍,这里就不赘述了。

    注意:①每条指令都要走完5个周期,尽管可能在某些阶段没有操作;②所有对寄存器的写回,都在WB阶段进行,这样才能保证按照指令顺序写回寄存器,不会出现顺序错乱的情况。

  • 实验之前,需要注意什么?

    因为我们是基于已搭建的单周期CPU,将其更改为流水线CPU,为了之后的操作更加顺畅,这里建议:①确保单周期CPU搭建的正确性;②将尽可能多的部分模块化;③合并多选器:将一位选择指令合并为多位选择指令。

二、单条指令的添加与测试

单周期CPU已经实现了单条指令单独走完一个周期的正确性,那么在流水线CPU里,是不是不用额外更改就可实现单条指令的完成?当然不是的。

读者可以注意以下几点:

  • 各条指令数据是否通过寄存器传递

  • 各条指令的控制信号是否在正确阶段产生

    控制信号由控制模块产生,但一个控制模块只服务于一个指令,而D、E、M、W四个阶段分别运行着4条不同的指令(F阶段无需生成控制信号),很显然一个control模块是不够用的。这里不妨采用分布式译码的方式(集中式译码读者可自行探索),即放入4个control模块,对每个阶段的指令进行译码操作。

    下图为M阶段的control模块:

​ 因为在进行该阶段时,才需要判断是否写入存储器,故我们只需要根据此阶段的指令生成一个memw信号。

  • 一些特殊指令:

    nop:什么都不用做

    jjalbeq:根据指令集,我们需要获取PC高4位与指令生成跳转地址,因为jjal都在D阶段判断执行跳转,故此二者都需从D阶段取出(而不是F)。对于beq指令,应注意此时IFU模块里地址已为PC+4而不是PC。特需注意的是:jal指令会将PC+4存入寄存器$31中,而添加延时槽后实际存入的值为PC+8

    为什么是PC+8:因为延时槽的存在,在跳转或分支指令之后,还会执行一条延时槽指令,该指令在来不及地址跳转的时候就已经被执行。
    IFU模块I/O数据:

  • 关于调试:

    关于mips中代码的导出与导入机器码,读者可移步:在线教程 → ROM、RAM的使用和MARS导出机器码的讲解。

    这个阶段的测试非常重要!因为还不考虑转发和阻塞,所以造数据的时候应确保不要冲突,观察寄存器堆的时候直接双击寄存器堆上的放大镜即可,点击左侧的模块是无法查看实质内容的(哭,记得多加nop

三、转发

首先,介绍一些概念:

  • tnew和tuse:

    对于指令的源寄存器s有时间tuse_D(/E/M),意思是在从D(/E/M)段开始,过几个时钟周期需要使用s里的数据。对于指令的目的寄存器des有数据tnew_D(/E/M),意思是从D(/E/M)段开始,过几个时钟周期将要写入des的数据产生。——指导书

    简而言之,①tuse只在判断阻塞的时候使用,指离源寄存器被使用的阶段还有多少个时钟周期。举个例子:对于addu指令而言,$rs$rt寄存器的值在E阶段被使用,故tuse_D_rs=1,因为D离E阶段还有一个时钟周期,而tuse_E_rs=0。②tnew判断离该阶段目的寄存器数据准备好还有多少个时钟周期当tnew=0,说明数据已经准备完毕,可以转发。举个例子,对于lw指令而言,其在M阶段进行取出存储器里数据的操作,故在W阶段数据准备完毕,则对于lw指令,tnew_D=3,tnew_E=2,tnew_M=1,tnew_W=0。③对于每一个指令来说,其在每一个阶段的tuse和tnew固定不变,那么,如何获取每一个阶段指令的tuse与tnew?在控制模块用与或门的方式,添加控制信号即可:

  • 内部转发

    对于写回模块到译码模块(W到D)的转发,由于写回阶段写入和译码阶段读出都是对于寄存器堆,我们可以采用内部转发:通过寄存器堆的结构使得,在同一个时钟上升沿,若有数据写入寄存器r,也有从寄存器r读出数据的请求,把将写入数据直接读出。这样一来,写回模块转发到译码模块的数据可以不通过外部转发,当然外部转发也可。

    内部转发逻辑:寄存器堆的RD1输出:是0当且仅当A1输入为0; 是将要写入寄存器A3的数据当且仅当将要写入的寄存器编号A3和读取寄存器编号A1相等; 是寄存器A1的数据当且仅当将要写入的寄存器编号A3和读取寄存器编号A1不相等。 寄存器堆的RD2输出:是0当且仅当A2输入为0;是将要写入寄存器A3的数据当且仅当将要写入的寄存器编号A3和读取寄存器编号A2相等; 是寄存器A2的数据当且仅当将要写入的寄存器编号A3和读取寄存器编号A2不相等。

    ——指导书

    什么意思呢?我们需要在寄存器堆内部进行改造,看看图就明白了:

那么,何时转发?

  • 转发都是从流水线寄存器出发,传递给各个执行阶段的

    为什么会这样?不如先来思考一下,如果想要转发ALU运算结果,应该在什么位置转发?在E阶段,数据还在进行运算,只有到E阶段末尾,数据才被运算完成。但我们无法判断E阶段末尾,而E/M寄存器中传递的值就是已经运算完成的ALU结果,所以应当转发E/M寄存器右侧数据,此时M阶段tnew为0,数据转发是有效的。

    那么,从这个思路出发,我们就会发现:

  • 一共有两种转发情况

    • E/M流水线寄存器转发: 转发给E和D阶段
    • M/W流水线寄存器转发:转发给E,M,D阶段。其中,转发给D阶段采用内部转发方式。

    可以发现,被转发的数据只有E阶段的运算结果和W阶段的写回数据,为什么没有对M阶段存入地址的判断过程?因为存储器访问地址不会造成冲突!

  • 注意进行第一种转发情况时,需要对jal指令在W阶段写回$31的数值进行转发。

下面来看看具体模块:

  • 内部:

    从此图我们可以看见模块对转发操作的实现原理,当源寄存器等于目的寄存器且目的寄存器的值已经准备好的时候(tnew=0),进行转发操作当多个阶段都满足转发条件,选择数据最新的那个阶段
    注意$0始终为0,故不进行$0的转发。

  • 外部

​ 了解了内部逻辑,再来看模块外观,就比较好理解了。

说明:des_E不是指E阶段ALU运算结果所在的目的寄存器,而是指M阶段当前指令的目的寄存器。其含义虽是前者,但由于实际在M阶段转发,所以采用des_M来直观表示。

四、阻塞

何时阻塞?

将要使用的数据来不及转发过来,如果不阻塞就会使用错误的数据进行计算。由于分支指令最早在译码阶段就需要使用寄存器数据计算,暂停阶段放在译码阶段,即DEC模块。

结合开始介绍的概念,每条指令i在译码段时(DEC模块运行指令i),要和它前面运行的指令j(前面模块运行的指令j,一般是前面的所有模块)对照判断,看是否需要暂停:当i的源寄存器和j的目的寄存器相同时(设为寄存器k),i和j存在数据关联,这时如果i的tuse小于j的tnew,代表k中数据还没被j准备好(甚至不能转发过来),这时需要暂停D段指令i,否则i会使用k中旧数据里错误运行。

——指导书

简而言之:① 数据关联;② 来不及转发;③ 如何判断:tuse < tnew

怎么阻塞?

将当前指令和其后面的指令全部“冻结”,而其前面的指令正常运行,以等待数据准备好。D段指令冻结也就是F/D寄存器仍然保持当前状态,而此时F段中的pc已经取出下一条指令地址,为防止丢失,pc也保持当前状态。再者,为了D段需要暂停的指令无法向后进行,需要在下一上升沿在E段插入气泡

故需要将D/E流水线寄存器同步复位(插入气泡,使当前指令无法向下传递),且IF/DEC流水线寄存器和PC锁定(维持当前状态,即禁止使能),直到暂停的条件被打破。

——指导书

模块外观如下,记得添加非门:

五、整体调试

  • 自造***钻的数据,① 先全部执行看寄存器堆里的值是否符合预期、存储器内的值是否正确,② 再单步调试看每一步每一阶段状态是否与mars一致(探针是个好东西。

  • 看测评结果,测评结果中含有WA测试点的的当前测试指令和相邻测试指令,将其转为二进制,对照指令集可以看出是执行哪个指令的时候出了问题。不过,不一定是该指令本身出了问题,如果检查后不是指令本身出错,可能是以下两种情况:

    • 写入的地址不对:跳转或分支指令出错
    • 写入的数据不对:涉及改变寄存器、存储器数据的所有指令都有出错可能。
  • 这里给出一组测试数据,读者可以试试

    ori $2,$1,0x1234
    lui $1,0x5678
    addu $1,$1,$2
    jal a
    nop
    beq $2,$1,a
    nop
    lw $2,0($0)
    a:
    sw $1,0($0)
    lw $2,0($0)
    sw $2,4($0)
    
    v2.0 raw
    34221234
    3c015678
    00220821
    0c000c08
    00000000
    10410002
    00000000
    8c020000
    ac010000
    8c020000
    ac020004
    
    output_reg:
    $1: 0x56781234
    $2: 0x56781234
    $31: 0x00003014
    
    output_mem:
    0x56781234 0x56781234
    

    (当然过了这组数据可能依然过不了...读者可以根据自己的测评情况制造更***钻的数据()


感谢观看!

posted @ 2021-11-23 14:43  PYuYi  阅读(1537)  评论(2编辑  收藏  举报