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/DEC
、DEC/EXE
、EXE/MEM
、MEM/WB
,给予统一的时钟与复位信号。每次时钟上升沿到来,寄存器便会将前一个阶段的信息传递给后一个阶段,即将所有指令均向后传递一个阶段,从而实现互不干扰,阶段推进。写回模块后面不需要流水线寄存器的原因是:其一,它运行的是指令执行的最后一个阶段,它的运行数据并不需要给对应指令的下一阶段使用;其二,写回部分是寄存器堆,本身可以看作“第五个流水线寄存器”。——指导书。
第五阶段寄存器写回寄存器不需要再等待一个时钟上升沿,但读出的rs与rt寄存器的值却并不准确,这里考虑内部转发,后文介绍,这里就不赘述了。
注意:①每条指令都要走完5个周期,尽管可能在某些阶段没有操作;②所有对寄存器的写回,都在
WB
阶段进行,这样才能保证按照指令顺序写回寄存器,不会出现顺序错乱的情况。
-
实验之前,需要注意什么?
因为我们是基于已搭建的单周期CPU,将其更改为流水线CPU,为了之后的操作更加顺畅,这里建议:①确保单周期CPU搭建的正确性;②将尽可能多的部分模块化;③合并多选器:将一位选择指令合并为多位选择指令。
二、单条指令的添加与测试
单周期CPU已经实现了单条指令单独走完一个周期的正确性,那么在流水线CPU里,是不是不用额外更改就可实现单条指令的完成?当然不是的。
读者可以注意以下几点:
-
各条指令数据是否通过寄存器传递
-
各条指令的控制信号是否在正确阶段产生
控制信号由控制模块产生,但一个控制模块只服务于一个指令,而D、E、M、W四个阶段分别运行着4条不同的指令(F阶段无需生成控制信号),很显然一个control模块是不够用的。这里不妨采用分布式译码的方式(集中式译码读者可自行探索),即放入4个control模块,对每个阶段的指令进行译码操作。
下图为M阶段的control模块:
因为在进行该阶段时,才需要判断是否写入存储器,故我们只需要根据此阶段的指令生成一个memw信号。
-
一些特殊指令:
nop
:什么都不用做j
、jal
与beq
:根据指令集,我们需要获取PC高4位与指令生成跳转地址,因为j
与jal
都在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
(当然过了这组数据可能依然过不了...读者可以根据自己的测评情况制造更***钻的数据()