RooKie_Z P4 Verilog单周期CPU设计文档
写在前面
这是RooKie_Z的P4单周期CPU设计文档,在课上测试中本CPU取得了 满分💯的成绩。
总体设计概述
本次要求实现的指令集为 add, sub, ori, lw, sw, beq, lui, jal, jr, nop
,要求与P3相似,考虑到我P3已经实现了这些指令,并且经过课上测试,感觉CPU总体架构与模块化较为清晰完备,故本次暂不重构,直接翻译Logisim电路。
同样的,与P3类似本次依然做好了实现 各类branch,jump,link指令和诸如lb,lbu,lh,lhu,sb,sh等指令的工作
,以备不时之需,但相较Logisim,显然Verilog里面的指令可以更加花哨,这时就比较考验写组合逻辑电路的手法了。
整体架构仍与与肖利民老师课件中的图类似
用ISE完成后整体文件树形图如下
仿真模板如下
关键模块介绍
命名规则
- 在顶层模块
mips.v
,内,所有实例化元件的命名均为大写字母开头的模块名,如Ifu, Npc, Alu
等。 wire
类电线命名仍采用小驼峰法与下划线结合的方式,力求可读性强,便于扩展,分为几类讨论,如在顶层模块mips.v
内采用小驼峰命名法,在不产生重复误会的前提下,力求wire名称与实例化后的模块端口名称一致,而在对于易混信号,则采用类似grf_rd1
,RD1_rs
,这样的命名法加以区别,在CTRL
内,对于判断具体指令,则采用指令相同的命名,如add,sub……
。- 控制信号如果是控制元件的操作类型以
Op
结尾,如ALUOp
,DMOp
等;如果是多路选择器的控制信号,则以Sel
结尾,如A3Sel
,WDSel
等; - 端口命名与P3相同
- 控制信号的宏定义命名为元件名+下划线+功能,如
ALU_add
,NPC_jal
,DM_w
,DM_h
等 opcode
和funct
信号的宏命名为OP
/FUNC
+下划线+小写指令名,如OP_beq
,FUNC_add
宏定义介绍
“工欲善其事必先利其器”,好的宏定义是规范CPU搭建的前提,否则指令一多,程序可读性便急剧降低。
因此,本次搭建CPU时特意建立了一个 signal_def.v
的宏定义文件,来统筹管理各个信号定义,具体分为一下几部分
- OP_Code and Func_Code,管理各指令的指令编码,便于识别;
- GRF signals,管理有关GRF的控制信号,包括
WDSel
和A3Sel
两块; - DM signals,管理DM读写方式的信号,主要供
DMOp
使用; - EXTOp, 管理扩展方式;
- NPCOp,管理NPC生成方式;
- CMPOp, 管理CMP的比较方式;
- ALUSrcB,管理B选择数据来源;
- ALUOp,管理ALU的计算方式;
至此,宏定义部分结束,课上添加指令时一定要看好并在signal_def.v
的文件里进行增添!
Warning:define后不要加分号!!!define生成的宏使用时要加 “`”!!!
综合考量了课上进行功能拓展的方便程度和适宜Verilog语法的需求,我对于P3的CPU中的模块进行了重新排列组合,并且参考当时CPU中BranchDefine子电路的思想,新增了CMP模块,来控制分支信号。
GRF
直接翻译P3电路
端口说明
端口名称 | 方向 | 描述 |
---|---|---|
A1[4:0] | I | 5位地址信号,将其对应寄存器中的值输出到RD1 |
A2[4:0] | I | 5位地址信号,将其对应寄存器中的值输出到RD2 |
A3[4:0] | I | 5位地址信号,将WD输入的值写入其对应的寄存器中 |
WD[31:0] | I | 32位输入信号,将值写入A3对应的寄存器中 |
WE | I | 1位写使能信号,高电平写入,低电平无事发生 |
RD1[31:0] | O | 32位输出值,输出A1对应寄存器中储存的值 |
RD2[31:0] | O | 32位输出值,输出A2对应寄存器中储存的值 |
clk | I | 时钟信号 |
reset | I | 同步复位信号,高电平将所有寄存器复位到0,低电平无事发生 |
相关控制信号说明
1、A3Sel
控制信号值 | 功能 |
---|---|
A3Sel_rd |
选择待写入寄存器地址来自Instr[15:11] (R Type) |
A3Sel_rt |
选择待写入寄存器地址来自Instr[20:16] (I Type) |
A3Sel_ra |
选择写入寄存器的地址为31($ra ) (含link的指令) |
2、WDSel
控制信号值 | 功能 |
---|---|
WDSel_DMout |
选择写入寄存器的数据来自DM (lw sw 等指令) |
WDSel_ALUout |
选择写入寄存器的数据来自ALU运算结果 |
WDSel_PC4 |
选择写入寄存器的数据为PC+4 (含link的指令存$ra 的值) |
EXT
将16位二进制数进行零扩展或符号扩展到32位
控制信号说明
控制信号值 | 功能 |
---|---|
EXT_unsigned |
零扩展 |
EXT_signed |
符号扩展 |
IFU(指令读取单元)
与P3不同,考虑到NPC需要便于维护,改为将IM与PC合在一起组成IFU
端口说明
端口名称 | 方向 | 描述 |
---|---|---|
NPC[31:0] | I | 待写入PC的指令地址 |
clk | I | 时钟信号 |
reset | I | 同步复位信号,高电平复位PC到32’h0000_3000 |
PC | O | 当前指令地址 |
Instr[31:0] | O | 32位的指令值 |
rs[4:0] | O | $rs地址 |
rt[4:0] | O | $rt地址 |
rd[4:0] | O | $rd地址 |
imm16[15:0] | O | 16位立即数 |
imm[31:0] | O | 32位立即数,实际上取26位 |
func[5:0] | O | 6位func |
opcode[5:0] | O | 6位opcode |
shamt[4:0] | O | 5位位移量,用于sll,srl 等位移指令 |
ALU
端口说明
端口名称 | 方向 | 功能描述 |
---|---|---|
A[31:0] | I | 32位输入运算数A |
B[31:0] | I | 32位输入运算数B |
shamt[4:0] | I | 移位指令所移位数 |
ALUOp[4:0] | O | 控制信号 |
ALUout[31:0] | O | 32位输出运算结果 |
控制信号说明
1. ALUOp
控制信号值 | 功能 |
---|---|
ALU_add |
执行加法运算 |
ALU_sub |
执行减法运算 |
ALU_or |
执行逻辑或运算 |
ALU_lui |
执行lui 指令 |
2. ALUBSel
控制信号值 | 功能 |
---|---|
SrcB_rt |
选择寄存器中的值进行运算 |
SrcB_imm |
选择立即数进行运算 |
CMP(分支比较)
参考P3中CPU内BranchDefine子电路生成的CMP模块,来生成jump信号,判断分支是否跳转,link是否写入
目前实现了beq, bne, bgtz, bltz
等指令
代码实现:
1 |
|
端口说明
端口名称 | 方向 | 功能描述 |
---|---|---|
rs[31:0] | I | $rs 寄存器的值 |
rt[31:0] | I | $rt 寄存器的值 |
CMPOp[2:0] | I | 控制信号 |
jump | O | 指示分支是否跳转 |
NPC(次地址计算)
有了CMP之后,NPC的功能也更加简洁,只需根据NPCOp和jump信号输出NPC信号的值就行
端口说明
端口名称 | 方向 | 功能描述 |
---|---|---|
PC[31:0] | I | 32位输入当前地址 |
imm[31:0] | I | 32位立即数 |
jump | I | 指示b类型指令是否跳转 |
NPCOp[2:0] | I | 控制信号 |
RD1_rs[31:0] | I | $ra 寄存器保存的32位地址 |
NPC[31:0] | O | 32位输出次地址 |
PC4[31:0] | O | 输出$PC+4$的值 |
控制信号说明
控制信号值 | 功能 |
---|---|
NPC_PC4 |
$NPC = PC + 4$ |
NPC_branch |
执行branch 指令 |
NPC_jal |
执行j ,jal 指令 |
NPC_jalr |
执行jalr ,jr 指令 |
DM(数据储存器)
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
Addr[31:0] | I | 待操作的内存地址 |
WD[31:0] | I | 待写入内存的值 |
clk | I | 时钟信号 |
reset | I | 同步复位信号 |
DMWr | I | 写使能信号;1:写入有效;0:写入无效 |
DMOp[2:0] | I | 控制信号 |
DMout[31:0] | O | 输入地址指向的内存中储存的值 |
控制信号说明
控制信号值 | 功能 |
---|---|
DM_w |
对应lw 和sw 指令,写入或读取整个字 |
DM_h |
对应lh 和sh 指令,写入或读取半字 |
DM_b |
对应lb 和sb 指令,写入或读取整个字 |
DM_hu |
对应lhu 指令 |
DM_bu |
对应lbu 指令 |
数据通路分析
指令 | opcode | funct | NPCOp | A3Sel | WDSel | EXTOp | GRFWE | ALUSRCB | ALUOp | DMWr | DMOp |
---|---|---|---|---|---|---|---|---|---|---|---|
add | 000000 | 100000 | NPC_PC4 |
A3Sel_rd |
WDSel_ALUout |
X | 1 | SrcB_rt |
ALU_add |
0 | X |
sub | 000000 | 100010 | NPC_PC4 |
A3Sel_rd |
WDSel_ALUout |
X | 1 | SrcB_rt |
ALU_sub |
0 | X |
ori | 001101 | X | NPC_PC4 |
A3Sel_rt |
WDSel_ALUout |
EXT_unsigned |
1 | SrcB_imm |
ALU_or |
0 | X |
lw | 100011 | X | NPC_PC4 |
A3Sel_rt |
WDSel_DMout |
EXT_signed |
1 | SrcB_imm |
ALU_add |
0 | DM_w |
sw | 101011 | X | NPC_PC4 |
X | WDSel_DMout |
EXT_signed |
0 | SrcB_imm |
ALU_add |
1 | DM_w |
beq | 000100 | X | NPC_branch |
X | X | X | 0 | X | X | 0 | X |
lui | 001111 | X | NPC_PC4 |
A3Sel_rt |
WDSel_ALUout |
X | 1 | SrcB_imm |
ALU_lui |
0 | X |
jal | 000011 | X | NPC_jal |
A3Sel_ra |
WDSel_PC4 |
X | 1 | X | X | 0 | X |
jr | 000000 | 001000 | NPC_jalr |
X | X | X | 0 | X | X | X | X |
调试过程记录
Warnings
- 首先是要看清楚RTL语言,这是最重要的。比如说最经典的就是
beq
指令后面的offset这个偏移量实际上要左移再加上$PC + 4$; - 再就是要区分RTL语言中的符号,比如说注意区分
rt
和$ rt
,前者代表rt
的地址,后者代表GPR($rt)
(即$rt
对应寄存器的值)
测试方案
通过弱测后,采用C++程序随机生成较强数据的方法进行测试
1 |
|
其中一组较强数据:
1 | ori $29,$0,0x22fc |
魔改MARS输出:
1 | @00003000: $29 <= 000022fc |
Isim仿真输出
1 | @00003000: $29 <= 000022fc |
完全一致,测试通过!
Warnings
易错点:
1.非阻塞赋值与display的内容不相符
评测机是通过看我们display的东西和它需要的一样不一样来评判我们是否正确的。如果采用非阻塞赋值,紧接着来一句display的话,由于非阻塞赋值是在过程块结束时才统一赋值的,所以输出的东西是未修改的。
2.位宽
对于0x0000_3000,它的位宽是32位,所以二进制写法是32’b00000000000000000011000000000000,省事写16进制的话,是32’h0000_3000,这里的32指的是位宽,而不是这个数在某种进制下有几位!
3.jr跳转
jr不只是可以跳31号寄存器存的地址,任何寄存器存储的地址它都可以跳!这是室友P4课上遭遇的车祸现场,课下弱测并没有测试出来! 请务必检查jr是否写对了!
4.还是display
需要输出32位宽的内存地址,不是【13:2】(12位)地址,请大家看好自己的输出!和教程中提供的输出比对一下,应该就会发现。
5.Reset常见误区
首先是同步复位,这个老生常谈了。之后就是reset的地方够不够,对不对,我们需要reset的地方是程序计数器PC,寄存器堆GRF,数据存储器DM,除此之外,如果发现自己在其他地方也使用了reset,就得好好斟酌一下,这里到底需不需要reset?比如,我发现自己在IM中加了一个reset,本地测试过了,提交没输出,为何?经过了一天的思考和从各方获取经验,我隐约意识到可能是复位出了问题,IM里面为什么需要reset?是防止PC值没有变成0x0000_3000吗?我最开始的确是这么想的,所以在里面加了reset,但是,后来我发现,我reset的并不是输入进来的PC,而是,指令存储空间!!也就是说,考虑到评测机最开始上来就reset一下,我读入的code直接没了,当然之后没有输出。为何本地没有测出来问题?因为我没有检查reset,只是保持reset=0跑的程序。这个点卡了我一天多,也卡了评论区不少同学好长时间。还有类似的问题如输出比正常的慢一周期的,也请看看reset。这样,关于reset的坑差不多就介绍完了。
新增指令要点:
注意条件分支带link的指令需改动的点,signal_def中添加OP,CMP_OP,在CMP中分析跳转模式,增加相应case分支。重点:CTRL内的信号添加。首先是添加相应导线,再在NPCOp内添加相应的
| 导线名
,WDsel的部分添加相应的| 导线名
,A3sel的部分也添加相应的| 导线名
,最后在于关键的WE信号,因为我采用了取反的方式,所以直接在括号内添加相应的指令模块,如对于bgezal指令
,WE可写为assign GRFWE = !(sw | beq | j | sh | sb | jr | (bgezal & !jump));
,重点就在添加(bgezal & !jump)
部分!!! 谨记!!!对于复杂计算,则观察计算方法,由于ALU内部采用了case语句的模式,故可以考虑另写functional来保证计算方式合理添加,具体寄存器选择信号则观察指令相似处,与相同模式指令同处加
| 导线名
即可!!!
思考题
阅读下面给出的 DM 的输入示例中(示例 DM 容量为 4KB,即 32bit × 1024字),根据你的理解回答,这个 addr 信号又是从哪里来的?地址信号 addr 位数为什么是 [11:2] 而不是 [9:0] ?
MIPS中以字节为单位,而在我们设计的DM中,每一个
reg[31:0]
为一个单位。Addr来自ALU的输出端口,代表要读取的DM存储器的地址,在DM设计中应当取[11:0],又因为按字节寻址,因此除以4取 [11:2];
思考上述两种控制器设计的译码方式,给出代码示例,并尝试对比各方式的优劣。
指令对应控制信号取值:
1
2
3
4
5
6
7
8
9
10
11
12
case(Instr[31:26])
`R: begin
case(Instr[5:0])
6'b100000:add RegDst=2'b01; Regwrite=1'b1; ALUsrc=2'b00; ALUctrl=3‘b000; Memwrite=0; MemtoReg=2'b00;
6'b100010:sub RegDst=2'b01; Regwrite=1'b1; ALUsrc=2'b00; ALUctrl=3‘b001; Memwrite=0; MemtoReg=2'b00;
……
endcase
end `
`lw: ……
endcase这种译码方式,对于信号的控制不容易遗漏,对于每一个信号都需要给一个值,清晰易读,比较直观,但是添加指令比较复杂,需要给出完整的控制信号,对于不需要的信号也需要给定默认值,这样也会导致控制部分的代码比较长。
控制信号每种取值所对应的指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18wire add=(Instr[31:26]==6'b000000 && Instr[5:0] == 6'b100000);
wire sub=(Instr[31:26]==6'b000000 && Instr[5:0] == 6'b100010); ……
assign RegDst[1]=1'b0|jal; assign RegDst[0]=1'b0|add|sub;
assign Regwrite=1'b0|add|sub|ori|lw|lui|jal;
assign EXTop=1'b0|lw|sw; assign ALUsrc[1]=1'b0;
assign ALUsrc[0]=1'b0|ori|lw|sw|lui; assign ALUctrl[2]=1'b0;
assign ALUctrl[1]=1'b0|ori;
assign ALUctrl[0]=1'b0|sub;
assign Memwrite=1'b0|sw;
assign MemtoReg[1]=1'b0|lui|jal;
assign MemtoReg[0]=1'b0|lw|lui;
assign NPCop[1]=1'b0|beq|jr;
assign NPCop[0]=1'b0|jal|jr|j;
assign Branchop[2]=1'b0;
assign Branchop[1]=1'b0;
assign Branchop[0]=1'b0;
assign DMop[1]=1'b0;
assign DMop[0]=1'b0;这种译码方式,是对控制信号用了或指令的方式,如果满足这条指令,就会使的控制信号有效,这种方式的优点在于可以很容易的添加指令,对于指令只需要在相应控制信号之后或上一个即可,但是缺点是不够直观,可能会造成漏加信号的错误。
经过P3的拷打,我认为还是第二种比较好。在相应的部件中,复位信号的设计都是同步复位,这与 P3 中的设计要求不同。请对比同步复位与异步复位这两种方式的 reset 信号与 clk 信号优先级的关系。
在同步复位中,clk的优先级是高于reset的,只有在时钟上升沿到来时reset信号有效才进行复位,单有reset信号而时钟上升沿信号没有到来不能进行复位。
在异步复位中,reset优先级高于clk,不论是否是时钟上升沿来临,只要reset信号高电平就清零。
C 语言是一种弱类型程序设计语言。C 语言中不对计算结果溢出进行处理,这意味着 C 语言要求程序员必须很清楚计算结果是否会导致溢出。因此,如果仅仅支持 C 语言,MIPS 指令的所有计算指令均可以忽略溢出。 请说明为什么在忽略溢出的前提下,addi 与 addiu 是等价的,add 与 addu 是等价的。提示:阅读《MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set》中相关指令的 Operation 部分。
根据RTL语言描述:
addi
与addiu
的区别在于当出现溢出时,addiu
忽略溢出,并将溢出的最高位舍弃;addi
会报告SignalException(IntegerOverflow)
故忽略溢出,二者等价。
RooKie_Z P4 Verilog单周期CPU设计文档