首先需要强调的是,这是一篇考后反思,所以相比于之前的HDLBit的以力证道,或者MIPS的技之巅峰(不要笑,我的技巧最高水平就那样了)。这篇文章要温吞很多。这篇文章讲的东西是一个单周期CPU的设计思路。正如大家所见,我不聪明,所以其实我写这种需要大量智商的东西是很羞愧的,所以大家就看个热闹就好了。
其实最精华的部分是我的代码,但是因为规定,是没有办法放出来的(其实我挺喜欢这个规定的,这样就好歹给我的智商留了一块遮羞布,不用把代码拿出来让大家检验)。其次精华的是第二、三章,但是实话实话,我自己看着都想睡觉。所以大家就看着个乐呵吧。第一章说的虽然都是我的总结,但是跟比如说岳哥哥,或者是松泽哥哥交流,或者看吴佬代码,或者看好多人完成P4的速度。好像这些东西大家都知道。这也不奇怪,因为比较笨的原因,确实很多比较显而易见的道理,我需要一些时间总结。所以大家就笑笑就好了。
我参加的是第一次P3上机,在考场上做出来了第一道题和第三道题,因为这场考试做的十分艰难(没挂真的是万幸),所以有必要好好反思一下问题都出在了哪里。
我卡的最久的是第一题,第一题是bezal,就是说判断一个寄存器内的值是否为0,如果是,就执行跳转操作和链接操作,如果不是,就相当于一个nop指令。在这道题中,我条件判断只管住了链接操作,也就是说,跳转操作无论条件判断如何,都会执行,所以就错了(应该是倒数第二个点)。所以我这道题做了一个半小时。
然后在第二题,slo,这道题说的是左移shamt位,然后左移的部分补1。这道题我跳过了,是因为我的ALU只有SrcA和SrcB两个输入端口,没有设置shamt的输入端口,所以如果想进行运算,就需要拓展端口,但是在考场上拓展端口,对我的心理压力过大。我最后是以在ALU外部搭建计算通路的方法实现的。
最后在第三题,lwor,就是用or的方式计算地址,然后lw,这没有什么难的,难点在地址的计算,这个计算也是涉及了三个运算数,应该是一个寄存器内的32位数,与一个拓展后的立即数进行或运算,然后再加上一个5位的数乘4。所以还是需要三输入端口ALU,显然我是没有的,所以我又搭建了一条旁路。最后侥幸过关(第二题也是利用了这条通路,但是最后太慌张了,点错按钮了,不过归根结底是考前准备步骤和设计理念不成熟)。
我在做课下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模块,优化控制信号 |
这一章来自吴哥哥的思想,就是对指令进行分类,经过长达四天的实践,我觉得还可以优化为对指令进行分解,因为指令是功能的单位,但是不是功能的最小单位,比如说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) |
我认为一个好的模块,就是用来实现一个功能的。为什么这么说呢?因为其实一个模块在一个周期内只能干一件事,比如尽管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模块和控制信号。
我认为好的控制是控制信号是简洁的,这里可能是我个人的偏好,我不太喜欢教材中的控制信号,因为不能望文生义,而且过于繁杂,看名字都不知道是控制哪一个模块的的控制信号,也不知道一个模块有几个控制信号。
我认为好的控制信号对于每个模块只有三种,决定是否发挥功能的使能信号,决定数据来源的选择信号,决定具体功能的功能信号。使能信号每个模块最多只有一个(在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;
在后面会有详细的设计介绍。
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 |
里面包含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没有控制信号
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 |
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位 |
我的一个重要的设计思想就是分化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 | 或 |
这是我单独组成的一个模块(受到了吴哥哥的启发),但是与吴哥哥不同的是,吴哥哥好像只写了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 | 小于等于零 |
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 | 加载半个字 |
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 |
关于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来决定读出的数据的形式。
加载到寄存器中的值不再是从DM中获得的,而且需要GRFOP的帮助,所以要么是立即数,要么是set类指令。
关于GRF和CU
指令 | RegWrite | RegAdd3Sel | RegInSel | GRFOP |
---|---|---|---|---|
standard | 1 | |||
lui | 1 | rt | EXTOut | LUI |
slt | 1 |
加载到寄存器的值是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。
加载到寄存器的值是下一条指令的地址,即link操作
关于GRF和CU
指令 | RegWrite | RegAdd3Sel | RegInSel | GRFOP |
---|---|---|---|---|
standard | 1 | - | PC | LINK |
jal | 1 | - | PC | LINK |
指的是计算一个寄存器和立即数的类型,包括地址计算和立即数计算
关于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也需要输入一个数,算是神奇利用了)。
指的是需要两个寄存器中内容的运算。因为没有了立即数,所以不再需要EXT模块
关于ALU和CU
指令 | SrcA(默认) | SrcBSel | SHF(默认) | ALUOP |
---|---|---|---|---|
standard | RegOut1 | RegOut2 | - | |
addu | RegOut1 | RegOut2 | - | ADD |
subu | RegOut1 | RegOut2 | - | SUB |
需要修改的控制信号是SrcBSel和ALUOP。
指的是不仅用到了两个寄存器中的内容(也可能是一个寄存器一个立即数,但是常见指令没有,这里就不改EXT模块了),还用到了SHF字段。
关于ALU和CU
指令 | SrcA(默认) | SrcBSel | SHF(默认) | ALUOP |
---|---|---|---|---|
standard | RegOut1 | SHF | ||
sll |
关于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 |
关于CMP和CU
指令 | num1(默认) | num2(默认) | CMPOP |
---|---|---|---|
standard | RegOut1 | RegOut2 | |
beq | RegOut1 | RegOut2 | EQ |
bgtz | RegOut1 | RegOut2 | |
slt | RegOut1 | RegOut2 |
这里的Branch操作包括beq或者jr或者j产生的跳转,简而言之就是所有使nextPC不指向紧挨着当前PC + 4的操作。
关于NPC和CU
指令 | BranchSel | NPCOP |
---|---|---|
standard | ||
beq | EXTOut | BRANCH/NORMAL |
bgtz | ||
jal | EXTOut | J |
jr | RegOut1 | JR |
关于EXT和CU
指令 | EXTOP |
---|---|
standard | |
lw | SE16 |
lb | SE16 |
lh | |
sw | SE16 |
sb | |
sh | |
ori | ZE16 |
beq | SE16 |
lui | ZE16 |
jal | ZE26 |
到底是用宏还是用参数parameter,我个人喜欢parameter,是因为如果用宏的话,各个模块可能有相似的信号,比如CU需要一个代表beq的宏,NPC判断的时候也需要一个代表beq的宏,为了区别这俩,可能就需要加前缀了(吴哥哥就是这么做的)。但是如果是像我这样记忆力比较差的,对于加了前缀的命名就很头痛,所以不如每个模块里进行parameter,这样更加简洁。但是需要注意的是,前缀还是没有办法避免的,尽管在功能模块里可以避免前缀(因为不会冲突),但是在CU里,每个信号都需要被定义,那么就是还是需要前缀的,所以其实两种方法都可以。用宏只用定义一遍,但是打字比较难,代码我觉得比较丑。用参数代码比较好看,但是需要定义两遍。
但是其实我还是觉得,我的更好一些,是因为如果用一个头文件对其进行宏定义,那么定义的宏相当于是全局可见了,而用模块内的参数定义,参数的可见范围仅限于模块内,所以封装性更好一些。
这个其实很多人都在用哈,就是因为连接的端口太多了,所以竖着会好看一些,但是只有少部分人用名称确定端口位置,因为毕竟是自己代码,写的时候都能记住每个位置应该输入哪个信号。但是因为我太笨了,所以我担心我考场上忘了位置,再两个文件之间回看,所以还是喜欢名称连接法,尤其是在MUX类有奇用。示例如下:
MUX8 BranchMux(
.out(branch),
.sel(BranchSel),
.in0(RegOut1),
.in1(EXTOut)
);
这个我自己做的也不是太好,因为确实P3第一次搭CPU,所以抄了很多教材,教材怎么命名,我就怎么命名,然后课件上就有一些命名,然后课下有的时候也对端口命名提出了要求,然后我也不能像之前那样记住所有东西。最后反正搞来搞去,命名就一直很糟糕,哪怕这次重写P4,命名也没有统一。我认为一套好的命名规范,可以让我只要想到这个功能或者需求,就能想起所需要的信号或者端口的名字。这其实不止是命名本身的问题,其实还有设计的问题。就好比如果一家一脉单传,就很好区分他是第几代人,但是如果近亲结婚,就很难再论辈分。简洁的命名其实是跟简洁的设计相关的。
就我个人而言,我在这里提出了一套规范,适合我自己用,也在上面的设计中有所体现。
事物 | 命名 |
---|---|
使能信号 | 对象 + “Write” |
功能信号 | 模块名 + “OP” |
选择信号 | 端口名 + “Sel” |
输出端口 | 模块名 + “Out” |
数据输入端口 | 模块名 + “In” |
地址输入端口 | 模块名 + “Add” + 编号 |
所谓的三目运算符就是这样:
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
感觉会好看一些。
这个吴佬就是牛逼哈,没啥说的,好像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
这个主要是万老师上课的时候,提到可以把不同数据位的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过于复杂没有形式证明来确保完全的正确性。只能通过测试样例来检验对错,但是明明课下好好的,上机就过不了,问题出现在哪里呢?哪怕投入再多的时间可能都发现不了,这是最恐怖的事情。就是这个错误,可能是一个不可能更改的错误。他永远藏在我的代码里,哪怕通过了,他可能还在,这是最恐怖的。
我也不知道为什么要写这篇文章,我为了这篇文章已经花很长时间。我不知道为什么。吴佬在朋友圈分享过一张截图:“我看腻了主角团互帮互助,克服难关的故事,请给我一些主角深受痛苦,而他的伙伴却无能为力,只能冷眼旁观的故事。”吴佬说,这种故事就发生在六系。我很幸运,没有深受痛苦。但是我很不幸,我只能旁观我的朋友深受痛苦。这就是我写这篇文章的激励模块。
鸣谢名单:
对了,想要代码的话,等我过了P4(那可能好久了),可以私聊(助教哥哥饶我狗命)。因为尽管我已经尽力把设计想法说的清楚一些了。但是感觉确实只放设计图,很多意思就是没法表达清楚,我的表达力太差了。