目录
一、模块设计
1、pc_reg.v
1.1、功能说明
1.2、整体框图
1.3、接口列表
1.4、内部信号说明
1.5、关键电路
2、id.v
2.1、功能说明
2.2、整体框图
2.3、接口列表
2.4、内部信号说明
2.5、关键电路
3、alu.v
3.1、功能说明
3.2、整体框图
3.3、接口列表
3.4、内部信号说明
3.5、关键电路
4、mem.v
4.1、功能说明
4.2、整体框图
4.3、接口列表
4.4、内部信号说明
4.5、关键电路
5、wb.v
本设计资源获取:先关注该账号,并收藏该文章,再去我的主页在我的资源里下载该代码。
ps:本文本设计借鉴了很多前人的工作,如有雷同,纯属致敬。
欢迎交流:邮箱[email protected]
本设计采用《计算机组成与设计硬件软件接口risc-v版》中的架构,如下图所示,这是典型的哈佛结构,即指令和数据分开存储,分别放在instr_mem和data_mem中,因为我要考虑后期流水线分解,在信号数据传输过程中按流水线进行设计(但这里还没加入流水线寄存器),以便于后续分解流水级。
该设计目前的版本为CPU_lv0,32位RISC-V架构CPU,实现了RV32I中的37条指令,包含R型、I型、S型、B型、U型、J型,其中就包含了运算指令、跳转指令、立即数扩展指令、访存指令,目前所有指令都已仿真调试通过,具体仿真调试过程参考后续。
目前初代版本的端口命名还不是特别规范,有点混乱,后续会陆续进行调整。。。
该设计一共有9个源文件,每部分作用汇总如下:
源文件 |
功能作用解释 |
pc_reg.v |
程序计数器的决断模块,决定下一条指令的pc是多少 |
id.v |
译码模块,将取出的指令进行译码,译码信息包含寄存器地址、控制信号、立即数等 |
reg_file.v |
通用寄存器模块,包含了32个32位的通用寄存器,根据译码出的寄存器地址进行读写数据 |
alu.v |
运算模块,执行运算指令的相关操作,加、减等 |
mem.v |
访存模块,根据alu计算出的地址或者reg_file取出的数据进行读写操作 |
data_mem.v |
数据存储器,在访存阶段配合完成读写 |
wb.v |
回写模块,经alu计算出结果或者load数据之后,写回通用寄存器 |
cpu_lv0.v |
顶层模块 |
define.v |
宏定义文件 |
本人在设计过程中参考的资料如下:
预学习阶段:
【CO001】课程基本信息 计算机组成原理/计算机组成与设计_哔哩哔哩_bilibili
设计学习阶段:
第0期 设计这个干嘛?| 绪论 | RISC-V设计入门指北_哔哩哔哩_bilibili
手把手教你设计RISC-V 处理器 第0期-蓄势待发_哔哩哔哩_bilibili
自行设计阶段:
从零开始写RISC-V处理器 | liangkangnan的博客
从零开始设计RISC-V处理器——单周期处理器的设计_设计并实现单周期处理器,支持包含risc-v rv32i整数指令(lw, sw,add,sub, o_不学无术的小胖子.的博客-CSDN博客
PC寄存器,又名程序计数器(PC,Program counter),用于存放指令的地址,为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称,为“取指令”。与此同时,PC中的地址或自动加4或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。
端口定义如下。
module pc_reg (
input clk,
input rst_n,
input pcsrc,
input [31:0] pc_i,
//input [31:0] imm, //imm
//input [31:0] rs1_data,//jalr
//input jump,
//input jump_reg,
output reg [31:0] instr,
output reg [31:0] curr_pc
);
端口名称 |
类型 |
位宽 |
说明 |
clk |
input |
1 |
系统时钟输入 |
rst_n |
input |
1 |
系统低有效复位 |
pcsrc |
input |
1 |
pc值源的选择位 |
pc_i |
input |
32 |
经特殊指令跳转之后的pc值 |
instr |
output |
32 |
取出的指令 |
curr_pc |
output |
32 |
当前执行的指令pc值 |
其中pcsrc是选择位,以选择下一个pc值,为0时选择默认pc+4,为1时选择pc_i,按地址读出指令输出,同时pc也需要输出,因为有一部分指令需要对pc操作。
指令存储器一般为片外rom,需要挂在总线上,现在暂时用reg寄存器代替。
端口名称 |
类型 |
位宽 |
说明 |
instr_mem |
reg |
32 |
指令寄存器,存放指令 |
输出指令地址,地址发送给指令存储器,其中地址有个来源,正常顺序pc+4,pc跳转输入pc_i,由pc_src选择来源,pc_i在输入之前就已经经过pc运算操作了,涉及的指令包括B型指令、jal、jalr、auipc指令。
在作为地址去指令存储器中pc_i读数据时,要将低两位舍去,或者右移两位,原因是指令存储是按字存储,但字节寻址,一条指令4字节,去掉低两位,字节寻址变为子寻址,一次寻址一条指令。
指令译码器(Instruction Decoder,ID) 是控制器中的主要部件之一。因为计算机能且只能执行“指令”,而一串二进制数字传输过来,cpu并不知道要做什么,ID就是告诉cpu要做哪些操作。指令由操作码、地址码、立即数和两个操作字段funct等组成。操作码(opcode)和funct表示要执行的操作性质,即执行什么操作;地址码是操作码执行时的操作对象的地址,立即数是操作的直接对象,操作之前需要对立即数进行相应的预处理及扩展。计算机执行一条指定的指令时,必须首先分析这条指令的操作码是什么,以决定操作的性质和方法,然后才能控制计算机其他各部件协同完成指令表达的功能。这个分析工作由指令译码器来完成。
端口名称 |
类型 |
位宽 |
说明 |
rst_n |
input |
1 |
系统低有效复位 |
pc_i |
input |
32 |
当前执行的指令pc值 |
instr_i |
input |
32 |
取出的指令 |
rs1_data_i |
input |
32 |
从32个通用寄存器中取出的rs1数据 |
rs2_data_i |
input |
32 |
从32个通用寄存器中取出的rs2数据 |
pc_o |
output |
32 |
当前执行的指令pc值 |
rs1_addr_o |
output |
5 |
源寄存器rs1的取值地址 |
rs2_addr_o |
output |
5 |
源寄存器rs2的取值地址 |
rd_addr_o |
output |
5 |
目的寄存器地址 |
branch |
output |
1 |
控制信号,pc跳转指示信号 |
memread |
output |
1 |
控制信号,访存读使能 |
memtoreg |
output |
1 |
控制信号,写回数据来源选择位 |
regwrite |
output |
1 |
控制信号,通用寄存器(目的寄存器)写使能 |
memwrite |
output |
1 |
控制信号,访存写使能 |
alusrc |
output |
1 |
控制信号,运算源alu_src2操作数选择位(alu_src2) |
aluop[1:0] |
output |
2 |
控制信号,运算类型选择 |
imm |
output |
32 |
预处理及扩展后的立即数 |
funct |
output |
4 |
功能码 {funct7[5],funct3},用于判断具体指令 |
opcode_o |
output |
7 |
7位操作码 |
端口名称 |
类型 |
位宽 |
说明 |
opcode |
wire |
7 |
不赘述了,看下图。 |
rd |
wire |
5 |
|
funct3 |
wire |
3 |
|
rs1 |
wire |
5 |
|
rs2 |
wire |
5 |
|
funct7 |
wire |
7 |
因为在数据通路中后续会使用到pc值,需要将pc也传递进来,然后将输入的指令解码之后得到的通用寄存器的地址,根据地址向寄存器堆reg_file读取数据,同时将写的rd地址传递到下一组合逻辑电路,在写回阶段用到此rd地址。
将7组控制信号输出到下一级进行相关控制,其中各种不同类型的控制信号赋值如下图所示:
信号 |
R |
I |
B |
J |
JR |
U |
UPC |
IL |
S |
branch |
0 |
0 |
1 |
1 |
1 |
0 |
0 |
0 |
0 |
memread |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
memtoreg |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
regwrite |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
0 |
memwrite |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
alusrc |
0 |
1 |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
aluop[1:0] |
10 |
10 |
01 |
11 |
11 |
11 |
11 |
00 |
00 |
同时在以上各种类型指令中(除了R型指令之外的)生成imm,并扩展成32位,以供后续计算或pc值跳转。其中imm_gen是嵌入在整个模块内部,同控制信号一起生成。
在计算机系统中,ALU(Arithmetic Logic Unit)是 中央处理器 的主要组成部分,它代表算术逻辑单元,执行算术运算、逻辑运算、移位操作,也可执行地址运算和转换。 它也称为整数单元 (IU,integer unit),它是 CPU 或 GPU 内的逻辑电路,是处理器中执行计算的最后一个组件。其中还包括跳转指令条件判断。
部分主要路径,总体逻辑以代码为准。
端口名称 |
类型 |
位宽 |
说明 |
pc_i |
input |
32 |
当前执行的指令pc值 |
rs1_data_i |
input |
32 |
从32个通用寄存器中取出的rs1数据 |
rs2_data_i |
input |
32 |
从32个通用寄存器中取出的rs2数据 |
rd_addr_i |
input |
5 |
目的寄存器地址 |
funct |
input |
4 |
功能码 {funct7[5],funct3},用于判断具体指令 |
imm |
input |
32 |
立即数 |
opcode |
input |
7 |
7位操作码 |
branch |
input |
1 |
控制信号,pc跳转指示信号 |
memread |
input |
1 |
控制信号,访存读使能 |
memtoreg |
input |
1 |
控制信号,写回数据来源选择位 |
regwrite |
input |
1 |
控制信号,通用寄存器(目的寄存器)写使能 |
memwrite |
input |
1 |
控制信号,访存写使能 |
alusrc |
input |
1 |
控制信号,运算源alu_src2操作数选择位(alu_src2) |
aluop[1:0] |
input |
2 |
控制信号,运算类型选择 |
branch_sign |
output |
1 |
跳转指令的指示信号(跳转条件判定后的标志位) |
result |
output |
32 |
执行操作的运算结果 |
rd_addr_o |
output |
5 |
目的寄存器地址 |
branch_o |
output |
1 |
--- |
memread_o |
output |
1 |
--- |
memtoreg_o |
output |
1 |
--- |
regwrite_o |
output |
1 |
--- |
memwrite_o |
output |
1 |
--- |
pc_o |
output |
32 |
--- |
write_data |
output |
32 |
访存操作中需要写入的数据 |
align |
output |
4 |
访存指令的对齐指示信号 |
aluop_o |
output |
2 |
--- |
端口名称 |
类型 |
位宽 |
说明 |
cond_beq |
wire |
1 |
beq、bne指令判断是否相等的条件 |
cond_bge |
wire |
32 |
bge、blt指令判断指令是否大于等于的条件(有符号) |
cond_bgeu |
wire |
32 |
bgeu、bltu指令判断指令是否大于等于的条件(无符号) |
alu_src1 |
wire |
32 |
参与运算的源操作数1 |
alu_src2 |
wire |
32 |
参与运算的源操作数2 |
mask |
reg |
32 |
用于执行逻辑右移操作的掩码 |
前一译码模块的输入进来的除了上述主要的控制信号之外,运算源操作数有三类,rs1_data、rs2_data、imm,其中运算源操作数alusrc1=rs1_data,运算源操作数alu_src2是rs2_data与imm二选一,选择控制位由alusrc决定。
对于跳转类型指令,需要在满足跳转条件才会执行,故而设置了三个条件判断标志位cond_beq、cond_bge、cond_bgeu,当满足跳转指令的对应条件时跳转标志位branch_sign置1,才会触发跳转操作,这里的跳转指令条件就是对rs1_data和rs2_data不等判断或者大小判断。其中注意cond_bge是有符号位的大于等于条件判断位,需要对待判断源操作数调用函数 $signed(rs1_data_i) 。
在执行逻辑操作,逻辑右移时,使用逻辑右移符号>>>不管用,使用$signed(xxx)>>>,也不管用,故而设置一个掩码mask,通过将mask算数左移相应位,再将其与算术右移的结果按位或操作,就可以等效为逻辑右移,这里需要注意mask的具体赋值是怎样的。
如果是访存指令,store指令就需要直接将rs2_data输出到下一级模块写入数据存储器,load和store指令都要输出读写和写入的数据存储器地址给下一级。
访存指令load和store的访存模块。
端口名称 |
类型 |
位宽 |
说明 |
clk |
input |
1 |
系统时钟输入 |
rst_n |
input |
1 |
系统低有效复位 |
branch_i |
input |
1 |
控制信号,pc跳转指示信号 |
memread_i |
input |
1 |
控制信号,访存读使能 |
memtoreg_i |
input |
1 |
控制信号,写回数据来源选择位 |
regwrite_i |
input |
1 |
控制信号,通用寄存器(目的寄存器)写使能 |
memwrite_i |
input |
1 |
控制信号,访存写使能 |
aluop_i |
input |
2 |
控制信号,运算类型选择 |
branch_sign |
input |
1 |
跳转指令的指示信号(跳转条件判定后的标志位) |
result |
input |
32 |
执行操作的运算结果(这里就是访存的地址或者写回数据) |
rd_addr_i |
input |
5 |
目的寄存器地址 |
pc_i |
input |
32 |
当前执行的指令pc值 |
write_data |
input |
32 |
访存操作中需要写入的数据 |
align |
input |
4 |
访存指令的对齐指示信号 |
rd_addr_o |
output |
5 |
目的寄存器地址 |
pc_o |
output |
32 |
当前执行的指令pc值 |
pcsrc |
output |
1 |
pc值源的选择位 |
read_data |
output |
32 |
从data_mem中读取的数据 |
rd_data |
output |
32 |
经运算得到的写回结果 |
memtoreg_o |
output |
1 |
--- |
regwrite_o |
output |
1 |
--- |
端口名称 |
类型 |
位宽 |
说明 |
ram_data_o |
wire |
32 |
从data_mem中读出的数据 |
ram_data_i |
reg |
32 |
写入data_mem中的数据 |
这个模块就简单了,根据访存指令的不同类型输出对应的读取数据,和输入对应的写入数据。
其中例化了data_mem数据存储器。
这个模块更简单了,根据输入的控制信号,regwrite和memtoreg写回数据,按照rd_addr写回对应的通用寄存器。
写回数据有两个来源,数据存储器读出的数据read_data,通用寄存器读出的数据rs1_data+imm(write_data),memtoreg=0时选择write,1选择read_data。