# 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 语句中添加逻辑