# P4 课下项目:单周期 CPU 设计 - Verilog 实现
本文仅仅是 P4 课下 CPU 设计实验报告添加了部分内容
通过阅读或许可以给您完成 P4 课下任务提供些许帮助
课下要求可能会发生变化,下面的电路仅供参考,未必完全正确
# 总体设计概述
要求实现的指令集包括 addu,subu,ori,lw,sw,beq, jal,jr,lui,nop
利用 Verilog
来实现时,参考了 P3 的 Logisim 电路设计,也做了一些改变
整体架构与高小鹏老师课件中的图类似
文件的树形结构层次如下
# 关键模块定义
变量名的命名遵从以下约定:
- 元件的命名为下划线开头的小写字母单词,例如
_grf
,_alu
等 - 电线
wire
的命名为以下划线分隔的小写字母单词,例如 ALU 的 32 位输出命名为alu_ans
,GRF 的两个数据输出命名为grf_rd1
和grf_rd2
,判断是否是addu
指令的电线命名为addu
- 控制信号如果关于元件的操作以
Op
结尾,如ALUOp
,DMOp
等;如果是多路选择器的控制信号,则以Sel
结尾,如GRFA3Sel
,GRFWDSel
和ALUBSel
等;如果是写使能信号则以WrEn
结尾,如GRFWrEn
,DMWrEn
等 - 端口命名与 P3 相同
- 控制信号的宏定义命名为元件名 + 下划线 + 功能,如
ALU_add
,NPC_jal_j
,DM_w
,DM_hu
等 opcode
和funct
信号的宏命名为op
/fun
+ 下划线 + 大写指令名,如op_ADDU
,fun_ADDU
- 所有的宏定义均包含在
def.v
文件内
# GRF(寄存器堆)
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
A1[4:0] | 输入 | 5 位地址输入信号,将其储存的数据读出到 RD1 |
A2[4:0] | 输入 | 5 位地址输入信号,将其储存的数据读出到 RD2 |
A3[4:0] | 输入 | 5 位地址输入信号,将其作为写入数据的目标寄存器 |
RD1[31:0] | 输出 | 输出 A1 指定的寄存器中的 32 位数据 |
RD2[31:0] | 输出 | 输出 A2 指定的寄存器中的 32 位数据 |
WD[31:0] | 输入 | 32 位数据输入信号 |
GRFWrEn | 输入 | 写使能信号;1:写入有效;0:写入无效 |
clk | 输入 | 时钟信号 |
reset | 输入 | 异步复位信号,将 32 个寄存器中的数据清零;1:复位;0:无效 |
# 控制信号说明
1. GRFA3Sel
控制信号值 | 功能 |
---|---|
A3Sel_rd | 选择待写入寄存器地址来自 Instr[15:11] |
A3Sel_rt | 选择待写入寄存器地址来自 Instr[20:16] |
A3Sel_ra | 选择写入寄存器的地址为 31 ( $ra ) |
2. GRFWDSel
控制信号值 | 功能 |
---|---|
WDSel_dmrd | 选择写入寄存器的数据来自 DM |
WDSel_aluans | 选择写入寄存器的数据来自 ALU 运算结果 |
WDSel_pc4 | 选择写入寄存器的数据为 PC+4 |
# IFU(取指单元)
把 PC 和 IM 合在一起形成了 IFU
信号名称 | 方向 | 功能描述 |
---|---|---|
NPC[31:0] | 输入 | 待写入 PC 的指令地址 |
clk | 输入 | 时钟信号 |
reset | 输入 | 异步复位信号 |
PC | 输出 | 当前指令地址 |
Instr[31:0] | 输出 | 32 位的指令值 |
# EXT(位扩展)
将 16 位二进制数进行零扩展或符号扩展到 32 位
控制信号说明
控制信号值 | 功能 |
---|---|
EXT_unsign | 零扩展 |
EXT_sign | 符号扩展 |
# ALU(算术逻辑单元)
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
A[31:0] | 输入 | 32 位输入运算数 A |
B[31:0] | 输入 | 32 位输入运算数 B |
ALUOp[4:0] | 输入 | 控制信号 |
shamt[4:0] | 输入 | 移位指令所移位数 |
C[31:0] | 输出 | 32 位输出运算结果 |
overflow | 输出 | 指示运算是否溢出(扩展用) |
控制信号说明
1. ALUOp
控制信号值 | 功能 |
---|---|
ALU_add | 执行加法运算 |
ALU_sub | 执行减法运算 |
ALU_or | 执行逻辑或运算 |
ALU_lui | 执行 lui 指令 |
2. ALUBSel
控制信号值 | 功能 |
---|---|
BSel_rt | 选择寄存器中的值进行运算 |
BSel_imm | 选择立即数进行运算 |
# CMP(比较器)
把原来 ALU 中比较值是否相等的运算移到了 CMP 里面,去指导 beq
这一类型的指令是否跳转
控制信号目前只有 CMP_beq
,未来可以扩展
`timescale 1ns / 1ps | |
`include "def.v" | |
module CMP( | |
input [31:0] rs, | |
input [31:0] rt, | |
input [2:0] CMPOp, | |
output jump | |
); | |
wire eq = (rs == rt); | |
wire ne = !eq; | |
wire le0 = (rs < 0); | |
wire ge0 = (rs > 0); | |
wire eq0 = (rs == 0); | |
assign jump = (CMPOp == `CMP_beq && eq) ? 1 : 0; | |
endmodule |
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
rs[31:0] | 输入 | $rs 寄存器的值 |
rt[31:0] | 输入 | $rt 寄存器的值 |
CMPOp[2:0] | 输入 | 控制信号 |
jump | 输出 | 指示是否跳转 |
# NPC(次地址计算单元)
把 beq
是否执行的判断交给了 CMP
,直接根据输入信号 jump
判断是否跳转
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
PC[31:0] | 输入 | 32 位输入当前地址 |
jump | 输入 | 指示 b 类型指令是否跳转 |
NPCOp[1:0] | 输入 | 控制信号 |
RA[31:0] | 输入 | $ra 寄存器保存的 32 位地址 |
NPC[31:0] | 输出 | 32 位输出次地址 |
PC+4[31:0] | 输出 | 输出 PC+4 的值 |
# 控制信号说明
控制信号值 | 功能 |
---|---|
NPC_pc4 | NPC=PC+4 |
NPC_b | 执行 beq 指令 |
NPC_j_jal | 执行 j , jal 指令 |
NPC_jalr_jr | 执行 jalr , jr 指令 |
# DM(数据储存器)
端口说明
信号名称 | 方向 | 功能描述 |
---|---|---|
Addr[31:0] | 输入 | 待操作的内存地址 |
WD[31:0] | 输入 | 待写入内存的值 |
clk | 输入 | 时钟信号 |
reset | 输入 | 异步复位信号 |
DMWrEn | 输入 | 写使能信号;1:写入有效;0:写入无效 |
DMOp[2:0] | 输入 | 控制信号 |
RD[31:0] | 输出 | 输入地址指向的内存中储存的值 |
控制信号说明
控制信号值 | 功能 |
---|---|
DM_w | 对应 lw 和 sw 指令,写入或读取整个字 |
DM_h | (保留)对应 lh 和 sh 指令,写入或读取半字 |
DM_b | (保留)对应 lb 和 sb 指令,写入或读取整个字 |
DM_hu | (保留)对应 lhu 指令 |
DM_bu | (保留)对应 lbu 指令 |
# 数据通路分析
CTRL 同时承担了译码的任务
指令 | opcode | funct | NPCOp | GRFA3Sel | GRFWDSel | EXTOp | GRFWrEn | ALUBSel | ALUOp | DMWrEn | DMOp |
---|---|---|---|---|---|---|---|---|---|---|---|
addu | 000000 | 100001 | NPC_pc4 | A3Sel_rd | WDSel_aluans | X | 1 | BSel_rt | ALU_add | 0 | X |
subu | 000000 | 100011 | NPC_pc4 | A3Sel_rd | WDSel_aluans | X | 1 | BSel_rt | ALU_sub | 0 | X |
ori | 001101 | X | NPC_pc4 | A3Sel_rt | WDSel_aluans | EXT_unsign | 1 | BSel_imm | ALU_or | 0 | X |
lw | 100011 | X | NPC_pc4 | A3Sel_rt | WDSel_dmrd | EXT_sign | 1 | BSel_imm | ALU_add | 0 | DM_w |
sw | 101011 | X | NPC_pc4 | X | WDSel_dmrd | EXT_sign | 0 | BSel_imm | ALU_add | 1 | DM_w |
beq | 000100 | X | 001 | X | X | X | 0 | X | X | 0 | X |
lui | 001111 | X | NPC_pc4 | A3Sel_rt | WDSel_aluans | X | 1 | BSel_imm | ALU_lui | 0 | X |
CTRL 首先可以用许多 wire
去表示当前的指令是什么
wire addu = (opcode == `op_ADDU && funct == `fun_ADDU); | |
wire subu = (opcode == `op_SUBU && funct == `fun_SUBU); | |
wire ori = (opcode == `op_ORI); | |
wire lui = (opcode == `op_LUI); | |
wire lw = (opcode == `op_LW); | |
wire sw = (opcode == `op_SW); | |
wire beq = (opcode == `op_BEQ); | |
wire j = (opcode == `op_J); | |
wire jal = (opcode == `op_JAL); | |
wire jr = (opcode == `op_JR && funct == `fun_JR); |
然后再用一堆多路选择器去处理各种信号,例如
assign ALUOp = (addu | lw | sw) ? `ALU_add : | |
(subu) ? `ALU_sub : | |
(ori) ? `ALU_or : | |
(lui) ? `ALU_lui : | |
4'b0000; |
最后在顶层 mips.v
文件里面实例化所有元件即可
先定义一堆导线,自己记得每个是干啥用的
wire[4:0] addr_rt, addr_rd, addr_rs, shamt; | |
wire[15:0] imm16; | |
wire[25:0] imm26; | |
wire[31:0] imm32; | |
wire[31:0] pc, npc, pc4; | |
wire[31:0] grf_rd1, grf_rd2; | |
wire[31:0] alu_ans; | |
wire[31:0] dm_rd; | |
wire[31:0] instr; | |
wire jump; | |
//wire overflow; | |
wire[3:0] ALUOp; | |
wire[2:0] CMPOp; | |
wire[2:0] DMOp; | |
wire[1:0] GRFA3Sel, GRFWDSel; | |
wire ALUBSel, DMWrEn, EXTOp, GRFWrEn; | |
wire[2:0] NPCOp; |
然后接上,例如
NPC _npc( | |
.PC(pc), | |
.imm26(imm26), | |
.imm16(imm16), | |
.rs(grf_rd1), | |
.jump(jump), | |
.NPCOp(NPCOp), | |
.PC4(pc4), | |
.NPC(npc) | |
); |
# 调试过程记录
- 需要注意本次实验 IM 和 DM 均扩展到了 4KB,因此输入地址位需要取
[11:2]
位
# 调试方法与辅助工具
采用随机程序生成和自动化测试的方法,支持的指令有 addu,subu,ori,lw,sw,beq, jal,lui,nop
采用 C 语言生成随机程序,用 iverilog
和 gtkwave
编译模拟, cmd
/ powershell
命令行脚本自动化循环测试的方式
# 思考题
- 根据你的理解,在下面给出的 DM 的输入示例中,地址信号 addr 位数为什么是 [11:2] 而不是 [9:0]?这个 addr 信号又是从哪里来的?
MIPS 中以字节为单位,而在我们设计的 DM 中,每一个 reg[31:0]
为一个单位。
Addr 来自 ALU 的输出端口,代表要读取的 DM 存储器的地址,在我们的 4KB 的 DM 设计中应当取 [11:0],又因为按字节寻址,因此取 [11:2]
- 思考 Verilog 语言设计控制器的译码方式,给出代码示例,并尝试对比各方式的优劣
三目运算符
assign ALUOp = (addu | lw | sw) ? `ALU_add : | |
(subu) ? `ALU_sub : | |
(ori) ? `ALU_or : | |
(lui) ? `ALU_lui : | |
4'b0000; |
case
语句
case(ALUOp) | |
`ALU_add: C = A + B; | |
`ALU_sub: C = A - B; | |
`ALU_or: C = A | B; | |
`ALU_and: C = A & B; | |
`ALU_xor: C = A ^ B; | |
`ALU_sll: C = B << shamt; | |
`ALU_srl: C = B >> shamt; | |
`ALU_sra: C = $signed($signed(B) >> shamt); | |
`ALU_lui: C = B << 16; | |
// Ready for add other instructions... | |
default: C = 32'h0000_0000; | |
endcase |
此外还可以用 if...else...
语句
assign
三目运算符不需要自己再定义寄存器case
语句和assign
都可以通过宏定义的方式使代码更加美观,增强可读性if...else...
语句没用过
- 在相应的部件中,reset 的优先级比其他控制信号(不包括 clk 信号)都要高,且相应的设计都是同步复位。清零信号 reset 所驱动的部件具有什么共同特点?
都是存储器,例如 PC
、 GRF
和 DM
- 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)
故忽略溢出,二者等价。
- 根据自己的设计说明单周期处理器的优缺点。
- 优点:设计简单,扩展性好
- 缺点:时钟频率取决于执行时间最长的指令,整体时钟周期长,效率较低
详细代码暂不提供
可以参考其他 dl 的,比如 %% Harahan 大佬的代码 buaa-CO-2021 / 计组 /p4 课下 at main・Harahan/buaa-CO-2021 (github.com)
UPD:2021/11/17
# 添加指令的注意事项
在考试之前总结一下如何添加各种类型指令,对号入座就行
# b 类型指令
添加 CMPOp
, EXT
为 EXT_sign
,修改 NPCOp
,在 NPC_b
中添加新指令
如果有 and link
要求则修改 GRFWrEn
为 (指令 & jump)
或者加上 !(指令 & !jump)
, GRFA3Sel
为 A3Sel_ra
(待写入寄存器为 $ra
), GRFWDSel
为 WDSel_pc4
不需要改动 ALUOp
, DMOp
, DMWrEn
, ALUBSel
需要在 CMP
里面添加跳转条件验证, NPC
里面不需要改动
# j 类型指令
无条件跳转,不需要验证跳转条件,即不经过 CMP
课下基本已经加完了
剩一个 jalr
,注意跳转的地址来自 $rs
,写入的地址不是 $ra
了, GRFA3Sel
应该是 A3Sel_rd
# 计算型指令
calc_r
不涉及立即数, ALUBSel
是 BSel_rt
, ALU
添加对应逻辑即可
calc_imm
涉及立即数,添加 EXTOp
对应信号, ALUBSel
选 BSel_imm
, ALU
添加对应逻辑即可
GRFWDSel
选 WDSel_aluans
, GRFA3Sel
选 A3Sel_rd
, GRFWrEn
置为 1
# 访存型指令
EXT
为 EXT_sign
, ALU
为 ALU_add
, ALUBSel
为 BSel_imm
, DMWrEn
和 GRFWrEn
根据需要判断
添加 DMOp
,在 DM
模块中根据要求添加对应逻辑, ALU
添加对应逻辑即可
如果是写寄存器, GRFWDSel
选 WDSel_dmrd
, GRFA3Sel
选 A3Sel_rt
, GRFWrEn
置为 1,在 DM 最后的 assign
赋值中添加逻辑
如果是写内存, GRFWrEn
置为 0, DMWrEn
置为 1,在 DM
的 case
语句中添加逻辑