Featured image of post 北航CO P5 流水线CPU搭建(1)

北航CO P5 流水线CPU搭建(1)

P5课下搭建任务 压力最大的一集


说在前面

这周真的太忙了。
不说流水线和数竞这两块大骨头,硬控两小时的党课,硬控两小时的挑战杯志愿者,硬控一小时的组会,硬控五小时的毛概PPT…

虽说这周还买了曲包摘了星

但是流水线CPU真难写。
读者可以看看我写设计文档心境的变化。

(提交截止前2h突然发现bug。。5点半硬控到7点去上党课)

为什么说这是流水线的(1)呢? 因为目前还只有10条指令。流水线(2)是P6,30条指令的事了。

那么,请君观之。

设计文档

流水线的设计与单周期的区别主要是两点:

  • 流水线寄存器的插入
  • 冒险的处理

这两点说起来轻轻松松,但实操起来实在繁杂。

在漫无目的地发愣了很久之后,我决心要先写出一篇设计文档,把思路理清了再去书写。

发愣的核心原因在于总感觉自己处理不好寄存器要放什么,谁应该在哪个区(浅显的思考当然不能处理好)

有的时候更是连第一步没想好就忙着转发阻塞什么的

哪怕后续转发的时候再优化,现在把需求理清,车间划好,才能想下一步迈进。

流水线寄存器

要将原来的单周期分割为五个小阶段,具体是什么样呢?

哪些阶段有什么部件?又要干些什么?

F

pc出值给IM
IM出指令
pc+4得更新到pc

这里已经涉及到pc的问题了,我不得不现在想清楚延迟槽。

所有不是pc+4的npc,都是经过跳转的;所有要跳转的,都要用延迟槽

所有用延迟槽的,都要先执行pc+4,因为跳转结果在第二周期才得出来(哪怕你提前得出来了你也得等延迟槽!!)

所以跳转结果是在下一个周期才赋值,是一种特殊的赋值!

pc应当默认使用pc+4,除非有NPC算出的东西进来

这需要一个控制信号,并且应当是任何跳转语句都能引发,我称之为not_pc_4

这个信号作用于PC,所以还是称它为PCSel吧
PCSel为1的条件就是所有跳转语句

这个信号在什么时候产生呢?
肯定已经到D阶段了

  1. beq在F阶段,此时PCSel仍为0

(因为跳转语句不在延迟槽,这一句不是延迟槽,则上一句不是跳转,则PCSel为0)

  1. beq在D阶段,延迟槽语句在F阶段

这时候PCSel为1

因此下一个pc将是跳转的结果,没有问题!

PCSel产生即使用,不经过流水线(虽然他其实能够在F阶段就拿出来,但是拿出来也要流水到下一周期才能用。此外译码一般都放在D级)

顺便谈到zero

zero经过提前到D级后,也是即拿即用,不需要经过流水线

pc,Instr都给 F_D

F_D拿到指令

F_D

存储PC和Instr,实现流水。

一个流水线寄存器是要干什么?

  • 暂存信息(时钟、复位信号与其他信息)
  • 从寄存器出信息

搞明白这一点,后面的流水线寄存器都大同小异了。

D

好,接下来看一条指令在D阶段要干些什么

D阶段最核心莫过于GRF
但是其他内容也都同等重要

首先是先前提到过的 NPC和CMP

单独搞一个CMP出来据说是为了解决P6大量branch语句

这个阶段要给imm_16变成IMM_32, imm_16已经没用了吗?要把它加入流水吗?

为了可扩展性,要不要把他一直存着呢?

但到时候哪怕用imm_16,直接从IMM_32里面取就行了。完全没必要存。

imm_26是j型指令用的,存着也没用,毕竟即拿即用,得赶快给PC送过去呢!

好的,那这个阶段把imm_16也解决掉了


剩下最后一个,就是GRF了!

读什么?根据rs,rt读呗。

这个阶段又不写。所以rd目前也没什么用。

是不是要注意GRF的A3选择的时候是rd_W,rt_W这些?写的时候那个pc也得是pc_W?关于W阶段啥东西都应该是W?

那就是W阶段的事。看似是一个部件,不同的功能要分开考虑。不能想着GRF在D级,就一口气把GRF接口都连好。
现在只考虑D级!

D_E

存储很多很多东西
还有用的控制信号(EXT,J,Jr当场使用了)
还有用的数据(imm_16和imm_26都没用了)
当然还有pc

多存点,说不定会有用的。

E

好嘞,该读的信息都读了,该处理的信息也处理了
下面就是执行操作了

关键的内容还是ALU

而ALU的核心就是各个东西都是E

(这个模块异常的简单?)

E_M

还是一个基本问题,后面需要什么

要传递一脉相承的信号,也要传递新生成的数据

pc是当然的
后面有Mem取数和Write回写
需要Mem写的地址,写的内容,GRF写的地址,写的内容
还有他们的读写使能

别忘了还要传递ALUData信息

M

整体和单周期也基本没有区别,只有这一个模块在工作而已。

M_W

添加MemReadData_M MemData_M和Mem_Addr_M也带上吧

W

回到GRF
该用W的都用上W即可

意外地快。 不处理冒险的数据通路搭好了??

冒险处理

我自己也没想到已经走到了这一步。
这一基础性的工作就这样被我搁置了这么久。

你也能够发现,上面的无非是添加接口,新建流水级的体力活。
接下来才是头脑风暴的核心。

转发和阻塞!!!

转发

首先要构建好转发的所有路径
转发的核心矛盾,是读了还没写的东西
(一定要注意0号寄存器!写进去了也还是0,因此不能转发)

我在这里直接考虑所有路径

指令 F D E M W
add,sub 取指令 取rs,rt 算数 得到数据进行转发rd 写数rd
ori,lui 取指令,扩展imm 取rs 算数 得到数据转发rt 写数rt
lw 取指令,扩展imm 取base,rt 算数 寻址取数 得到数据转发rt,写数rt
sw 取指令,扩展imm 取base,rt 算数 寻址存数
j 取指令,扩展imm 算出NPC延迟槽后用
jal 取指令,扩展imm 算出NPC延迟槽后用,算出PC_8 可转发(但没人需要) 可转发 可转发,写数$31
jr 取指令 读rs并给NPC

举个例子add
例如我们add读寄存器值的时候可能数据还没出来,但是真正要用到的时候其实在ALU
因此使用时间在E级。

在此诠释Tnew的定义:最早的可转发时间。也就是正确数据已产出,且存在转发通路。

我在此分如下几类指令:

  • R型读2求1型
    rs与rt要读,正常转发
    Tuse_rs = 1(ALU的时候有就行)
    Tuse_rt = 1(ALU的时候有就行)
    Tnew 初始(即相对于E)= 1 (M才能开转)
    RegAddr 现场算

  • I型拿1读1求1型
    rs 要读,正常转发
    Tuse_rs = 1(ALU的时候有就行)
    Tuse_rt = inf
    imm正常传,传到了num_2,因此rt的值在E级要额外存为V2_E_Data
    Tnew 初始 = 1 (M才能开转)

  • 读寄存器跳转型
    说白了就是jr()
    Tuse_rs = 0 !!! Tuse_rt = inf
    所有跳转的特点都在于其必须在D级得出跳转结果。
    而jr就必须从V1_D拿到完全正确的值
    因此E->D的转发是有益的
    (但从E开始的转发需要是在D级就得出结果的指令(不经过ALU)) 符合条件的并不多。
    我暂时采用阻塞。

  • 直接跳转型 j不必多说
    Tuse_rs = inf Tuse_rt = inf
    RFWr = 0

  • 条件跳转型 beq。关键在于br和zero两个指令。
    其中zero同样要保证在D级算对!必须拿到对的值。
    拿不到?我也先采用阻塞。
    Tuse_rs = 0 Tuse_rt = 0 RFWr = 0

  • Link型 典例jal
    Tnew = 0 !!!!!(不过我还是把它当做Tnew = 1来用) Tuse_rs = inf
    Tuse_rt = inf

    E级就能用。因此E->D这条转发路径是有益的。我目前先用了阻塞。
    写$ra太慢了!必须转发!
    该怎么转?能在M和W转。(E也能转)
    一定要注意:一旦选择转它,你要拿他去覆盖转的值!!

    1
    2
    
    wire [31:0] ALUData_E_true;           
    assign ALUData_E_true = (Link_E) ? pc_E + 32'd8 : ALUData_E;      
    

    这个ALUData_E_true直接传进流水线E_M和M_W,
    即如果本指令为Link型,你就只能读到这一个$31数!

  • 条件跳转Link型
    最关键的便是不跳转,就不Link!
    比如beqal,其link信号应为beqal&&zero(传Link信号的时候就控制好)

  • L型 rs rt 正常获取转发
    Tuse_rs = 1 Tuse_rt = inf(看题意新指令会不会特殊条件使用rt)
    imm正常扩展

    ALU算的值会在M级被转发!! 但是在W级正确的值还会被转发过去。

    准确来说,阻塞逻辑就是为了保证你要用的时候一定能拿到正确的值,无论这中间发生过什么。

    因为你的Tnew就是正确数据生成时间,Tuse就是使用时间。

    如果传过去被使用的是错误数据,那就说明正确数据还没被生成,也就是说这条指令是满足阻塞条件的。所以根本不会出现这种情况。

    Tnew = 2 最慢!!!(一直到M级取出来,W级才能用)

  • S型 Tuse_rs = 1, 在E级需要算对数rs,
    Tuse_rt = 2,在M级需要有正确的寄存器值rt
    RFWr = 0
    Tnew = 0,不对寄存器进行写

转发路径共四条:

转发方向 数据 判断
M->D 传ALUData_M(经过pc+4和ALUData的选择) RegAddr_M跟A1A2比
W->D 传RegData(经过MemReadData_W和ALUData_W的选择) RegAdde_W跟A1A2比
M->E 传ALUData_M(经过pc+4和ALUData的选择) RegAddr_M跟rs_E rt_E比
W->E 传RegData(经过MemReadData_W和ALUData_W的选择) RegAddr_W跟rs_E rt_E比

为什么没有到M的转发?
因为M级使用即Tuse = 2,而Tnew没有大于2的(Tnew = 3即W级才产出数据。这显然是荒谬的?)

事实上,W->M 是必要的!!  

要知道,Tnew从来不是产出时间,而是最早转发时间! 

如果你写了Tnew=2,那你就表明了自己能在W级转发数据,怎么能不写W级转发通路呢?

咱们在编写Tnew的时候,一定要和我们能实现的转发通路一一对应。      
只有在你做到能转则转的情况下,你才能说Tnew是正确数据的最早生成时间。  
否则,你的效率会更低,且你需要修改你的Tnew表。    

为什么没有E->D的转发?
E->D 即 Tnew = 0 且 Tuse = 0,满足需求只有jal(lui也可以。但是我已经把他塞进ALU了)
但是我还没写。 因此Tnew = 1,要阻塞。

从这里也能看出来Tnew和转发通路的关系

阻塞

我们采用把阻塞控制在D阶段完成的方法。
Tuse:在D级的时候,读寄存器的最晚周期D级的差
Tnew_X:X周期的指令要写的内容能够转发出去的最早周期与E级的差
(在周期A算出来,则可以在周期A+1转发)

我们需要考虑D与E关系,D与M关系
为什么不考虑D与W关系??
因为Tnew_W一定为0了(谁家指令在W级还没算出来数(笑))

对于两个阻塞的关系,我们采用暴力地起来
一条通道可能不通就阻塞。

这可能确实有点暴力,但我也确实没什么精力去优化描述,以更好地处理例如
lw $31, 0($0)
jal label
add $t0, $31, $31
这样的情况。这种情况原则上jal转发优先级更高,他还更快,所以如果他有正确的值且寄存器匹配且不为0且T关系等等就可以不管lw的感受。
我没管那么多。

阻塞只需满足:

    确实发生读写冲突且来不及转发(Tuse < Tnew)     

我们先分为rs冲突与rt冲突

1
    assign stall = stall_rs || stall_rt;

然后以rs为例:

stall_rs = stall_rs_DE || stall_rs_DM

以stall_rs_DE为例:

确实发生读写冲突:
(rs_D(即A1)== RegAddr_E) && (RFWr_E) && (A1 != 5’b0)
且 &&
来不及转发:
(Tuse_rs < Tnew_E)

所以

1
2
3
4
stall = ((A1 == RegAddr_E) && (RFWr_E) && (A1 != 5'b0) && (Tuse_rs < Tnew_E)) ||
        ((A1 == RegAddr_M) && (RFWr_M) && (A1 != 5'b0) && (Tuse_rs < Tnew_M)) ||
        ((A2 == RegAddr_E) && (RFWr_E) && (A2 != 5'b0) && (Tuse_rt < Tnew_E)) ||
        ((A2 == RegAddr_M) && (RFWr_M) && (A2 != 5'b0) && (Tuse_rt < Tnew_M));

接下来,只需对 Tuse 和 Tnew_X 下定义:

功能还在CTRL中实现:
因为我要用指令来描绘 Tnew 和 Tuse_rs 和 Tuse_rt

指令 Tnew Tuse_rs Tuse_rt
add 1 1 1
sub 1 1 1
ori 1 1 5
lui 1(个人原因) 5 5
beq 无(0) 0 0
jr 无(0) 0 5
jal 1(不转发导致的) 5 5
lw 2 1 无(5)
sw 无(0) 1 2

Tuse 直接生成
Tnew_X 初值生成,递减操作

递减操作的实现:
放进流水线嘛,过个周期减一下

测试文档

刚搭完流水线寄存器,出来一堆compile bug
或多或少的少加控制信号,打错字什么的。。
然后运行了充斥着nop的代码,没什么问题。

然后就向Hazard进军了!

Hazard测试的编写很有针对性。
前提是你要针对好所有的代码情况,事先分析好Tnew,Tuse关系
才能模拟出所有的冒险。
所有的转发,所有的阻塞,都要到位。

思考题

  1. 我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。

    beq提前使得其Tuse降为0,这无疑提高了阻塞率。 当这条指令前面是Tnew = 1的add,sub,ori等等均阻塞

    用原来的方法,虽有可能损失更大,但是也存在很多无损失的情况。提前分支判断则是较为稳定的损失。

    举例:

    1
    2
    
     add $16 $16 $16        
     beq $16 $0 label
    

因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计?

正因题意所说,因为延迟槽的存在,jal语句后的那条指令是已被执行的,跳转回来时当然要跨过延迟槽这句话

我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM、ALU),请思考为什么?

功能部件本就耗时,如果再加上转发逻辑,二者必定是顺序关系,导致时钟周期长度增长,频率上限降低,效率也就低了。
但是如果在下一周期,转发操作和下一周期的操作是独立并行的,不影响执行速度。

我们为什么要使用 GPR 内部转发?该如何实现?

GRF内部转发本质上是W到D的转发,只是W和D用了同一个部件
有同学使用了negedge的办法进行W,这样posedge读的时候就一定已经被新值写入。
我采用了传统转发。无非转发的值是RegData_W,参与判断的是RegAddr_W罢了。

我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?

需求者严格意义上有很多,不过他们是可以合并的。
所有需要使用或传递的部件,都是需求者。
但是我们可以先进行转发,再把结果给需求者们。
总体来说,需求者可以是CMP的两个数,ALU的两个数,D_E和E_M流水线寄存器。

需求者还有M级 sw时RD2的值!!!!!需要换上最新数据!!!

供给者我们都安排到流水线寄存器的输出。
M级的数据可以是ALUData,Pc+8
W级的数据可以是MemReadData,ALUData,Pc+8

前文也提到了,E到D的转发尚未考虑。

因此,共有M->D,M->E,W->D,W->E,W->M五种。
(以及E->D)(6种)

!!刚de出的bug!!!还有W->M转发!!!!

再总结一下所有被转发:

RD1–MD,WD转发–>V1_D—>V1_E–ME,WE转发–>num_1—>运算

RD2–MD,WD转发–>V2_D—>V2_E–ME,WE转发–>V2_E_Data—>V2_M_Data–WM转发–>MemData_M—>被写入

RD2–MD,WD转发–>V2_D—>V2_E–ME,WE转发、选择–>num_2—>运算

总结一下所有转发:

M级为ALUData,Pc+8经过选择变为ALUData_true的转发 W级为MemReadData,ALUData,Pc+8经过选择变为RegData的转发

转发地址为RegAddr已各自计算

在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。

这一问题在前文已有谈论。 总结起来,关键还是处理好Tnew,Tuse,读谁,写谁这四个问题 (其中,读谁,写谁都是需要控制信号的)
以及最核心,最有不确定性的“处理”这一部分

确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。

我使用的是对控制信号找相关指令的方式。
先由opcode和funct得出结果,再得出指令;接下来由指令得到控制信号。
优势是简洁,不足是容易漏写,错写。
加指令时一定要分析好所有相关信号,再书写。

comments powered by Disqus
Easy Life and Easy Learning
使用 Hugo 构建
主题 StackJimmy 设计