简单流水线CPU设计
本文是对《CPU设计实战》(汪文祥 邢金璋 著)一书第四章的简单总结、概括。
设计一个简单的单周期CPU
CPU设计的一般方法:数据通路+控制逻辑
准备工作
如何划分模块
- 某层次设计细节很多,很难画出来,把他干成一个框,作为一个模块
- 模块的接口不应该有太多
- 可以被多次使用的,比如译码器、多路选择器、寄存器堆、RAM、FIFO(First In First Out)
- 两个模块间数据总体应该呈单向流动,至多一去一回。
PC
PC的输入有两个:一个是复位值0xBFC00000
,一个是复位撤销后每执行一条指令,当前PC+4
的结果。
虚实地址转换
在实现TLB和MMU之前,我们采用固定映射的地址映射机制。
kseg0
(0x80000000
`0x9FFFFFFF`)映射到物理地址最低512M(`0x00000000`0x1FFFFFFF
);
kseg1
(0xA0000000
`0xBFFFFFFF`)也映射到物理地址最低512M(`0x00000000`0x1FFFFFFF
);
其余三个段(kuseg
、kseg2
、kseg3
)虚地址等于物理地址。
指令RAM
由于欲实现的CPU指令宽度为32比特,所以RAM的宽度至少为32比特。但RAM还是按照字节寻址的,所以我们要将取指地址除4后取整的结果作为RAM的地址输入。
数据通路设计
ADDU指令
通用寄存器堆的读端口1和读端口2分别和rs
、rt
相连;
要实现加法,需要一个加法器。加法器两个输入src1
、src2
分别来自通用寄存器堆的rdata1
、rdata2
,输出result
连接通用寄存器堆的wdata
。
ADDIU指令
ADDIU和ADDU指令很像,如何兼顾差异性,又能最大限度复用呢?
从差异性入手,ADDIU和ADDU指令区别于ADDIU第二个操作数来自于指令的15..0
位符号拓展至32位后形成的数据。所以在加法器src2
输入端口前面加一个二选一部件。in0
来自于通用寄存器堆的rdata2
,in1
来自指令的15..0
位符号拓展至32位后形成的数据。控制信号的生成(包括接下来若干条指令的控制信号)统一放到最后说。
另一个区别,ADDU写入第rd
号寄存器,ADDIU写入第rt
号寄存器。在通用寄存器堆前面也加一个二选一部件,in0
来自rd
,in1
来自rt
。
剧透一下,这里的二选一部件,我们实现为**“独热码”**,即有多少个输入源,就安排多少个选择信号,一个信号控制一个源,要输出哪一路数据,直接将这个信号置为1即可。
SUBU指令
复用加法器来实现减法,处理方法如下:
其中~
是按位取反,上式中即为对[B]原码
的32位取反。
于是需要在加法器的src2
前面再加一个二选一部件:处理加法时不取反,直接进来;处理减法时先取反,再进来。
后面还有一个+
,可以通过操作加法器进位输入实现。处理加法进位输入是0,操作减法进位输入是1。
LW指令
访存地址生成
要求是将rs
和指令中的立即数相加,和ADDIU
一致。
从数据RAM中读数据
根据指令系统规范文档中对LW
指令的描述,LW
一次性要读4个字节写入第rt
号寄存器。所以数据RAM宽度也应该是32位,因此LW
指令的访存地址应该是4的倍数。
寄存器写回结果选择
之前我们只有ADDU
、ADDIU
两条指令写寄存器,那俩都是将加法器的结果接到通用寄存器堆的wdata
上,写入寄存器。现在又多一个LW
,那只能在寄存器堆wdata
前面再加一个二选一部件。
SW指令
SW
指令将rt
寄存器的内容写入数据RAM,因此我们将通用寄存器堆的rdata2
和数据RAM的wdata
相连,将数据RAM的写使能作为控制信号。
BEQ和BNE指令
判断分支条件
要比较两个寄存器中的数字,一种方法是复用加法器,做减法,看结果;另一种方法是设置独立的分支判断逻辑。
计算跳转目标
MIPS中有“延迟槽”的概念,由于判断分支条件会浪费一些时间,于是便可以把不管是不是跳转一定要执行的指令放在条件分支指令(注意是条件分支)的后面,在判断分支条件时并行执行,提高效率。
这意味着如果有一个PC为A的指令,使用分支跳转指令如果成功跳到B的话,其执行轨迹实际上是A、A+4、B。所以执行分支指令后面的延迟槽指令时才是真正调整PC的时机。
故BEQ
和BNE
计算跳转目标时是基于延迟槽指令的PC再加上(offset
左移两位的结果)得到的。
相关信息
在执行延迟槽指令的时候,当前PC值自然便是延迟槽指令的PC,加上上面说的计算规则,所以我们在执行延迟槽指令时,进行计算PC的工作。
所以我们还要把
offset
用一个触发器给存起来
PC更新
由于是在下一条延迟槽指令进行PC更新,~~所以我们需要将加法器(毕竟复用了ADDIU
的数据通路么)的输出放入一个触发器中。~~理解错了!!不是在执行BEQ
、BNE
指令的时候算好,而是在执行延迟槽指令的时候基于存起来的offset
算。
既然引入了分支跳转指令,nextPC
的来源不只是当前指令PC+4了。我们要给nextPC
加一个二选一部件,in0
来自PC+4,另一个输入in1
来自执行延迟槽指令时的计算。
什么时候选择触发器中的数据呢?条件有二:一是当前正在执行延迟槽中的指令,二是BEQ
、BNE
要求跳转。对于第二点,我们需要在执行BEQ
、BNE
指令的时候就用一个触发器记录下来是否跳转,以便在执行延迟槽指令时做出正确选择。
JAL指令
JAL
=Jump And Link
,其中Link
要求将该跳转的延迟槽指令PC+4的结果写入第31号寄存器中。这么做可以解决函数调用中call
和return
的问题:通常,在某处call
函数时,函数执行完成后要return
到call
的地方。那么就可以在call
函数时调用JAL
指令,将返回处PC写入寄存器,在return
时从这个寄存器中取欲返回的地址即可。
计算跳转目标
同BEQ
指令那般,我们同样需要计算跳转目标。JAL
指令计算跳转目标的方式是拼接,跳转目标由该分支指令对应的延迟槽指令的PC的最高4位与指令中的立即数instr_index
左移2位后的值拼接得到。
写寄存器
由于还要完成上文介绍的Link
流程,所以JAL
指令除了要更改PC,还要将返回地址写寄存器。返回地址理论上是延迟槽指令的下一条,即延迟槽指令的PC+4
,但由于返回地址写寄存器的过程是在执行JAL
指令而不是执行延迟槽指令时完成的,所以写寄存器的值便是JAL指令的PC+8
。
这个值如何计算呢?可以借助已有的加法器进行计算。这就要求在加法器的src1
前面加一个二选一,in0
来自寄存器堆的rs
,in1
来自当前的PC;将原本src2
前面的二选一改成三选一,加一个in2
,in2
为常值8。
由于JAL
隐含写第31号寄存器,所以寄存器堆的waddr
又要增加一个新的来源,变成三选一(书上写得三选一,个人认为到目前为止还是二选一,存疑。前面介绍的ADDU
、ADDIU
、SUBU
、LW
都是采用了指令中的rt
字段作为waddr
)。
更新PC
更新PC是在执行延迟槽指令时进行,和前面的BEQ
、BNE
想法类似。基本思路是在执行JAL
指令时将指令中的立即数存起来,执行延迟槽指令的时候再用来计算。由于计算方法和BEQ
、BNE
不同,所以原本nextPC
前面接的二选一要变成三选一了。
JR指令
JR
指令是无条件跳转指令,跳转目标为寄存器rs
中的值。
根据手册,其仍然是在延迟槽指令更改PC,所以nextPC
要变成四选一了,新增in3
信号直接和寄存器堆的读端口rdata1
相连。
SLT和SLTU指令
SLT
和SLTU
指令比较rs
和rt
寄存器中所存数据的大小,如果rs < rt
,则把rd
寄存器置1
。
比大小,可以复用加法器做减法,节省资源。
对于有符号数,先比较符号,如果一正一负,不用再比了;如果符号一致,再做减法,结果最高位如果是1,说明被减数小。
对于无符号数,直接通过减来确定。如果采取上面看最高位的思路,走不通。因为无符号数没有符号位,32位全部用来表示数字了。方案一是将32位加法器变成33位,方案二是利用加法器的Cout
,如果是1,说明被减数小。两者原理一致。
SLL、SRL和SRA指令
移位器的输入、输出
输入:被移位的数值src
(由于src
是指令中指定的rt
号寄存器的值,故来自寄存器堆rdata2
端口)、5位的位移量sa
(指令中的sa
字段)、控制移位类型的op
输出:32位移位结果res
移位器的内部实现
省流:被移位数据逆序排列,使得左移用右移实现
书上解释为啥要共用数据通路
移位逻辑本质上是译码逻辑和多路选择逻辑,32位数的移位就是对32个32位数进行32选1,这套逻辑的面积大、延迟长。
//书上是这么写的shft_src,我感觉好像反了
assign shft_src = op_srl ? {src[0], src[1], src[2], src[3],
src[4], src[5], src[6], src[7],
src[8], src[9], src[10], src[11],
src[12], src[13], src[14], src[15],
src[16], src[17], src[18], src[19],
src[20], src[21], src[22], src[23],
src[24], src[25], src[26], src[27],
src[28], src[29], src[30], src[31],} : src[31:0];
assign shft_res = shft_src[31:0] >> shft_amt[4:0];//不写[]会怎么样呢?
assign sra_mask = ~(32'hffff_ffff >> shft_amt[4:0]);
//下面就是各个指令的结果了
assign srl_res = shft_res;
assign sra_res = (sra_mask & {32{shft_src[31]}}) | shft_res;
assign sll_res = {
shft_src[0], shft_src[1], shft_src[2], shft_src[3],
shft_src[4], shft_src[5], shft_src[6], shft_src[7],
shft_src[8], shft_src[9], shft_src[10], shft_src[11],
shft_src[12], shft_src[13], shft_src[14], shft_src[15],
shft_src[16], shft_src[17], shft_src[18], shft_src[19],
shft_src[20], shft_src[21], shft_src[22], shft_src[23],
shft_src[24], shft_src[25], shft_src[26], shft_src[27],
shft_src[28], shft_src[29], shft_src[30], shft_src[31],
};
LUI、AND、OR、XOR和NOR指令
对于AND
、OR
、XOR
和NOR
指令,都是rs
与rt
寄存器做运算后,将结果写入rt
寄存器,可以复用现有的数据通路。
对于LUI
,将立即数写入rt
寄存器中,也可以复用已有通路。(复用LW
的)
ALU
我们可以将ADDU
、ADDIU
、SUBU
、SLT
、SLTU
、SLL
、SRL
、SRA
、LUI
、AND
、OR
、XOR
和NOR
指令处理运算的逻辑集中到一个模块中。
输入:32位的alu_src1
、alu_src2
,控制信号alu_op
(采用独热码)
输出:32位的alu_res
、32位的mem_addr
(结果直接来自加法器,优化访存指令的时序)
控制信号
为了节省篇幅,仅介绍大致思路,更详细的内容请看书。
ALU的控制信号
ALU的控制信号指的是alu_op
,设计成独热码。
有些指令虽然不是ALU运算指令,但是却会用到譬如加法器、移位器等部件。这就意味着不同的指令,可能会共用一个alu_op
。
选择器的控制信号
上文中我们引入了一些“X选一”选择器,这一小节主要关注如何处理这些“X选一”选择器的控制信号。
这些控制信号到底取什么,取决于当前指令是什么。所以我们可以先对指令进行“分析”,获知当前是什么指令,sa
域是多少,func
域是多少,然后便知道这是条什么指令了:
assign op = inst[31:26];
assign sa = inst[10:6];
assign func = inst[5:0];
decoder_6_64 u_dec0(.in(op), .out(op_d));
decoder_5_32 u_dec0(.in(sa), .out(sa_d));
decoder_6_64 u_dec0(.in(func), .out(func_d));
assign inst_addu = op_d[6'h00] & func_d[6'h21] & sa_d[5'h00];//是不是ADDU指令?
//...
于是,对于只有1位的控制信号,可以这么写:
assign data_ram_en = inst_lw | inst_sw;
//只有是inst_lw、inst_sw的时候,ram_en是1
//很清晰吧!
书上没有给多位控制信号的例子,我想或许可以用移位写?
如果有多位控制信号、且采用独热码,可以将每一位看成是1位的控制信号,针对每一位进行操作:
assign alu_op[ 0] = inst_addu | inst_addiu | inst_lw | inst_sw | inst_jal;
assign alu_op[ 1] = inst_subu;
assign alu_op[ 2] = inst_slt;
assign alu_op[ 3] = inst_sltu;
assign alu_op[ 4] = inst_and;
assign alu_op[ 5] = inst_nor;
assign alu_op[ 6] = inst_or;
assign alu_op[ 7] = inst_xor;
assign alu_op[ 8] = inst_sll;
assign alu_op[ 9] = inst_srl;
assign alu_op[10] = inst_sra;
assign alu_op[11] = inst_lui;