Skip to content

XMirror0/JLU_XMirror0_CPU

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

流水线CPU设计的一些思考

文件使用指南

|JLU_XMirror0_CPU
    |---单周期CPU.cric
    |---流水线CPU_BTB.cric  //流水线CPU带动态分支预测
    |---流水线CPU.cric
    |---myCPU               //LA32R指令集46指令流水线CPU
        |---alu.v
        |---decoder_2_4.v   //decoder用于ID段指令解码
        |---decoder_4_16.v
        |---decoder_5_32.v
        |---decoder_6_64.v
        |---mycpu_top.v
        |---regfile.v

myCPU下的代码是基于 https://gitee.com/loongson-edu/cdp_ede_local 修改的,完成到exp11,用Verilog编写。Verilog的语法和C很像,可以看看ConfliDetecter中冲突的判断逻辑。

冲突是否发生的判断

以前使用Logisim时,可能就通过Decoder解码出的控制信号来判断当前指令需要什么,后面三个流水段的指令要做什么,其实这是相对低效的。因为判断的对象是指令,而不是控制信号,所以这种设计方式经历了将指令翻译为各种控制信号->从控制信号反推是什么指令。这是重复性的工作。在Logisim里只实现9条指令,完全可以通过放9根线,一个指令分配一根(类似于独热码,在实际中可以减少后续的组合逻辑电路深度,提高运行速度,优化时序)。另外,还要对指令进行分类。即这个指令从哪里取数据?向哪里写数据?注意这个取决于指令的具体行为,而不是指令格式中的Rj、Rk、Rd。比如bl指令,指令格式中没有任何寄存器地址,但是他要写$r1。而且有的指令数据来源不同,有的是Rj Rk,有的是Rj Rd,有的只来自Rj,对这些都进行分类显然是一种浪费,不仅增加工作量,可能还会使判断逻辑变复杂,编写难度上升,时序表现还不好。所以将他们总结为need_r1和need_r1_r2。因为总结各个指令的行为,要么r1和r2都读,都需要他们的数据,要么就只读r1(注意还有need_nothing,像是b、bl、jirl这种无条件跳转,但是这种一定没有数据冲突了,不用纳入数据冲突的考虑范围)。另外还要判断流水段后面的指令有没有写。这个通过Wen控制信号来判断就好了。这样,一读一写,不就完美对应了数据冲突的根本所在了吗?如此,再加上一些其他零碎的判断条件,比如比较的两条指令都是有效的,读写地址是相同的,等等等等。这样判断逻辑就十分清晰了。

解决完上述的冲突,就解决了流水线CPU中最常见,最频繁的冲突,接下来还有控制冲突,在现阶段,比较简单的流水线CPU中主要表现为分支指令和跳转指令。当这种指令进入ID段时,如果判定需要跳转(也就是br_taken确定有效),那么需要将IF段的指令无效化。而无效化的条件也很简单:只要看br_taken信号和reset信号就好了。

冲突的解决方式

对于数据冲突,有两种解决方式:1.阻塞,等待前置指令执行完毕再继续执行;2.转发,等到所需数据计算完就开始执行。显然,转发所需的周期数是更少的,但是转发的时序表现不太好:它引入了一条极长的逻辑路径。比如ID和EX段的指令存在冲突,那么要经历ID段和EX段的所需控制信号进入ConflictDetecter->进行条件判断->等待EX阶段执行完毕,得出结果->结果经过Mux,到达流水线寄存器。这样,我们就不得不降低主频以满足这条逻辑路径的时序要求。但是阻塞就没有这种烦恼。所以,两种方式没有优劣之分:转发注重IPC,阻塞注重主频,性能由IPC和主频共同决定。

对于结构冲突,非常简单:只要把前一条无效化就可以了。

那么,冲突、转发、无效化,在流水线CPU中怎么具体实现呢?这就要提到流水线各级之间的握手机制和指令有效性了。我们使用ready_go信号来实现各级之间的握手。比如ready_go_Ex,当这个信号为0时,意味着Ex段的指令还未执行完毕(比如除法和取模类的指令,往往需要执行三十多个周期)。那么Mem段的流水寄存器得知Ex段未执行完毕,就不读入Ex段的结果,而ID段得知Ex段的ready_go为0,就也将自己的ready_go置0,不再继续往下走,等待Ex段执行完,才继续往下走进入Ex段。而IF段同理,读取ID段的ready_go信号,决定自己要不要继续往下走。这种观察下一级是否准备好,从而确定自己是否向下执行的方式,叫做流水线反压。而指令有效性,就是指每条指令都携带一个valid信号,指示这条指令是否有效,要不要被执行。设想以下汇编代码:

1 bl ……
2 addu ……

因为bl指令一定跳转,所以addu指令一定不被执行。但是当b指令进入ID段,CPU得知需要跳转时,addu已经进入流水线的IF段了。怎么把addu指令无效化呢?这就要用到IF段的valid信号了。我们只要将IF段的valid信号置0,就自动将addu指令无效化了。以后,当这条addu指令进入某个流水段后,器件根据自己的需要,检查它所处阶段的valid信号,就知道这条指令不需要执行,也就实现了无效化

纯阻塞方式

这种方式非常简单粗暴:数据冲突我就阻塞,结构冲突我就无效化。不需要考虑其他的。除了IPC低一点,没啥毛病。

转发方式

这种方式要考虑的就多了,因为有一种特殊的数据冲突无法依靠纯转发方式来解决:Load-Use。因为Load类指令要等到Mem阶段才能得出结果,所以当Load类指令位于Ex段,且与ID段的指令存在冲突时,无法得出结果转发给ID段的指令。所以必须阻塞一个周期,让Load指令进入Mem段得出结果再转发给ID段的指令。另外还需要注意的是,转发是具有优先级的。以下面这段汇编代码为例

1 lu12i $r1 0xaaaaa
2 lu12i $r2 0x11111
3 lu12i $r2 0x22222
4 add.w $r3 $r1 $r2

你认为,$r3中的值应该是0xaaaaa000+0x11111000=0xbbbbb000,还是0xaaaaa000+0x22222000=0xccccc000呢?显然是后者,对吧。我们来分析在流水线CPU中发生了什么:当add.w处在ID段时,lu12i(3)正处在Ex段,lu12i(2)正处在Mem段。此时,我们发现,ID和Ex、Mem都存在冲突!应该转发谁的数据呢?显然应该转发lu12i(3)的数据而丢弃lu12i(2)的数据。因为lu12i(3)的数据相较于lu12i(2)更新。所以我们可以得出这样的规律:Ex段的转发优先级>Mem>WB。

而转发的实现,我采用了forward_source信号。它指示了前递的数据来源。这个设计因人而异,可以选择自己认为比较好的设计。

碎碎念

在用Logisim做了一个简单的9指令MIPS流水线CPU后,再用Verilog做一个46指令的LA32R流水线CPU,以前很多不明白,不清晰的点都明晰了很多。Verilog中有三个概念叫做:门级描述数据流级描述行为级描述。我们的课本更多的是数据流级描述,而我们面对Logisim则要使用门级描述,导致我们知道什么时候数据该怎么流动,但是却不知道电路怎么解读这些数据流,电路该做出什么样的行为。

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors