本文是笔者在使用Verilog开发流水线CPU过程中的一些记录与反思,希望能给大家开发流水线CPU带来一点帮助。内容主要分为冲突的覆盖性组合分析、带转发的数据通路与信号设计、初步代码编写、设计测试代码与调试过程以及一些思考五个部分,在设计与开发过程中会向读者频繁地传达流水线设计的工程化方法。
虽然笔者在完成Project0~4的过程中并未对流水线有过深的认识,但是本文的内容基于完成Project4 Verilog开发单周期CPU,所以正文中会直接使用许多前期已经使用过的术语。
一些写在前面的说明:笔者设计的是MIPS指令集的常用指令,设计为五级流水线,使用的是集中式译码,使用延迟槽和控制冒险前移,数据冒险处理风格为Detector型。
虽然是先做了无转发数据通路的设计再开始的冲突分析,但是前思后想认为还是应该先讲讲这个。
在Project5阶段,指令集的大小为10,直接对每一对指令进行分析即可。但是到了Project6阶段,指令集的大小骤然增加为50条,一条一条地分析指令颇为耗时,所以笔者选择了将指令根据族别和功能进行分类,最终确实方便了编写控制单元和冒险单元的代码。
按照指令族别可以分为,load型、store型、Reg-Reg型、Reg-Imm型、Branch型、Jump型、Move型(mfhi/mflo/mthi/mtlo)。
而按照指令功能(是否涉及写回、有无符号等)又可以将每个指令族里的指令分得更细(JAL型、JR型、MF型、MT型),也可以将Reg-Reg型、Reg-Imm型、JAL型(jal/jalr)、MF型(mfhi/mflo)看作是Write-Reg型指令等等。为了方便分析,在本文中认为jalr既属于JAL型也属于JR型指令,而j指令则不必考虑数据冒险。
注意到Project6中需要支持乘除单元的指令,乘法需要5个周期,除法需要10个周期,但其只会对乘除单元的指令造成数据冒险,所以在分析指令冲突的时候可以将涉及乘除法寄存器的指令拿出额外考虑。
经过分类之后就可以进行轻松许多的数据冒险设计了。
首先需要确定 Tuse 和 Tnew 的概念, Tuse 即指令进入流水线IF/ID寄存器最晚需求某个寄存器值的周期时间, Tnew 即在某个阶段的指令产生寄存器的写值(供应值)最少还需要的周期时间,数据冒险就在于指令间的供需匹配。
对于 Tnew=0 的指令,其供应值已经产生,作为”供应者”的指令需要及时地给”需求者”的指令提供最新的数据,便于”需求者”进行自身的计算。这是转发。
但是如果出现 Tnew>Tuse 的情况,这里出现了一个时间上的逆向需求,”供应者”无法及时地给”需求者”提供数据,这时候”需求者”就需要等待”供应者”的回应。这是暂停。
对于暂停情况,考虑指令对之间 Tnew−Tuse 的值可以算出要暂停的周期数,在本Project中这个周期数是不超过2的,暂停的做法也很简单,将当前ID阶段的指令锁住,清除EX阶段指令即可(相当于向EX阶段插入一条nop指令),实现上也不需要计算上述的值,只要知道现在是否需要暂停即可。
暂停信号可以写为stall = stall_ld_calr || stall_ld_st || stall_ld_bjr || stall_calr_bjr
这五种情况,其中stall_ld_bjr
需要同时考虑EX阶段和MEM阶段,但实际上这些情况之间是存在包含关系的,利用吸收律进行化简之后就只剩下三种情况和Busy信号的暂停了,这里不给出详细的做法,留给读者思考。
对于转发情况,WB阶段的指令可以通过GPR内部转发完成W→D
的需求,而其他的情况就需要进行转发,常见的转发有M→D
,M→E
,W→E
三种,但是特殊的情况会用到其他的转发,比如下面的例子:
lw $7, 4($0)
sw $7, 8($0)
对sw
指令进行分析,没有时间上的逆向需求,所以可以进行转发,但是这里的转发就应该是W→M
了,如果再进一步讨论,可以发现对于sw rd, offset(rs)
中rd
的转发都可以放在W→M
进行,出于对之前Project的设计,笔者并没有选择改为W→M
。
关于为什么可以改为W→M
,可以考虑lw-lw-sw
对sw
的rd
转发,W→M
会改写W→E
的数据,所以数据还是最新的。
按照这样说似乎也可将一些W→E
的转发改为M→D
的转发,然而还是出于对之前Project的设计,这里不提倡修改,只需要保证最近的转发才是有效的即可(可以思考一下add-add-add
的)。
由于Project 5的通路已经设计好,这一阶段剩下的主要就是进行一些简单的通路、信号归并,产生一些新的通路、信号,使得操作变得更易控制。
考虑指导书中的图,可以发现MEM级主要的转发源是ALUOut和PC+8,WB级主要的转发源是Result,所以可以设置一个EXOut表示ALUOut或PC+8便于转发。
lui指令的rs默认为0,将操作改成GPR[rt]←GPR[rs]+(Imm || 0^16)不会影响结果,反而可以把操作并入EX阶段的ALU中。
ID阶段有一些Link型指令(jal/jalr/bltzal)的效果都是将PC+8写入GPR,所以可以增加一个LinktoReg的信号来辨识Result是否为PC+8。
可以设计一个NPC部件对PC+4、BranchAddr、JumpAddr进行筛选。
注意到Project 6还需要设计关于比较部件、乘除部件、数据存储器的信号,这是指导书的图不能给出的,但是根据其他部件的信号设计方式,可以很容易得出这些部件所需的信号,此外还要对MF型指令设计多选器的信号等等。
自己进行这样的设计对于下一个Project进行自动机状态的设计是大有裨益的。
Project 5是建立流水线CPU的主要部分,需要一定的时间进行代码的编写。而Project 6基于Project 5,但是需要增加和扩展信号的内容,相对于Project 5的工作,Project 6的工作主要是批量地引入信息、修改部件端口的属性、增加相应部件。
IF阶段注意IM的地址(将PC的第12位取反),EX阶段注意ALU的实现(见下)和乘除部件的设计,MEM阶段注意传入的信号。
如果之前对于加指令的实验操作做得还不错,这里都是很轻松的。
这里存在一个坑点,Verilog HDL是一种硬件描述语言,而不是一种程序设计语言,运算有无符号是取决于整个表达式的,Verilog的扩散性设计(如果一个运算式中任何一个运算数是无符号的,那么整个运算也会是无符号的)会使得一些情况下有符号数在计算过程中变成无符号数,这里只举两个例子。
assign C_Signed = $signed(B) >>> A[4:0];
assign C = Op == 4'b0110 ? C_Signed :
Op == 4'b1011 ? ($Signed(A) < $Signed(B) : {31'b0, 1'b0} : 0) : 0;
考虑 C 的第一行为什么这样写,由于第一个冒号之后的值是无符号的,所以 C_Signed 也是无符号的。
考虑 $signed(A)<$signed(B)?{31'b0,1'b1}:0 ,这个表达式是无符号的,但是不取决也不影响 $signed(A)<$signed(B) 的符号,就算会影响其的符号,也是影响 $signed(A)<$signed(B) 值( 0 或 1 )的符号,而对于 $signed(A)<$signed(B) 这个表达式, $signed(A) 和 $signed(B) 都是有符号的,所以运算时是有符号的,值是无符号的。
对于存取型指令,需要设计一些指令测试功能,需要避免地址异常。
对于运算型指令,需要设计一些指令测试数据冒险。
对于逻辑型指令,需要设计一些指令测试功能与数据冒险,例如lui-lw的转发还是较为难设计的(由于DM地址太小= =)。
对于跳转型指令,需要设计一些指令测试延迟槽的功能与数据冒险。
对于分支型指令,需要设计一些指令测试分支功能与数据冒险。
对于乘除型指令,需要测试乘除型指令在数字为正负零的情况下的功能,以及Move型指令的功能,还需要测试乘除型指令与非乘除型指令的并行效果。
构造一组还是很轻松的=_=,构造多组还是写代码随机生成比较好,注意存取地址的自然对齐,注意JAL&JR型的设计可以使用一些特殊的构造方式测试递归效果。
似乎第2条的做法涵盖了大部分的查BUG方法=_=,笔者想不起还有什么问题了。
最后给出笔者在开发过程中对一些事实的认知与思考,如有偏差欢迎批评指正。
笔者的做法是利用工程化方法进行分析,再经过精简压缩,使用Detector型的风格进行编程,需要对流水线有一定的理解(为此抱着黑书读了一晚上),少写什么内容比较不容易找出来。如果习惯使用批量处理,可以按照Planer型的风格进行编程,难度降低,码量增加。
使用延迟槽之后,分支或跳转指令不会立即执行,而是先执行延迟槽中的指令,再到达新的地址。
这样做使流水线的设计难度降低,提高流水线的吞吐量,硬件无需处理编译调度指令。
但是槽里放入分支或跳转指令的不确定性仍是一个问题,而且微体系架构暴露给指令集是一种兼容性差的做法,实际上延迟槽的填充率也不是很高,相比之下分支预测反而是较为实用的做法。
注意在下一个Project中,延迟槽的指令产生异常返回的地址是上一条指令的地址(然而MARS不会这样做)。
乘除相关的指令只利用到了暂停,却没有使用到转发机制,如果将Busy信号转化为Counter信号,得知乘除单元还需运行的周期数,也可增加流水线的吞吐量,只是设计上需要更加谨慎。
在Project5&6的指导书上都写有:为了解决数据冒险而设计的转发数据来源必须是某级流水线寄存器,不允许从功能部件的输出开始。
对于这种机制,可以认为是不增加计算(或读取)单元的操作延迟,更好地保证流水线的吞吐量。
在Write-Reg型指令中存在一些指令,它们可以在ID级就得到写值,例如本Project中的lui,slt,slti,sltiu,sltu
。若紧随其后的是Branch型或JR型指令,则不需要暂停,直接E→D
转发即可,可以发现这确实是理论上存在的六种转发中漏掉的那一种。
做到ID级计算写值是否会延长周期长度?可以发现这五条指令的值就是EXT部件或CMP部件的输出,实际并没有增加流水线的负担。
本Project大概存在五种不可预测行为,jalr $31, $31
、GPR[rs]
当作地址时没有自然对齐、在延迟槽中放入分支或跳转指令、整数除零、乘除后并未立即从乘除寄存器取数,在本Project中需要正常执行这些指令,产生任意结果均可。在下一个Project中需要考虑是否产生异常与中断。