如果还没有实现过单周期的朋友建议先去实践一下单周期再来钻研流水线哦~点击下方链接转至单周期CPU实现过程
单周期CPU传送门
单周期CPU的实现从某种意义上来说是为了给流水线CPU打基础,毕竟单周期CPU无论是理论还是工程实现上的意义都不大,将CPU所有的工作全部交给一个时钟周期搞定,未免有些浪费本就珍贵的硬件资源,为了体现分治策略思想,同时更重要的是为了提升CPU的主频以实现更高的性能,在硬件条件不能更改时,流水线是一种非常经典的性能优化策略。
也许我们都知道在一个忙碌的工作间里面,工人们热火朝天地干活,一个工业器件从原料到成品的加工可能要经过多个步骤,设想如果将所有的工作全部堆到一个工人的头上,将是一件多么恐怖的任务,但如果我们将这个加工过程分解,譬如:原料,设计,成形,打磨,质检;并且每个工人只负责一个步骤,那么这项本来很繁重的任务将变得更加轻松。
这就是流水线的思想,和分治思想有着异曲同工之妙,通过分解任务,将原本单周期需要在一个CPU周期内解决的问题分成多个周期解决,大幅度减轻了CPU在单个时钟周期内的工作负担,从而使得CPU完成一个任务所需时间变短,从而使得CPU频率得以提高。
流水线的工作示意图如图1所示 图1
此图中IM为指令存储器,Reg为寄存器堆,DM为数据存储器。
没有单周期CPU理论知识基础的话,即便设计出流水线也是瞎猫碰到死耗子的巧合罢了,所以基础知识一定要夯实打牢!
后序章节中的流水线寄存器之原型,为数字逻辑设计中时序逻辑部分重点内容:触发器;建议充分掌握其理论内容,这里仅简单列出D触发器的部分。
D触发器,最简单的触发器之一,其工作原理是:若第一个时钟周期其输入D改变,则第二个时钟周期其输出Q将跟随D的改变而改变,即输出跟随输入变化而变化,并且在输入D每个周期都变化的基础下,输出Q总是“落后“D”一个时钟周期,并且就像是D的跟屁虫一般对其如影随形,此特性决定了我们要想人为分割一条指令的执行,就必须设置对应的触发器以完成这一时序逻辑电路。
我们将CPU的工作划分成5个步骤的工作仅仅是理论层面的,实际在Verilog中需要我们通过一些特殊的模块来实现这个划分功能。
虽然叫“寄存器”,但是其本质仍是触发器,至于存储什么样的内容,则要仔细分析我们对一条指令分割的过程。
我们可以简单的将一条指令的执行过程分为五个部分(也可以分成更多的部分,这里仅讨论最简单的五级流水线),其实这一划分在单周期CPU设计中就有所体现,这五个阶段分别是:取指、译码、执行、访存、写回;(其中控制单元被放入译码阶段)也就是说在我们的流水线中,可能在某一个CPU周期内,同时有5条指令在进行着不同阶段的处理,不妨拿图1中的例子来说明:
在CC5(CPU clock 5)中,从纵向时间切面来看,流水线此刻有5条指令正在被处理,且第一条指令ld x10, 40(x1)会在下一CPU周期流出流水线,而第六条指令(图中没有列出)将在下一CPU周期流入流水线。同理,在其他的CPU周期里,譬如CC1,此时流水线刚好开始工作,仅有第一条指令流入流水线,而CC9,流水线即将结束工作,仅有最后一条指令还在被执行着。
第一条指令ld x10, 40(x1)已经跑到第五个阶段:写回,而第二条指令sub x11,x2,x3则跑到第四个阶段:访存,第三条指令add x12,x3,x4则只是第三阶段:执行,第四条指令ld x13,48(x1)跑到第二阶段:译码,第五条指令add x14,x5,x6恰好进入流水线,正处于第一阶段:取指。
图中位于两两器件之间的蓝色竖条,就是我们流水线的关键部件,流水线寄存器,所以从图中我们大致可以得知,流水线寄存器充当了两个流水线阶段之间的“缓冲区”。
以上我们分析了某一个CPU周期的整条流水线运行情况,可以看出,流水线CPU的运行过程中,似乎被划分的五个阶段是单独进行着自己的工作,且除了整个程序的开始和结束那一小段特殊时期之外,其余大部分时间里,每一个阶段都在“不断地”工作,每个时钟上升沿到来时,都会有一条指令从这个阶段完成对应操作并流出,同时下一条指令又立刻流入这个阶段并开始进行处理,可以类比为一条工厂的装配线,履带不断向前以固定速度滚动着,各个装配阶段的机械臂不断落下对经过的元器件进行一些操作,然后抬起,待下一个元器件经过,再次落下,这种情况正是我们流水线CPU的工作情形。
显然,不同阶段之间不可能真的相互独立,甚至可以说每个阶段之间都有着密切的数据、控制联系,所以很简单地能够想到我们要将两个阶段之间有关联的数据存入对应流水线寄存器,使得被关联的阶段能够拿到关联之阶段的相关数据,所以流水线寄存器输入的第一类信号就很明确了——相关数据。
但是问题并未就此结束,我们只有一个控制单元!(当然,如果你非要给各个阶段都增加对应控制单元,也不是不可以,但是这么做的结果将是导致硬件资源的浪费以及电路延时的增加,从而违背了我们建立流水线的初衷!)而且我们大部分的控制信号都是从控制单元出发向其他单元传送,按照我们流水线划分,控制单元在第二阶段——指令译码(Instruction Decode);
我们不妨单从一条指令的执行角度来看,此指令的第一个CPU周期时就会将指令取出,第二个CPU周期时就会将指令送入控制器进行译码,从而产生各个控制信号;但是指令的完整执行需要五个CPU周期,后面的三个CPU周期要正确运行,不可能离得开这第二CPU周期中产生的控制信号;但是从完整的一个程序来看,这些信号也不能一直保持在“原地”,不然后序指令就只能塞在这里等待前面这条指令执行,这样的话流水线就形同虚设了。
因此,一条指令在第二CPU周期产生的控制信号不能原地滞留,需要跟着流水线一起流动,回到我们上一小节做的类比,如果生产线上面的机械臂能够识别元器件内部信息,同时我们向这个元器件内部写入一些信息告诉后面的机械臂“对这个器件,你要这么那么做”,是不是就能够在不增加控制单元的前提下将控制信号向后传递呢?所以我们向流水线寄存器输入的第二类信号也就明确了——后序相关控制信号
流水线工程划分大致如图2所示,其中流水线寄存器命名方式为:上一阶段名/下一阶段名
图2
由于我们要将控制信号和一些相关数据都顺着流水寄存器向后传递,因此每一阶段的传递都需要一个额外的变量,故我们的变量较单周期CPU而言有着数量上的陡然增长,为了代码方便管理同时为了debug时不至于头昏眼花,我们在变量命名方面也需要一些小技巧,就是在原本名称的前面加上其作用阶段的简写,比如我们EXE阶段里,ALU单元需要alu_op信号,但是这个信号才CU单元中产生,那么产生的信号命名为cu_alu_op,并将之输入IF/EXE流水寄存器,但是其对应的输出信号可以命名为exe_alu_op,这样子能够极大地方便我们编写流水线。
以EXE/MEM流水寄存器为例:
module EXE_MEM(
input clk ,
input rst ,
input [31:0] wb_wD ,
input [31:0] exe_imm ,
input [31:0] exe_pc ,
input [31:0] exe_alu_c ,
input [31:0] exe_rD2 ,
input exe_rf_we ,
input exe_dram_wr ,
input [ 1:0] exe_wd_sel ,
input [ 4:0] exe_wR ,
output reg [31:0] mem_imm ,
output reg [31:0] mem_pc ,
output reg [31:0] mem_alu_c ,
output reg [31:0] mem_rD2 ,
output reg mem_rf_we ,
output reg mem_dram_wr ,
output reg [ 1:0] mem_wd_sel ,
output reg [ 4:0] mem_wR
);
always @(posedge clk, posedge rst)
begin
if(rst) mem_imm <= 32'h0 ;
else mem_imm <= exe_imm;
end
always @(posedge clk, posedge rst)
begin
if(rst) mem_pc <= 32'h0 ;
else mem_pc <= exe_pc;
end
/*...其余的变量类似赋值*/
endmodule
其实理想流水线的功能很有限,除了使用了哈佛结构解决了结构冲突,另外两种冲突无法解决,甚至可以说一旦出现这两种冲突,整条流水线将会崩溃。
仅仅有理想流水线是远远不够的,我们还需要更多的模块和变量信号来解决数据冲突和分支冲突,此文章不涉及此类内容,后序我会对此进行补充并放置超链接。
更新:后话和冲突解决都在我的个人博客中
流水线第一章
流水线第二章