# CPU 设计文档及思考题
# 设计草稿
# 顶层模块设计
- 在 P3 的基础上,将其顶层结构翻译映射为 Verilog HDL 的工程文件。
- 因此,对 P3 文档稍加修订,得到顶层模块的工程设计如下
- 数据通路
- 控制
指令 NPCMode[1:0] RegWrite ALUOp[3:0] MemoryWrite ExtOp[1:0] ALUSrc[1:0] GRFSrc[1:0] GRFA3Src[1:0] add 00 1 0000 0 xx 00 00 00 sub 00 1 0001 0 xx 00 00 00 ori 00 1 0010 0 00 01 00 01 lw 00 1 0000 0 01 01 01 01 sw 00 0 0000 1 01 01 xx xx beq 01 0 0001 0 xx 00 xx xx lui 00 1 0011 0 xx 01 00 01 jal 10 1 xxxx 0 xx xx 10 10 jr 11 0 xxxx 0 xx xx xx xx nop 00 0 xxxx 0 xx xx xx xx
- 数据通路
# 子模块
程序计数器 PC
信号名 方向 描述 Clk Input 时钟 Reset Input 同步复位到 0x00003000 In Input[31:0] npc Out Output[31:0] pc 下一指令运算器 NPC
信号名 方向 描述 Mode Input[2:0] 运算模式 Zero Input ALU 结果是否为零 pc Input[31:0] PC Imm Input[25:0] 25 位立即数 ra Input[31:0] ra pc4 Output[31:0] PC+4 npc Output[31:0] NPC 指令存储器 IM
信号名 方向 描述 A Input[13:2] 指令的字节地址 - 0x00003000 Instr Output[31:0] 指令 寄存器堆 GRF
信号名 方向 描述 Clk Input 时钟 Reset Input 同步复位 WE Input 写使能 A1[4:0] Input 操作数 1 地址 A2[4:0] Input 操作数 2 地址 A3[4:0] Input 操作数 3 地址 WD[31:0] Input 写入数据 D1[31:0] Output 读出数据 1 D2[31:0] Output 读出数据 2 控制器 Controller
信号名 方向 描述 Opcode[5:0] Input Funct[5:0] Input NPCMode Output[2:0] NPC 模式使能 RegWrite Output GRF 写使能 ALUSrc[1:0] Output B 为寄存器 / 立即数 ALUOp[3:0] Output ALU 模式 MemoryWrite Output DM 写使能 ExtOp[1:0] Output EXT 使能和模式 GRFSrc[1:0] Output ALU/DM 回写 算术逻辑运算单元 ALU
信号名 方向 描述 A[31:0] Input 操作数 1 B[31:0] Input 操作数 2 ALUOp[3:0] Input 模式 C[31:0] Output 运算结果 Zero Output 结果是否为零 数据存储器 DM
信号名 方向 描述 Clk Input 时钟 Reset Input 同步复位 En Input 写使能 A[31:0] Input 读地址 D[31:0] Input 写数据 Data[31:0] Output 读出数据 结合上述模块输入输出和连接关系,做出各个子模块以及顶层架构,下面展示了顶层架构:
module mips(
input clk,
input reset
);
NPC npc(.mode(controller.NPCMode),
.Zero(alu.Zero),
.pc(pc.out),
.imm(im.Instr[25:0]),
.ra(grf.RD1));
PC pc(.clk(clk),
.reset(reset),
.in(npc.npc));
wire [31:0] IM_Addr;
assign IM_Addr = pc.out - 32'h0000_3000;
IM im(.A(IM_Addr[13:2]));
GRF grf(.clk(clk),
.reset(reset),
.WE(controller.RegWrite),
.A1(im.Instr[25:21]),
.A2(im.Instr[20:16]),
.A3(mux_grf_A3.B),
.WD(mux_grf_D.B),
.pc(pc.out));
ALU alu(.A(grf.RD1),
.B(mux_alu_B.B),
.ALUOp(controller.ALUOp));
DM dm(.clk(clk),
.reset(reset),
.WE(controller.MemoryWrite),
.A(alu.C[13:0]),
.D(grf.RD2),
.pc(pc.out));
EXT ext(.imm(im.Instr[15:0]),
.EXTOp(controller.EXTOp));
MUX_5_4 mux_grf_A3(.A0(im.Instr[15:11]),
.A1(im.Instr[20:16]),
.A2(5'h1f),
.chose(controller.GRFA3Src));
MUX_32_4 mux_grf_D(.A0(alu.C),
.A1(dm.Data),
.A2(npc.pc4),
.chose(controller.GRFSrc));
MUX_32_4 mux_alu_B(.A0(grf.RD2),
.A1(ext.ext),
.chose(controller.ALUSrc));
Controller controller(.Opcode(im.Instr[31:26]),
.Funct(im.Instr[5:0]));
endmodule
# 测试方案
- 思路
- 沿用 P3 测试的思路和方法,重用其代码。可以完成 add,sub,ori,lw,sw,lui,nop 等运算和访存指令的测试。
- 下面是 P3 的测试方案:
例如:
#include<stdio.h> #include<stdlib.h> #include<time.h> #define BATCH_SIZE 50 FILE *pCode; int main() { srand((unsigned int)time(0)); pCode = fopen("mips_code.txt","w"); char s[120]; int i; for (i = 0; i< 32; i++) { sprintf(s,"ori $%d,$%d,%d\n",i,i + 1,0); fprintf(pCode,s); } fclose(pCode); pCode = fopen("mips_code_ori1.txt","w"); for (i = 0; i< 32; i++) { sprintf(s,"ori $%d,$%d,%d\n",i,i + 1,0xffff); fprintf(pCode,s); } fclose(pCode); pCode = fopen("mips_code_ori2.txt","w"); for (i = 0 ; i < BATCH_SIZE; i++) { sprintf(s,"ori $%d,$%d,%d\n",rand()% 32,rand()%32,rand()%0x10000); fprintf(pCode,s); } fclose(pCode); return 0; }
- 对于 beq,jal,jr 等命令,手动编写了若干条指令进行测试。
例如:
.data .text jal loop beq $t1,$t2,end ori $t1,$0,114 loop: ori $t1,$0,100 ori $t2,$t1,0 jr $ra beq $0,$0,loop end:
# 思考题
- 思考题 1:
- 在课下实验中该 addr 信号从 ALU 的输出端口 C 来,这是因为访存时候的寻址通常是基址寻址,地址由 ALU 计算得到;但若有其他寻址方式,则还有可能从 EXT,GRF 等地方读取而来。
- 由 ALU 计算得到的地址是字节地址,但 DM 中的数据索引是字地址,因此应该将字节地址除以四得到字地址,即 addr [11:2]。
- 思考题 2:
- 对于记录 “指令对应的控制信号如何取值”, 以其中一条指令 add 为例:
记录每种指令对应信号的方式便于以指令为单位进行代码逻辑的查看,不同指令之间不进行耦合,有利于消除不同指令之间的逻辑冲突,能够减少 bug,也有利于直接添加指令。缺点则是代码数量偏大,每增加一个指令就需要重写全部控制信号,相当于不进行代码的复用,且检查时工作量较大。if(add) begin NPCMode = 3'b000; RegWrite = 1'b1; ALUOp = 4'b0000; MemoryWrite = 0; EXTOp = 2'b00; ALUSrc = 2'b00; GRFA3Src = 2'b00; GRFSrc = 2'b00; end
- 对于记录 “控制信号每种取值所对应的指令” 的编码方式,与搭建电路时采用或门阵列一致,示例代码如下:
这种方式与实际实现的电路有良好的对应关系。代码量明显较少,编写和修改时工作量较小,无需列出控制信号取 0 时的指令。但是不同指令耦合在一起,修复错误和增加指令时更容易出现逻辑问题;并且为了使得代码清晰,将控制信号不同位拆开表示。考虑到已经写清楚控制逻辑真值表理清逻辑,故采用此方式。NPCMode[0] = beq | jr; NPCMode[1] = jal | jr; NPCMode[2] = 0; RegWrite = add | sub | ori | lw | lui | jal; ALUOp[0] = sub | beq | lui; ALUOp[1] = ori | lui; ALUOp[2] = 0; ALUOp[3] = 0; MemoryWrite = sw; EXTOp[0] = lw | sw; EXTOp[1] = 0; ALUSrc[0] = ori | lw | sw | lui; ALUSrc[1] = 0; GRFA3Src[0] = ori | lw | lui; GRFA3Src[1] = jal; GRFSrc[0] = lw; GRFSrc[1] = jal;
- 思考题 3:
- (以上升沿 + 高电平有效为例)异步复位当中复位信号的优先级比时钟信号要高,只要复位信号为高电平,不论时钟信号是什么,立即复位。即
always @(posedge clk,posedge reset) begin if(reset) begin /*reset*/ end /*code*/ end
- 同步复位当中时钟信号的优先级比复位信号要高,当且仅当时钟上升沿时复位信号为高电平才进行复位,将有一个周期无其他操作。即:
always @(posedge clk) begin if(reset) begin /*reset*/ end /*code*/ end
- 思考题 4:
addu 相比于 add 指令,其操作多了
部分,即溢出检查,若忽略溢出,不进行检查,则操作为 else 部分if temp32 ≠ temp31 then SignalException(IntegerOverflow)
和 add 完全相同。因此,忽略溢出时,add 与 addu 是等价的。addi 和 addiu 同理。else GPR[rd] ← temp endif