计组P4课下作业总结
部件建模
PC.v
端口 | 方向 | 功能 |
---|---|---|
CLK | I | 处理时钟 |
Reset | I | 复位信号(同步复位) |
DI[31:0] | I | 32位输入下一条指令地址 |
DO[31:0] | O | 32位输出本条指令地址 |
注意P4和P3要求的差别:首先Reset信号变成了同步复位,则不需要放入敏感表,同时初始及复位时默认PC指向0x00003000,用来配合MIPS的.text位置设定。
代码:
1 |
|
NPC.v
端口 | 方向 | 功能 |
---|---|---|
PC[31:0] | I | 输入当前指令的地址 |
NPCOp[1:0] | I | NPC计算方法控制: 00:PC+4 01:beq 10:jal 11:jr |
Zero | I | 判断beq指令中rs和rt是否相等的比较结果 |
Imm26[25:0] | I | 26位立即数(beq,jal跳转指令地址) |
RA[31:0] | I | rs寄存器保存的32位返回地址 |
NPC[31:0] | O | 32位输出(下一条指令的地址) |
PC4[31:0] | O | 32位输出PC+4,便于存入rs寄存器 |
P4需要新加入jal和jr指令,所以说对比P3,其功能选择控制器NPCOp需要输入两位来判断四种地址跳转方式。其次是我们根据这两条指令的描述:
jal:PC<-PC[31,28]║instr_index║0^2 R[31]<-PC+4 因此我们需要将imm扩展为26位用来存放jal的index,同时由于要把PC+4存入31号寄存器,因此需要输出PC+4的值来存入,因此需要新添加PC4输出。
jr:PC<-R[rs] 因此我们需要将rs的值再通过PC端口输出,所以只需要设定NPCOp值即可。
此处要注意:由于Beq的判断有两个条件(即npcop和zero),则针对npcop==1但zero==0的情况,我们需要让pc+4,不能让他进入异常状态!
代码:
1 |
|
IM.v
端口 | 方向 | 功能 |
---|---|---|
PC[31:0] | I | 输入当前指令的地址 |
Instr[31:0] | O | 输出当前指令内容 |
和P3一样,将指令地址输入IM,IM将输出当前指令机器码Instr。这里要使用$readmemh指令读取code.txt中的机器码到设定的寄存器阵列,模拟rom的功能。
由于本题IM要求的容量是4096*32bit,则取地址应该取PC的12位(4096=2^12),则取[13:2]位。(和P3一致,PC地址偏移4位相当于这里取寄存器阵列的下一个寄存器,因此相当于要除2)
还有要注意,code的位置是从0x00003000开始的!所以有两个选择,可以在readmemh中设定导入数据就从0x00003000处开始,但是这样比较浪费空间,所以我选择将PC-0x00003000作为真正的地址寻找机器码。
代码:
1 |
|
GRF.v
端口 | 方向 | 功能 |
---|---|---|
CLK | I | 处理时钟 |
Reset | I | 复位信号(同步复位) |
A1[4:0] | I | 读取的第一个寄存器编号 |
A2[4:0] | I | 读取的第二个寄存器编号 |
A3[4:0] | I | 写入的寄存器编号 |
WD[31:0] | I | 写入值 |
PC[31:0] | I | 当前执行指令的地址 |
Wr | I | 写使能,判断是否能够写入 |
RD1[31:0] | O | 读取的第一个寄存器值 |
RD2[31:0] | O | 读取的第二个寄存器值 |
还是要注意0号寄存器不能被写入。此处依然利用reg阵列模拟寄存器。以及P4中要求Reset信号要实现的是同步复位。
同时要注意本题有特殊要求:在 GRF 模块中,每个时钟上升沿到来时若要写入数据(即写使能信号为 1 且非 reset 时)则输出写入的位置及写入的值,格式(请注意空格)为:$display("@%h: $%d <= %h", WPC, Waddr, WData);
其中 WPC 表示相应指令的储存地址,从 0x00003000 开始;Waddr 表示输入的 5 位写寄存器的地址;WData 表示输入的 32 位写入寄存器的值。
因此为了输出WPC,我们还需要把当前执行指令地址PC输入才能输出。
此处为了初始化和清空存储指令空间,我定义了一个i,并用它循环来清空所有寄存器。
代码:
1 |
|
ALU.v
端口 | 方向 | 功能 |
---|---|---|
A[31:0] | I | 第一个读入数RS |
B[31:0] | I | 第二个读入数RT或立即数 |
MSel[1:0] | I | 判断要进行什么运算(控制信号) |
C[31:0] | O | ALU运算结果输出 |
Zero | O | 相等判断结果(==) |
和P3一样,还是要在四种情况中进行选择,分别是加法,减法,与运算和直接输出B,所以需要一个select bits为2的多路选择器进行选择,因此控制信号MSel需要设定为两位。
此处MSel设定还是00为加,01为减,10为or,11为输出B。
代码:
1 |
|
DM.v
端口 | 方向 | 功能 |
---|---|---|
CLK | I | 处理时钟 |
Reset | I | 复位信号(同步复位) |
A[31:0] | I | 地址输入 |
WD[31:0] | I | 需要被写入地址A的数据 |
PC[31:0] | I | 本步骤对应指令的地址 |
Wr | I | 控制是读功能还是写功能的控制信号,0读1写 |
RD[31:0] | O | 将A地址中的数据从RD读出 |
本题的RAM同样需要自己构造,其大小为3072*32bit,此处和IM一致,由于3072>2^11,则需要12位地址进行存入/取出数据地址计算。所以存取地址都要截取A的[13:2]位。
P4对DM也有额外的显示要求:在 DM 模块中,每个时钟上升沿到来时若要写入数据(即写使能信号为 1 且非 reset 时)则输出写入的位置及写入的值,格式(请注意空格)为:$display("@%h: *%h <= %h", pc, addr, din);
其中 pc 是该操作对应的指令的地址,和 GRF 模块要求一致,addr 表示将要存入数据的 32 位地址,din 表示输入的 32 位写入 DM 的值。
所以也和GRF同理,我们需要额外在模块中输入PC用来显示。
代码:
1 |
|
EXT.v
端口 | 方向 | 功能 |
---|---|---|
Input[15:0] | I | 需要被扩展的16位输入 |
EXTOp[1:0] | I | 扩展单元控制信号 |
EXT[31:0] | O | 输出32位的扩展结果 |
本题和P3一样,同样有三种扩展方式,即符号扩展,0扩展和lui扩展(高16位加载扩展),所以需要用两位的控制信号EXTOp进行,设定0为0扩展,1为符号扩展,2为lui扩展。在Verilog中直接进行拼接就可以完成扩展操作。
代码:
1 |
|
Control.v
端口 | 方向 | 功能 |
---|---|---|
opcode[5:0] | I | 机器码中六位opcode输入 |
funct[5:0] | I | 机器码中六位funct输入 |
NPCOp[1:0] | O | NPC:0 → +4; 1 → branch; 2 → jal; 3 → jr |
WRSel[1:0] | O | A3:0 → “rt”; 1 → “rd”; 2 → reg31 |
WDSel[1:0] | O | RF-WD:0 → ALU; 1 → Mem; 2 → NPC |
RFWr | O | RF:1 → write register |
EXTOp[1:0] | O | 见ext |
BSel | O | ALU-B:0 → RF-RD2; 1 → imm16 |
MSel[1:0] | O | 见alu |
DMWr | O | DM-Wr: 1 → write memory |
P4中由于是在Verilog中实现,因此不需要像P3中分为and和or两个模块(我认为P3我分开的一个很大的原因就是可以通过连线的亮暗判断正在进行什么指令),但是思路还是一样的:通过opcode和funct输入分成不同的指令,然后再针对不同的指令判断对应控制信号的取值。
同时注意本题由于需要实现jal和jr,所以NPCOp,WRSel(NPC需要把pc+4写入31号寄存器),WDSel(NPC指令需要PC+4的值)需要有两位,然后EXTOp和MSel本来也需要有两位。
此处我把不同指令的funct和opcode的值列举于下:
指令 | 机器码 |
---|---|
add | opcode:000000,funct:100000 |
sub | opcode:000000,funct:100010 |
ori | opcode:001101 |
lw | opcode:100011 |
sw | opcode:101011 |
lui | opcode:001111 |
beq | opcode:000100 |
jal | opcode:000011 |
jr | opcode:000000,funct:001000 |
nop | opcode:000000,funct:000000 |
以及不同指令针对不同控制信号的取值:
指令 | NPCOp | WRSel | WDSel | RFWr | EXTOp | BSel | MSel | DMWr |
---|---|---|---|---|---|---|---|---|
nop | 00 | 00 | 00 | 0 | 00 | 0 | 00 | 0 |
add | 00 | 01 | 00 | 1 | 00 | 0 | 00 | 0 |
sub | 00 | 01 | 00 | 1 | 00 | 0 | 01 | 0 |
ori | 00 | 00 | 00 | 1 | 00 | 1 | 10 | 0 |
lw | 00 | 00 | 01 | 1 | 01 | 1 | 00 | 0 |
sw | 00 | 00 | 00 | 0 | 01 | 1 | 00 | 1 |
lui | 00 | 00 | 00 | 1 | 10 | 1 | 11 | 0 |
beq | 01 | 00 | 00 | 0 | 00 | 0 | 00 | 0 |
jal | 10 | 10 | 10 | 1 | 00 | 0 | 00 | 0 |
jr | 11 | 00 | 00 | 0 | 00 | 0 | 00 | 0 |
至于真正的代码就是对每一个控制信号进行三目运算符判断即可,比较简单机械,这里不赘述。
main.v
需要使用这些模块并进行接线,我们可以先参考P3的接线将没有jal/jr的接线情况完成。图如下(注意P3中我们先将pc,npc及im合成了ifu,这里需要分别接线):
在Verilog中,接线的东西特别混乱,我想到了几个稍微清洁一点的方法:
- 对每根线的命名方式都是模块_出口名__模块_入口名,这样可以比较直观看出这根线是干嘛的。
- 对于同一个出口要接到不同入口的情况我宁可多定义一根线,这样看上去比较清晰,而且可以遵守我上面的命名方法。
- 将模块的使用放在一起,模块之间的连线情况放在一起,看上去比较清楚。
按照这些写出来大概是这样(好像还是有点乱):
首先是模块的使用:
1 |
|
然后是连线情况:
1 |
|
思考题
DM的addr由ALU计算所得,取[11:2]的原因是:因为我们定义DM中的寄存器阵列时方法是[31:0]RAM[0:3071],则我们要选择寄存器的地址就应该以四个字节,即一个寄存器为一位。但是ALU给出的地址是以一个字节为一位,因此为了对齐,就需要给ALU给出的地址除二,从第三位开始取。
指令对应的控制信号如何取值:
1 |
|
控制信号每种取值所对应的指令:
1 |
|
我觉得第一种比较直观,但是写起来也比较繁复,因为每一条指令都需要对其进行赋值,第二条比较简洁,但是不太容易加入新指令,因为看起来比较不直观。
异步复位的reset信号优先级是最高的,而同步复位clk信号优先级最高。
比较addi和addiu:
addi的描述:
temp ← (GPR[rs]31||GPR[rs]) + sign_extend(immediate)
if temp32 ≠ temp31 then
SignalException(IntegerOverflow)
else
GPR[rt] ← temp31..0
endif
addiu的描述:
GPR[rt] ← GPR[rs] + sign_extend(immediate)
可以发现在不考虑溢出时temp32==temp31,且temp=gpr[rs]+sign_extend(immediate),此时将GPR[rt] ← temp31..0,和addiu的描述完全等价。add和addu同理。
以上是P4的内容,我感觉还是比较有趣,希望上机能通过。但是P5看了两眼,确实离谱ORZ