# P4 课下项目:单周期 CPU 设计 - Verilog 实现

本文仅仅是 P4 课下 CPU 设计实验报告添加了部分内容
通过阅读或许可以给您完成 P4 课下任务提供些许帮助
课下要求可能会发生变化,下面的电路仅供参考,未必完全正确

# 总体设计概述

要求实现的指令集包括 addu,subu,ori,lw,sw,beq, jal,jr,lui,nop

利用 Verilog 来实现时,参考了 P3 的 Logisim 电路设计,也做了一些改变

image-20211107220626749

整体架构与高小鹏老师课件中的图类似

复制自互联网

文件的树形结构层次如下

image-20211113152010449

# 关键模块定义

变量名的命名遵从以下约定:

  • 元件的命名为下划线开头的小写字母单词,例如 _grf_alu
  • 电线 wire 的命名为以下划线分隔的小写字母单词,例如 ALU 的 32 位输出命名为 alu_ans ,GRF 的两个数据输出命名为 grf_rd1grf_rd2 ,判断是否是 addu 指令的电线命名为 addu
  • 控制信号如果关于元件的操作以 Op 结尾,如 ALUOpDMOp 等;如果是多路选择器的控制信号,则以 Sel 结尾,如 GRFA3SelGRFWDSelALUBSel 等;如果是写使能信号则以 WrEn 结尾,如 GRFWrEnDMWrEn
  • 端口命名与 P3 相同
  • 控制信号的宏定义命名为元件名 + 下划线 + 功能,如 ALU_addNPC_jal_jDM_wDM_hu
  • opcodefunct 信号的宏命名为 op / fun + 下划线 + 大写指令名,如 op_ADDUfun_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_pc4NPC=PC+4
NPC_b执行 beq 指令
NPC_j_jal执行 jjal 指令
NPC_jalr_jr执行 jalrjr 指令

# DM(数据储存器)

端口说明

信号名称方向功能描述
Addr[31:0]输入待操作的内存地址
WD[31:0]输入待写入内存的值
clk输入时钟信号
reset输入异步复位信号
DMWrEn输入写使能信号;1:写入有效;0:写入无效
DMOp[2:0]输入控制信号
RD[31:0]输出输入地址指向的内存中储存的值

控制信号说明

控制信号值功能
DM_w对应 lwsw 指令,写入或读取整个字
DM_h(保留)对应 lhsh 指令,写入或读取半字
DM_b(保留)对应 lbsb 指令,写入或读取整个字
DM_hu(保留)对应 lhu 指令
DM_bu(保留)对应 lbu 指令

# 数据通路分析

CTRL 同时承担了译码的任务

指令opcodefunctNPCOpGRFA3SelGRFWDSelEXTOpGRFWrEnALUBSelALUOpDMWrEnDMOp
addu000000100001NPC_pc4A3Sel_rdWDSel_aluansX1BSel_rtALU_add0X
subu000000100011NPC_pc4A3Sel_rdWDSel_aluansX1BSel_rtALU_sub0X
ori001101XNPC_pc4A3Sel_rtWDSel_aluansEXT_unsign1BSel_immALU_or0X
lw100011XNPC_pc4A3Sel_rtWDSel_dmrdEXT_sign1BSel_immALU_add0DM_w
sw101011XNPC_pc4XWDSel_dmrdEXT_sign0BSel_immALU_add1DM_w
beq000100X001XXX0XX0X
lui001111XNPC_pc4A3Sel_rtWDSel_aluansX1BSel_immALU_lui0X

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 语言生成随机程序,用 iveriloggtkwave 编译模拟, cmd / powershell 命令行脚本自动化循环测试的方式

# 思考题

  1. 根据你的理解,在下面给出的 DM 的输入示例中,地址信号 addr 位数为什么是 [11:2] 而不是 [9:0]?这个 addr 信号又是从哪里来的?

66.png

MIPS 中以字节为单位,而在我们设计的 DM 中,每一个 reg[31:0] 为一个单位。

Addr 来自 ALU 的输出端口,代表要读取的 DM 存储器的地址,在我们的 4KB 的 DM 设计中应当取 [11:0],又因为按字节寻址,因此取 [11:2]

  1. 思考 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... 语句没用过

  1. 在相应的部件中,reset 的优先级比其他控制信号(不包括 clk 信号)都要,且相应的设计都是同步复位。清零信号 reset 所驱动的部件具有什么共同特点?

都是存储器,例如 PCGRFDM

  1. C 语言是一种弱类型程序设计语言。C 语言中不对计算结果溢出进行处理,这意味着 C 语言要求程序员必须很清楚计算结果是否会导致溢出。因此,如果仅仅支持 C 语言,MIPS 指令的所有计算指令均可以忽略溢出。 请说明为什么在忽略溢出的前提下,addi 与 addiu 是等价的,add 与 addu 是等价的。提示:阅读《MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set》中相关指令的 Operation 部分 。

根据 RTL 语言描述: addiaddiu 的区别在于当出现溢出时, addiu 忽略溢出,并将溢出的最高位舍弃; addi 会报告 SignalException(IntegerOverflow)

故忽略溢出,二者等价。

  1. 根据自己的设计说明单周期处理器的优缺点。
  • 优点:设计简单,扩展性好
  • 缺点:时钟频率取决于执行时间最长的指令,整体时钟周期长,效率较低

详细代码暂不提供

可以参考其他 dl 的,比如 %% Harahan 大佬的代码 buaa-CO-2021 / 计组 /p4 课下 at main・Harahan/buaa-CO-2021 (github.com)

UPD:2021/11/17


# 添加指令的注意事项

在考试之前总结一下如何添加各种类型指令,对号入座就行

# b 类型指令

添加 CMPOpEXTEXT_sign ,修改 NPCOp ,在 NPC_b 中添加新指令

如果有 and link 要求则修改 GRFWrEn(指令 & jump) 或者加上 !(指令 & !jump)GRFA3SelA3Sel_ra (待写入寄存器为 $ra ), GRFWDSelWDSel_pc4

不需要改动 ALUOpDMOpDMWrEnALUBSel

需要在 CMP 里面添加跳转条件验证, NPC 里面不需要改动

# j 类型指令

无条件跳转,不需要验证跳转条件,即不经过 CMP

课下基本已经加完了

剩一个 jalr ,注意跳转的地址来自 $rs ,写入的地址不是 $ra 了, GRFA3Sel 应该是 A3Sel_rd

# 计算型指令

calc_r 不涉及立即数, ALUBSelBSel_rtALU 添加对应逻辑即可

calc_imm 涉及立即数,添加 EXTOp 对应信号, ALUBSelBSel_immALU 添加对应逻辑即可

GRFWDSelWDSel_aluansGRFA3SelA3Sel_rdGRFWrEn 置为 1

# 访存型指令

EXTEXT_signALUALU_addALUBSelBSel_immDMWrEnGRFWrEn 根据需要判断

添加 DMOp ,在 DM 模块中根据要求添加对应逻辑, ALU 添加对应逻辑即可

如果是写寄存器, GRFWDSelWDSel_dmrdGRFA3SelA3Sel_rtGRFWrEn 置为 1,在 DM 最后的 assign 赋值中添加逻辑

如果是写内存, GRFWrEn 置为 0, DMWrEn 置为 1,在 DMcase 语句中添加逻辑