北航2021届计组 -- 单周期CPU设计

单周期CPU设计

写在前面的话

​ 首先需要强调的是,这是一篇考后反思,所以相比于之前的HDLBit的以力证道,或者MIPS的技之巅峰(不要笑,我的技巧最高水平就那样了)。这篇文章要温吞很多。这篇文章讲的东西是一个单周期CPU的设计思路。正如大家所见,我不聪明,所以其实我写这种需要大量智商的东西是很羞愧的,所以大家就看个热闹就好了。

​ 其实最精华的部分是我的代码,但是因为规定,是没有办法放出来的(其实我挺喜欢这个规定的,这样就好歹给我的智商留了一块遮羞布,不用把代码拿出来让大家检验)。其次精华的是第二、三章,但是实话实话,我自己看着都想睡觉。所以大家就看着个乐呵吧。第一章说的虽然都是我的总结,但是跟比如说岳哥哥,或者是松泽哥哥交流,或者看吴佬代码,或者看好多人完成P4的速度。好像这些东西大家都知道。这也不奇怪,因为比较笨的原因,确实很多比较显而易见的道理,我需要一些时间总结。所以大家就笑笑就好了。

一、设计思路总论

1.1 P3教训

​ 我参加的是第一次P3上机,在考场上做出来了第一道题和第三道题,因为这场考试做的十分艰难(没挂真的是万幸),所以有必要好好反思一下问题都出在了哪里。

​ 我卡的最久的是第一题,第一题是bezal,就是说判断一个寄存器内的值是否为0,如果是,就执行跳转操作和链接操作,如果不是,就相当于一个nop指令。在这道题中,我条件判断只管住了链接操作,也就是说,跳转操作无论条件判断如何,都会执行,所以就错了(应该是倒数第二个点)。所以我这道题做了一个半小时。

​ 然后在第二题,slo,这道题说的是左移shamt位,然后左移的部分补1。这道题我跳过了,是因为我的ALU只有SrcA和SrcB两个输入端口,没有设置shamt的输入端口,所以如果想进行运算,就需要拓展端口,但是在考场上拓展端口,对我的心理压力过大。我最后是以在ALU外部搭建计算通路的方法实现的。

​ 最后在第三题,lwor,就是用or的方式计算地址,然后lw,这没有什么难的,难点在地址的计算,这个计算也是涉及了三个运算数,应该是一个寄存器内的32位数,与一个拓展后的立即数进行或运算,然后再加上一个5位的数乘4。所以还是需要三输入端口ALU,显然我是没有的,所以我又搭建了一条旁路。最后侥幸过关(第二题也是利用了这条通路,但是最后太慌张了,点错按钮了,不过归根结底是考前准备步骤和设计理念不成熟)。

1.2 到底要加几条指令?

​ 我在做课下P3的时候,跟其他的p不同,就是我很焦虑。因为第一次弄这个,然后身边的同学就搭了好多条指令,有的人的CPU都能跑全排列了(别看别人,就是你杰哥)。我倒是不求说搭的指令最多,但是别人都弄我不弄,就很紧张,所以也调了几条搭建了几下。但是正如上面的分析,因为没有搭到移位指令,所以我的ALU是残缺的,只搭建了一个分支指令beq,所以导致我的beq在课下的时候就是一个补丁一样的存在,这也造成了对于第一题我的疏忽。

​ 我认为,加指令的目的是有两个:完善模块端口和功能优化控制。就比如sll,不加它,ALU就只能计算两个数,所以不加不行。又比如beq,如果只有一个,控制信号可以长得很随意就实现了,非得是加一个bgez,才能达到独立CMP模块的目的和控制信号统一的目的。所以到底要加几条指令?我这里认为加这么几条指令就够了,需要强调指令本身不重要,重要的是通过加指令,能给CPU本身带来怎样的优化。如果只关注指令本身的实现,就会导致很多补丁的产生,反而对CPU整体的设计有害。我觉得好的加指令操作是可以是CPU更加简洁优雅,而不是更加冗杂

​ 我认为本着能懒就懒的原则,如果想生成一个比较功能强大而且优雅的CPU,需要这样几条指令,我会解释加他们的目的。目的比实现更重要。

指令 目的
subu ALU的两寄存器运算
sll ALU的两寄存器和5位数运算
ori EXT功能和ALU与EXT联系
lw 从DM加载到GRF
sw 从GRF存储到DM
lui 将寄存器加载到GRF,可以衍生GRF的一个功能控制信号
slt 选定寄存器置1,可以衍生GRF的另一个控制信号
jr 特定寄存器置PC + 4,可以衍生GRF的另一个控制信号,和补全数据通路
sh 增加DM中的输入时,对DMIn数据进行预处理电路
lb 增加DM输出时时,对数据的处理电路
beq 增加NPC的功能
bgtz 产生CMP模块,优化控制信号

1.3 指令的分解

​ 这一章来自吴哥哥的思想,就是对指令进行分类,经过长达四天的实践,我觉得还可以优化为对指令进行分解,因为指令是功能的单位,但是不是功能的最小单位,比如说lw,就可以分解为计算地址读取DM数据存到GRF这两个部分,而像考试中的bezal就是比较分支,**加载(一类特殊的存储)**的三个部分。所以到底CPU有几个功能呢,这个我其实也是通过实践归纳总结的,就是在第三章的内容,我个人将其分为六个部分:load、store、caculate、compare、branch、extend。我觉得我遇见所有指令都可以表示为这六个功能的“线性组合”。

功能 线性
load GRF[num] <=
store Mem[Address] <=
caculate operation1 operator1 operation2 operator2 operation3
branch PC <=
compare if(condition)
extend 32-digit <= extend(not-32-digit)

1.4 什么样的模块是好模块?

​ 我认为一个好的模块,就是用来实现一个功能的。为什么这么说呢?因为其实一个模块在一个周期内只能干一件事,比如尽管ALU既能算加法,又能算减法,但是一个周期只能干加法或者减法。举个例子,在我最开始的设计中,我是没有EXT模块的,我的ALU有一个16位的端口用来处理立即数,但是为什么不好呢,因为我的ALU的控制变复杂了,我的ALU有一个运算叫做零扩展到32位再加法,不难想象,还会有一个运算叫做符号扩展到32位再做加法、符号扩展到32位再做加法,这样没完没了。这种再举一下CMP模块(用来比较两个数,或者一个数和0)的例子,如果有一个跳转指令,如果不独立CMP模块,就会让calculate和compare不能同时出现在一个指令中。正是因为功能之间是线性无关的,所以也就是说,至少一个模块分别实现一个功能。而简洁性的要求,所以最好用越少的模块实现一个功能。结合这两点,最好一个模块实现一个功能。所以有6个功能模块:IFU(我拆成了NPC和IFU,只是疏忽,可以合并)(branch)、GRF(load)、EXT(extend)、ALU(caculate)、CMP(compare)、DM(store)。

​ 此外,还有一个事情,我觉得还是一个很漂亮的设计,就是统一模块的接口的规格。以教材为例,NPC最开始只有一个16位的branch类指令的输入,但是当我们加入j指令的时候,就需要再多增加一个26位的输入接口。如果j指令是在考场上加的,可以想到,我需要在NPC上加个端口,需要把26位数据接进去,我还要搭建一条专门的计算通路来扩展这个数,然后再搭一个专门的计算通路来处理这个数,然后还要想办法加一个控制信号。太麻烦了,而且极难检验,在考场上,只要不是考试型的天才,都容易犯错误。而且这种困难是无法预料的,无论加多少条指令,只要出现新的指令截断,就在不可预测的位置会造成新的端口,然后就会有一连串的反映。所以最好的办法就是统一端口,这样新的数据只要将其拓展为32位,就可以利用原来的数据通路了,唯一需要修改的就是EXT模块和控制信号。

1.5 什么样的控制是好控制?

​ 我认为好的控制是控制信号是简洁的,这里可能是我个人的偏好,我不太喜欢教材中的控制信号,因为不能望文生义,而且过于繁杂,看名字都不知道是控制哪一个模块的的控制信号,也不知道一个模块有几个控制信号。

​ 我认为好的控制信号对于每个模块只有三种,决定是否发挥功能的使能信号决定数据来源的选择信号决定具体功能的功能信号。使能信号每个模块最多只有一个(在MIPS里只用GRF和DM有),功能信号每个模块只能有一个,数据来源信号每个输入端口最多有一个。只要对控制信号分好类,就可以很清楚的控制CPU。

​ 本质上,对控制信号的思考就是对指令过程的思考。对于一个指令,我们要思考的只有三个问题:哪个模块干活?(使能)干活的模块处理的数据是什么?(选择)要怎样处理数据(功能)只要回答清楚了这三个指令,就可以快速的在考场上搭建一条指令。

​ 在我的CPU中,每次加一条指令,其实就是从这段代码中选择一部分复制粘贴,稍加修改,我觉得是比较好的办法。

//Load
RegWrite = 1;
RegAdd3Sel = RegAdd3Sel_rt/rd;
RegInSel = RegInSel_ALUOut/DMOut/PC/EXTOut;
GRFOP = GRFOP_FULL/LUI/LINK/SLT;
DMOP = DMOP_W/H/B;

//Store
MemWrite = 1;
DMOP = DMOP_W/H/B;

//Caculate
SrcBSel = SrcBSel_EXTOut/RegOut2;
ALUOP = ALUOP_ADD/SUB/OR;

//Compare
CMPOP = CMPOP_;

//Branch
BranchSel = BranchSel_RegOut1/EXTOut;
NPCOP = NPCOP_NORMAL/BRANCH/J/JR; 

//Extend
EXTOP = EXTOP_ZE16/ZE26/SE16;

​ 在后面会有详细的设计介绍。

二、模块介绍

2.1 NPC

​ NPC没有按照我之前都构想与PC合成一个整体(一个大的IFU),这是因为link操作需要将下一条的地址写入寄存器,如果写成一个大的IFU,那么就会有两个输出端口了(一个输出PC,一个输出nextPC),这与我的设计思想不符。此外,我也没有采用更多更加不整齐的输入端口,也是因为与我的设计思想不符,我将这部分功能分摊给了NPCOP和EXT。

NPC端口:

端口 方向 宽度 解释
PC IN 32 当前指令地址
branch IN 32 所有分支或者跳转指令的待处理数据
nextPC OUT 32 下一条指令地址
NPCOP IN 4 NPCOP控制了NPC以什么样的方式确定下一条指令的地址

NPCOP:

信号 编码 解释
NORMAL 0 PC + 4
BRANCH 1 PC + 4 + branch << 2
J 2 PC[31:28], branch[25:0], 00
JR 3 branch

2.2 IFU

​ 里面包含PC和IM,因为大部分的控制功能都被NPC承担了,所以这个IFU的就没有那么多功能了,当然他也两个输入端口了,emmm,我当时设计的时候还以为是一个,所以就没拆成IM和PC,而且PC自己一个寄存器一个模块也太傻了。确实这里不够优雅。因为本质是时序电路,所以还需要接入时钟和复位信号,GRF和DM也相同

IFU端口

端口 方向 宽度 解释
clk IN 1 时钟
reset IN 1 同步复位信号
nextPC IN 32 下一条指令地址
PC OUT 32 当前指令地址
instr OUT 32 当前指令

​ IFU没有控制信号

2.3 GRF

​ GRF就是很普通的设计,这里我没有用原来的端口命名,是因为A1,A2这样的名字在考试中还需要与rs,rt这样的名字对应起来,太间接了,所以直接用的是指令集中的命名,这样避免名称转换时的错误。

GRF端口

端口 方向 宽度 解释
PC in 32 评测机需要
clk IN 1 时钟信号
reset IN 1 同步复位
RegAdd1 IN 5 读出寄存器1的地址
RegAdd2 IN 5 读出寄存器2的地址
RegAdd3 IN 5 写入寄存器的地址
RegIn IN 32 写入寄存器的成熟待处理数据
RegOut1 OUT 32 寄存器r1中的内容
RegOut2 OUT 32 寄存器r2中的内容
RegWrite IN 1 控制是否写入
GRFOP IN 4 控制数待处理数据的处理方式

​ GRF本来应该没有控制信号,这是因为像lh,lb这样数据的预处理需要在DM中进行(因为需要地址信息,我不想再接一个DMAdd到GRF上),所以本来从DM中写回数据应该是成熟的,但是因为有一个叫做lui的指令,它是加载立即数,也就是不需要通过DM获得数据,所以还是需要GRF对数据进行处理,所以为了避免上机的时候有这类指令,还是加一个GRFOP来以防万一吧。

​ 好像set类指令和link也是需要用到这个功能的,还不错诶,没有白瞎。

GRFOP:

信号 编码 解释
FULL 0 不处理
LUI 1 左移16位
LINK 2 PC + 4
SLT 3 置1

2.4 EXT

​ EXT的功能其实不只是扩展数据,我更愿意将其理解为他是一个适配器,即接口转换装置,通过这个装置,我可以将原来不规整的16位或者26的数据转换成我想要的数据宽度,也就是32位。EXT是我的设计思想的一个代表体现。

EXT端口:

端口 方向 宽度 解释
EXTIn IN 26 其实包括了Imm16和Imm26两种
EXTOut OUT 32 获得规整的32位数
EXTOP IN 4 调整扩展的方式

EXTOP:

信号 编码 解释
ZE16 0 零拓展16位
SE16 1 符号拓展16位
ZE26 2 零拓展26位

2.5 ALU

​ 我的一个重要的设计思想就是分化ALU的功能,所以可以看到ALU原来的比较器功能被单独形成了一个CMP模块,lui其实也可以用ALU很简洁的实现,但是我还是把它放到了GRF中实现,是为了对指令的多个功能分别思考,不让其互相妨碍。所以我的ALU只有计算求地址两个功能。需要注意的是,ALU的是四输入端,有一个5位的SHF,这主要是为了适应sll这种指令,可以看到,在P3的lwor原创指令中,也是需要把r2接到SHF端口,才能在不在模块外外接组合电路的前提下(我甚至没过ALU),达到实现指令的目的。

ALU端口:

端口 方向 宽度 解释
SrcA IN 32 第一个运算数
SrcB IN 32 第二个运算数
SHF IN 5 补充运算数
ALUOut OUT 32 运算结果
ALUOP IN 4 控制运算的种类

ALUOP:

信号 编码 解释
ADD 0 加法
SUB 1 减法
OR 2

2.6 CMP

​ 这是我单独组成的一个模块(受到了吴哥哥的启发),但是与吴哥哥不同的是,吴哥哥好像只写了beq指令(他还写了blez,但是我没看懂咋实现的,我的错),所以他的CMP的功能还是不够强大(但是在只写了一个指令的情况下,还能把它单独成块,这种意识,我愿称之为神)。所以我的CMP功能更多一些。而且全都是输出到CU模块,因为这是我的集中控制的设计思想。这个也是一个单输出端口,用到了CMPOP来减少端口的数量至一个。

CMP端口:

端口 方向 宽度 解释
num1 IN 32 第一个比较数
num2 IN 32 第二个比较数,当与零比较的时候,不会被用到
CMPOut OUT 1 一个布尔值,来表示比较结果是否符合CMPOP要求
CMPOP IN 4 控制比较的种类

​ 其实CMPOP最多有12(大于,小于,等于,不等,大于等于,小于等于一共6个,与0比较翻一倍)个,还是都写了为好,省的补代码了。

CMPOP:

信号 编码 解释
EQ 0 等于
G(好像GT是保留字) 1 大于
LT 2 小于
NE 3 不等
GE 4 大于等于
LE 5 小于等于
EQZ 6 等于零
GTZ 7 大于零
LTZ 8 小于零
NEZ 9 不等零
GEZ 10 大于等于零
LEZ 11 小于等于零

2.7 DM

​ DM的创新点就是在于解决了sb和lb这类指令的问题,吴佬的宏定义很漂亮,但是可能是在store的时候69 、 70两行有错误。

DM端口:

端口 方向 宽度 解释
PC IN 32 用来评测
clk IN 1 时钟信号
reset IN 1 同步复位
DMAdd IN 32 DM读数据的地址
DMIn IN 32 DM输入的待处理数据
DMOut OUT 32 DM输出数据
MemWrite IN 1 控制是否向DM中写入数据
DMOP IN 4 控制处理的操作

​ sb和lb是可以共用一个信号的,同理,lh和sh也是可以共用一个信号的。

DMOP:

信号 编码 解释
W 0 不进行任何处理
B 1 加载一个字节
H 2 加载半个字

2.8 CU

​ CU其实也是个组合逻辑电路,所以没必要神话他的地位。CU的输出控制信号可以分为两类:决定其他模块功能的OP类信号决定模块数据来源的Sel类信号。我的CU体现了集中控制的思想,所有的控制信号必须从CU中输出,这样做的目的是为了使模块化程度更深,而且更加好debug和增加指令(我增加指令想要达到的效果是只修改CU,而不改变数据通路)。

CU的输入信号:

端口 方向 宽度 解释
opcode IN 6 instr[31:26]
funcode IN 6 instr[5:0]
CMPOut IN 1 用来产生正确的b类指令(其实一个b类指令对应两种控制信号)

CU的OP信号和使能信号:

端口 方向 宽度 解释
NPCOP OUT 4 控制NPC行为
RegWrite OUT 1 控制是否向GRF写数据
GRFOP OUT 4 控制GRF行为
EXTOP OUT 4 控制EXT行为
ALUOP OUT 4 控制ALU行为
CMPOP OUT 4 控制CMP行为
MemWrite OUT 1 控制是否向DM写数据
DMOP OUT 4 控制DM行为

​ 因为Sel信号我规定都为3位,所以我的mux都是sel为3位的8选1

CU的Sel信号:

选择信号 0 1 2 3 4 5 6 7
RegInSel ALUOut DMOut PC EXTOut
RegAdd3Sel rt rd
SrcBSel RegOut2 EXTOut
BranchSel RegOut1 EXTOut

三、指令功能分解

3.1 LoadFromDM

关于GRF和CU,有

指令 RegWrite RegAdd3Sel RegInSel GRFOP
standard 1 rt DMOut FULL
lw 1 rt DMOut FULL
lb 1 rt DMOut FULL
lh 1 rt DMOut FULL

​ 因为是从DM中加载,所以一定从DM中取数据。所以只需要考虑取出的数据以什么形式存储。

关于DM和CU

指令 MemWrite DMAdd(默认) DMInSel DMOP
standard 0 ALUOut - W/H/B
lw 0 ALUOut - W
lb 0 ALUOut - B
lh 0 ALUOut - H

​ 虽然不需要考虑往DM中存数据,但是依然要用DMOP来决定读出的数据的形式。

3.2 LoadNeedOP

​ 加载到寄存器中的值不再是从DM中获得的,而且需要GRFOP的帮助,所以要么是立即数,要么是set类指令。

关于GRF和CU

指令 RegWrite RegAdd3Sel RegInSel GRFOP
standard 1
lui 1 rt EXTOut LUI
slt 1

3.3 LoadFromALU

​ 加载到寄存器的值是ALU的计算结果。

关于GRF和CU

指令 RegWrite RegAdd3Sel RegInSel GRFOP
standard 1 ALUOut
addu 1 rd ALUOut FULL
subu 1 rd ALUOut FULL
ori 1 rt ALUOut FULL
sll 1 ALUOut

​ 需要修改的是RegWrite、RegAdd3Sel、RegInSel,GRFOP。

3.4 LoadAsLink

​ 加载到寄存器的值是下一条指令的地址,即link操作

关于GRF和CU

指令 RegWrite RegAdd3Sel RegInSel GRFOP
standard 1 - PC LINK
jal 1 - PC LINK

3.3 Calculate1

​ 指的是计算一个寄存器和立即数的类型,包括地址计算和立即数计算

关于ALU和CU

指令 SrcA(默认) SrcBSel SHF(默认) ALUOP
standard RegOut1 EXTOut -
lw RegOut1 EXTOut - ADD
lb RegOut1 EXTOut - ADD
lh RegOut1 EXTOut - ADD
sw RegOut1 EXTOut - ADD
sb RegOut1 EXTOut - ADD
sh RegOut1 EXTOut - ADD
ori RegOut1 EXTOut - OR

​ 只有SrcA比较稳定,其他的输入端口和计算方式都有可能变化(就像P3的lwor指令,SrcB好像是立即数,SHF也需要输入一个数,算是神奇利用了)。

3.4 Calculate2

​ 指的是需要两个寄存器中内容的运算。因为没有了立即数,所以不再需要EXT模块

关于ALU和CU

指令 SrcA(默认) SrcBSel SHF(默认) ALUOP
standard RegOut1 RegOut2 -
addu RegOut1 RegOut2 - ADD
subu RegOut1 RegOut2 - SUB

​ 需要修改的控制信号是SrcBSel和ALUOP。

3.5 Calculate3

​ 指的是不仅用到了两个寄存器中的内容(也可能是一个寄存器一个立即数,但是常见指令没有,这里就不改EXT模块了),还用到了SHF字段。

关于ALU和CU

指令 SrcA(默认) SrcBSel SHF(默认) ALUOP
standard RegOut1 SHF
sll

3.6 Store

关于DM和CU

指令 MemWrite DMAdd(默认) DMIn(默认) DMOP
standard 1 ALUOut RegOut2
sw 1 ALUOut RegOut2 W
sb 1 ALUOut RegOut2 B
sh 1 ALUOut RegOut2 H

3.7 Compare

关于CMP和CU

指令 num1(默认) num2(默认) CMPOP
standard RegOut1 RegOut2
beq RegOut1 RegOut2 EQ
bgtz RegOut1 RegOut2
slt RegOut1 RegOut2

3.8 Branch

​ 这里的Branch操作包括beq或者jr或者j产生的跳转,简而言之就是所有使nextPC不指向紧挨着当前PC + 4的操作。

关于NPC和CU

指令 BranchSel NPCOP
standard
beq EXTOut BRANCH/NORMAL
bgtz
jal EXTOut J
jr RegOut1 JR

3.9 Extend

关于EXT和CU

指令 EXTOP
standard
lw SE16
lb SE16
lh
sw SE16
sb
sh
ori ZE16
beq SE16
lui ZE16
jal ZE26

四、代码风格

4.1 宏还是参数?

​ 到底是用宏还是用参数parameter,我个人喜欢parameter,是因为如果用宏的话,各个模块可能有相似的信号,比如CU需要一个代表beq的宏,NPC判断的时候也需要一个代表beq的宏,为了区别这俩,可能就需要加前缀了(吴哥哥就是这么做的)。但是如果是像我这样记忆力比较差的,对于加了前缀的命名就很头痛,所以不如每个模块里进行parameter,这样更加简洁。但是需要注意的是,前缀还是没有办法避免的,尽管在功能模块里可以避免前缀(因为不会冲突),但是在CU里,每个信号都需要被定义,那么就是还是需要前缀的,所以其实两种方法都可以。用宏只用定义一遍,但是打字比较难,代码我觉得比较丑。用参数代码比较好看,但是需要定义两遍。

​ 但是其实我还是觉得,我的更好一些,是因为如果用一个头文件对其进行宏定义,那么定义的宏相当于是全局可见了,而用模块内的参数定义,参数的可见范围仅限于模块内,所以封装性更好一些。

4.2 端口竖直排列,按名称连接

​ 这个其实很多人都在用哈,就是因为连接的端口太多了,所以竖着会好看一些,但是只有少部分人用名称确定端口位置,因为毕竟是自己代码,写的时候都能记住每个位置应该输入哪个信号。但是因为我太笨了,所以我担心我考场上忘了位置,再两个文件之间回看,所以还是喜欢名称连接法,尤其是在MUX类有奇用。示例如下:

MUX8 BranchMux(
.out(branch),
.sel(BranchSel),
.in0(RegOut1),
.in1(EXTOut)
);

4.3 命名规范

​ 这个我自己做的也不是太好,因为确实P3第一次搭CPU,所以抄了很多教材,教材怎么命名,我就怎么命名,然后课件上就有一些命名,然后课下有的时候也对端口命名提出了要求,然后我也不能像之前那样记住所有东西。最后反正搞来搞去,命名就一直很糟糕,哪怕这次重写P4,命名也没有统一。我认为一套好的命名规范,可以让我只要想到这个功能或者需求,就能想起所需要的信号或者端口的名字。这其实不止是命名本身的问题,其实还有设计的问题。就好比如果一家一脉单传,就很好区分他是第几代人,但是如果近亲结婚,就很难再论辈分。简洁的命名其实是跟简洁的设计相关的。

​ 就我个人而言,我在这里提出了一套规范,适合我自己用,也在上面的设计中有所体现。

事物 命名
使能信号 对象 + “Write”
功能信号 模块名 + “OP”
选择信号 端口名 + “Sel”
输出端口 模块名 + “Out”
数据输入端口 模块名 + “In”
地址输入端口 模块名 + “Add” + 编号

4.4 不用三目运算符

​ 所谓的三目运算符就是这样:

assign npc = (Br == `BR_pc4) ? pc + 4 :
                 (Br == `BR_j) ? {pc[31:28], imm26, 2'b0} :
                 (Br == `BR_jr) ? RD2 :
                 (Br == `BR_beq && jump) ? pc + 4 + {{14{imm26[15]}}, imm26[15:0], 2'b0} :
                 pc + 4;

​ 其实也没啥不好的,我不喜欢可能就是我个人习惯问题,我还是喜欢声明成reg变量后用**always @(*)**写,像这样

always @(*) begin
    case(NPCOP)
        NORMAL:
            nextPC = PC + 4;
        BRANCH:
            nextPC = PC + 4 + (branch << 2);
        J:
            nextPC = {PC[31:28], branch[25:0], 2'b00};
        JR:
            nextPC = branch;
        default:
            nextPC = 32'dx;
    endcase
end

​ 感觉会好看一些。

4.5 代码技巧

4.5.1 DM中的宏

​ 这个吴佬就是牛逼哈,没啥说的,好像SH和SB写错了,然后输出可能也是有问题。但是这个写法实在是太好看了。大家膜就好了。

`define word memory[waddr]
`define half `word[15 + 16 * addr[1] -:16]
`define byte `word[7 + 8 * addr[1:0] -:8]

always @(posedge clk) begin
	if (reset) begin
		for (i=0; i<1023; i=i+1) memory[i] <= 0;
	end
	else if (DMWr) begin
		if (DMType == `DM_w) `word <= WD;
		else if (DMType == `DM_h) `half <= WD[15 + 16 * addr[1:1] -:16];
        //修改
        //else if (DMType == `DM_h) `half <= WD[15:0];
		else if (DMType == `DM_b) `byte <= WD[7 + 8 * addr[1:0] -:8];
        //修改
        //else if (DMType == `DM_b) `byte <= WD[7:0];
		$display("@%h: *%h <= %h", pc, addr, WD);
	end
end
4.5.2 参数重载

​ 这个主要是万老师上课的时候,提到可以把不同数据位的MUX写在一个文件里,但是其实只要sel位相同,都可以用参数重载的方法实现复用,写法如下

module MUX8
    #(parameter WIDTH=32) //定义的是时候这样
	(
	output reg [WIDTH - 1:0] out, 
	input [2:0] sel,
	input [WIDTH - 1:0] in0,
	input [WIDTH - 1:0] in1,
	input [WIDTH - 1:0] in2,
	input [WIDTH - 1:0] in3,
	input [WIDTH - 1:0] in4,
	input [WIDTH - 1:0] in5,
	input [WIDTH - 1:0] in6,
	input [WIDTH - 1:0] in7
    );

	always @(*) begin
		case(sel)
			0: out = in0;
			1: out = in1;
			2: out = in2;
			3: out = in3;
			4: out = in4;
			5: out = in5;
			6: out = in6;
			7: out = in7;
		endcase
	end

endmodule

MUX8 #(5) RegAdd3Mux(//这样使用
	.out(RegAdd3),
	.sel(RegAdd3Sel),
	.in0(rt),
	.in1(rd)
	);

写在后面的话

​ 因为P3考的非常惨烈,所以当时我出了考场,真的十分后怕,倒不是说我怕挂P啥的,做到今天,又有几个人没有挂过P,连于哥哥这样的人都挂过,我没挂只能说明老天不开眼。我怕的是我要是挂了,可能永远没有办法知道自己问题是啥。之前还好,不过是一个状态机,一段程序,还是有那么一丝丝可能通过课下反思知道自己错在哪里了。但是对于CPU而言,怎么知道自己有没有问题呢?不知道,很多时候CPU过于复杂没有形式证明来确保完全的正确性。只能通过测试样例来检验对错,但是明明课下好好的,上机就过不了,问题出现在哪里呢?哪怕投入再多的时间可能都发现不了,这是最恐怖的事情。就是这个错误,可能是一个不可能更改的错误。他永远藏在我的代码里,哪怕通过了,他可能还在,这是最恐怖的。

​ 我也不知道为什么要写这篇文章,我为了这篇文章已经花很长时间。我不知道为什么。吴佬在朋友圈分享过一张截图:“我看腻了主角团互帮互助,克服难关的故事,请给我一些主角深受痛苦,而他的伙伴却无能为力,只能冷眼旁观的故事。”吴佬说,这种故事就发生在六系。我很幸运,没有深受痛苦。但是我很不幸,我只能旁观我的朋友深受痛苦。这就是我写这篇文章的激励模块。

鸣谢名单:

  • roife
  • 李松泽
  • 曾凡一

​ 对了,想要代码的话,等我过了P4(那可能好久了),可以私聊(助教哥哥饶我狗命)。因为尽管我已经尽力把设计想法说的清楚一些了。但是感觉确实只放设计图,很多意思就是没法表达清楚,我的表达力太差了。

你可能感兴趣的:(fpga开发)