简单流水线CPU设计

jielahou大约 12 分钟

本文是对《CPU设计实战》(汪文祥 邢金璋 著)一书第四章的简单总结、概括。

设计一个简单的单周期CPU

CPU设计的一般方法:数据通路+控制逻辑

准备工作

如何划分模块

  • 某层次设计细节很多,很难画出来,把他干成一个框,作为一个模块
  • 模块的接口不应该有太多
  • 可以被多次使用的,比如译码器、多路选择器、寄存器堆、RAM、FIFO(First In First Out)
  • 两个模块间数据总体应该呈单向流动,至多一去一回。

PC

PC的输入有两个:一个是复位值0xBFC00000,一个是复位撤销后每执行一条指令,当前PC+4的结果。

虚实地址转换

在实现TLB和MMU之前,我们采用固定映射的地址映射机制。

kseg00x80000000`0x9FFFFFFF`)映射到物理地址最低512M(`0x00000000`0x1FFFFFFF);

kseg10xA0000000`0xBFFFFFFF`)也映射到物理地址最低512M(`0x00000000`0x1FFFFFFF);

其余三个段(kusegkseg2kseg3)虚地址等于物理地址。

指令RAM

由于欲实现的CPU指令宽度为32比特,所以RAM的宽度至少为32比特。但RAM还是按照字节寻址的,所以我们要将取指地址除4后取整的结果作为RAM的地址输入。

数据通路设计

ADDU指令

通用寄存器堆的读端口1和读端口2分别和rsrt相连;

要实现加法,需要一个加法器。加法器两个输入src1src2分别来自通用寄存器堆的rdata1rdata2,输出result连接通用寄存器堆的wdata

ADDIU指令

ADDIU和ADDU指令很像,如何兼顾差异性,又能最大限度复用呢?

从差异性入手,ADDIU和ADDU指令区别于ADDIU第二个操作数来自于指令的15..0位符号拓展至32位后形成的数据。所以在加法器src2输入端口前面加一个二选一部件。in0来自于通用寄存器堆的rdata2in1来自指令的15..0位符号拓展至32位后形成的数据。控制信号的生成(包括接下来若干条指令的控制信号)统一放到最后说。

另一个区别,ADDU写入第rd号寄存器,ADDIU写入第rt号寄存器。在通用寄存器堆前面也加一个二选一部件,in0来自rdin1来自rt

剧透一下,这里的二选一部件,我们实现为**“独热码”**,即有多少个输入源,就安排多少个选择信号,一个信号控制一个源,要输出哪一路数据,直接将这个信号置为1即可。

SUBU指令

复用加法器来实现减法,处理方法如下:

[A]原码31..0[B]原码31..0=[A]原码31..0+(\textasciitilde[B]原码31..0)+1 [A]_{原码31..0}-[B]_{原码31..0}=[A]_{原码31..0}+(\textasciitilde[B]_{原码31..0})+1

其中~是按位取反,上式中即为对[B]原码的32位取反。

于是需要在加法器的src2前面再加一个二选一部件:处理加法时不取反,直接进来;处理减法时先取反,再进来。

后面还有一个+,可以通过操作加法器进位输入实现。处理加法进位输入是0,操作减法进位输入是1。

LW指令

访存地址生成

要求是将rs和指令中的立即数相加,和ADDIU一致。

从数据RAM中读数据

根据指令系统规范文档中对LW指令的描述,LW一次性要读4个字节写入第rt号寄存器。所以数据RAM宽度也应该是32位,因此LW指令的访存地址应该是4的倍数。

寄存器写回结果选择

之前我们只有ADDUADDIU两条指令写寄存器,那俩都是将加法器的结果接到通用寄存器堆的wdata上,写入寄存器。现在又多一个LW,那只能在寄存器堆wdata前面再加一个二选一部件。

SW指令

SW指令将rt寄存器的内容写入数据RAM,因此我们将通用寄存器堆的rdata2和数据RAM的wdata相连,将数据RAM的写使能作为控制信号。

BEQ和BNE指令

判断分支条件

要比较两个寄存器中的数字,一种方法是复用加法器,做减法,看结果;另一种方法是设置独立的分支判断逻辑

计算跳转目标

MIPS中有“延迟槽”的概念,由于判断分支条件会浪费一些时间,于是便可以把不管是不是跳转一定要执行的指令放在条件分支指令(注意是条件分支)的后面,在判断分支条件时并行执行,提高效率。

这意味着如果有一个PC为A的指令,使用分支跳转指令如果成功跳到B的话,其执行轨迹实际上是A、A+4、B。所以执行分支指令后面的延迟槽指令时才是真正调整PC的时机

BEQBNE计算跳转目标时是基于延迟槽指令的PC再加上(offset左移两位的结果)得到的。

相关信息

在执行延迟槽指令的时候,当前PC值自然便是延迟槽指令的PC,加上上面说的计算规则,所以我们在执行延迟槽指令时,进行计算PC的工作。

所以我们还要把offset用一个触发器给存起来

PC更新

由于是在下一条延迟槽指令进行PC更新,~~所以我们需要将加法器(毕竟复用了ADDIU的数据通路么)的输出放入一个触发器中。~~理解错了!!不是在执行BEQBNE指令的时候算好,而是在执行延迟槽指令的时候基于存起来的offset

既然引入了分支跳转指令,nextPC的来源不只是当前指令PC+4了。我们要给nextPC加一个二选一部件,in0来自PC+4,另一个输入in1来自执行延迟槽指令时的计算。

什么时候选择触发器中的数据呢?条件有二:一是当前正在执行延迟槽中的指令,二是BEQBNE要求跳转。对于第二点,我们需要在执行BEQBNE指令的时候就用一个触发器记录下来是否跳转,以便在执行延迟槽指令时做出正确选择。

JAL指令

JAL=Jump And Link,其中Link要求将该跳转的延迟槽指令PC+4的结果写入第31号寄存器中。这么做可以解决函数调用中callreturn的问题:通常,在某处call函数时,函数执行完成后要returncall的地方。那么就可以在call函数时调用JAL指令,将返回处PC写入寄存器,在return时从这个寄存器中取欲返回的地址即可。

计算跳转目标

BEQ指令那般,我们同样需要计算跳转目标。JAL指令计算跳转目标的方式是拼接,跳转目标由该分支指令对应的延迟槽指令的PC的最高4位与指令中的立即数instr_index左移2位后的值拼接得到。

写寄存器

由于还要完成上文介绍的Link流程,所以JAL指令除了要更改PC,还要将返回地址写寄存器。返回地址理论上是延迟槽指令的下一条,即延迟槽指令的PC+4,但由于返回地址写寄存器的过程是在执行JAL指令而不是执行延迟槽指令时完成的,所以写寄存器的值便是JAL指令的PC+8

这个值如何计算呢?可以借助已有的加法器进行计算。这就要求在加法器的src1前面加一个二选一,in0来自寄存器堆的rsin1来自当前的PC;将原本src2前面的二选一改成三选一,加一个in2in2为常值8。

由于JAL隐含写第31号寄存器,所以寄存器堆的waddr又要增加一个新的来源,变成三选一(书上写得三选一,个人认为到目前为止还是二选一,存疑。前面介绍的ADDUADDIUSUBULW都是采用了指令中的rt字段作为waddr)。

更新PC

更新PC是在执行延迟槽指令时进行,和前面的BEQBNE想法类似。基本思路是在执行JAL指令时将指令中的立即数存起来,执行延迟槽指令的时候再用来计算。由于计算方法和BEQBNE不同,所以原本nextPC前面接的二选一要变成三选一了。

JR指令

JR指令是无条件跳转指令,跳转目标为寄存器rs中的值。

根据手册,其仍然是在延迟槽指令更改PC,所以nextPC要变成四选一了,新增in3信号直接和寄存器堆的读端口rdata1相连。

SLT和SLTU指令

SLTSLTU指令比较rsrt寄存器中所存数据的大小,如果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指令

对于ANDORXORNOR指令,都是rsrt寄存器做运算后,将结果写入rt寄存器,可以复用现有的数据通路。

对于LUI,将立即数写入rt寄存器中,也可以复用已有通路。(复用LW的)

ALU

我们可以将ADDUADDIUSUBUSLTSLTUSLLSRLSRALUIANDORXORNOR指令处理运算的逻辑集中到一个模块中

输入:32位的alu_src1alu_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;
Loading...