三级流水线 RISC-V 设计(小试牛刀)
何为 RISC-V?
出身背景
想象一下,在计算机世界的早期,x86 和 ARM 架构就像是两位已经成名已久的老牌明星,它们各自拥有着庞大的粉丝群和广泛的市场影响力。然而,随着时代的发展,这些老牌架构变得越来越复杂,专利和架构授权问题也让许多开发者望而却步。就在这时,一位名叫 RISC-V 的新人出现了,它带着简洁、开放、灵活的特点,迅速吸引了人们的目光。
设计理念
RISC-V 的设计哲学可以用"大道至简"来形容。它摒弃了复杂且不常用的指令,只保留了处理器最常用的指令,通过执行多条常用指令的方式来达到同样的效果。这样一来,处理器的并行度和流水线能力得到了显著提升,功耗也相应降低。同时,RISC-V 还采用了模块化设计,用户可以根据自己的需求定制指令集,实现不同的功能和性能优化。
开源特性
相对 ARM 而言,开源是 RISC-V 的一大亮点。任何人都可以免费使用、修改和贡献 RISC-V 的规范和实现,无需支付任何版权费用或许可费用。这种开源特性促进了社区的协作和创新,使得 RISC-V 能够快速适应新技术和应用需求。同时,RISC-V 还拥有一个庞大的生态系统,包括各种硬件设备、软件工具、操作系统、编程语言等,为开发者提供了便捷的开发环境和技术支持。
精简指令集(RISC)
RISC-V 的指令集非常精简,其中包含最常用的指令,如加减乘除、逻辑运算、比较、存储和加载等。相对 X86 的复杂指令集,RISC-V 的指令集更加简洁,使得处理器的实现更加简单和容易。由于精简的特性,也使得 RISC-V 更加灵活、更低的功耗、更强的适应性。
设计前的准备
数电相关知识。 Verilog 基本语法。 VSCode / Notepad++、Vivado / ModelSim。 若有过单片机学习经历,也是"肥肠"好的。
RISC-V 知识
一、何为三级"流水线"
三级流水线是指将指令执行过程分解为取指(IF)、译码(ID)和执行(EX)三个阶段,每个阶段由不同的硬件单元并行处理。这种设计允许多个指令同时在不同阶段进行,从而提高了处理器的吞吐量和效率。
取指阶段(IF)
处理器从内存中取出下一条要执行的指令。该阶段的主要任务是读取指令,并将其准备送往下一阶段。
译码阶段(ID)
处理器将取到的指令进行译码,将指令分解为各个部分,如操作码、寄存器地址等。该阶段的主要任务是解析指令,确定指令的操作类型和目标寄存器。
执行阶段(EX)
处理器根据译码阶段解析出的操作码和操作数,执行指令指定的操作。该阶段的主要任务是完成指令的执行,并将结果存储在寄存器中(DATA BACK)。
二、RISC-V 指令集
四种核心指令(R / I / S / U)
指令格式
![[core] core](https://gitee.com/sjzc_etca/image-bed/raw/master/img/riscv/core.png)
格式讲解
opcode 操作码,并且标志指令类型。 rd:目标寄存器。 funct3 和 funct7:用于指定具体的操作(如加法、减法等)。 rs1 和 rs2:操作数、目标寄存器。 imm:立即数,用于指定操作数。
指令集类型
R 型指令(寄存器操作)
功能
用于寄存器之间的操作,所有操作数都来自寄存器,结果也写入寄存器。
指令格式

指令讲解

示例代码
| |
I 型指令
功能
用于寄存器与立即数之间的操作,其中一个操作数是立即数(直接嵌入指令中)。 PS:可以初步理解成将常量存入寄存器。
指令格式

指令讲解

示例代码
Load 和 Store 指令
功能
只有 Load 和 Store 指令可以访问存储器。
指令格式

指令讲解

常见指令
U 型指令(Upper Immediate 指令)
功能
用于将 20 位立即数加载到寄存器的高位。
指令格式
![[Upper Immediate 指令] Upper Immediate 指令](https://gitee.com/sjzc_etca/image-bed/raw/master/img/riscv/Upper_Immediate.png)
指令讲解
![[Upper_Immediate_open 指令] Upper_Immediate_open 指令](https://gitee.com/sjzc_etca/image-bed/raw/master/img/riscv/Upper_Immediate_open.png)
示例代码
B 型指令(Branch 指令)
功能
用于条件分支,根据寄存器值的比较结果决定是否跳转。
指令格式

指令讲解
BEQ 和 BNE: BEQ:如果 rs1 和 rs2 的值相等,就跳转。 BNE:如果 rs1 和 rs2 的值不相等,就跳转。
BLT 和 BLTU: BLT:如果 rs1 的值小于 rs2 的值(有符号数比较),就跳转。 BLTU:如果 rs1 的值小于 rs2 的值(无符号数比较),就跳转。
BGE 和 BGEU: BGE:如果 rs1 的值大于等于 rs2 的值(有符号数比较),就跳转。 BGEU:如果 rs1 的值大于等于 rs2 的值(无符号数比较),就跳转。
示例代码
J 型指令(Jump 指令)
功能
跳转指令
指令格式

指令讲解

示例代码
NOP 指令
功能
占位:在代码中插入 NOP 指令,用于占位或对齐指令流。 延迟:在流水线中插入 NOP 指令,用于解决数据冒险或控制冒险。 调试:在调试时,可以用 NOP 指令替换某些指令,观察程序行为。
指令格式

指令解释
在 RISC-V 中,NOP 指令并不是一个独立的指令,而是通过其他指令实现的。通常,NOP 指令可以表示为 ADDI x0, x0, 0,其含义是将寄存器 x0 的值加 0,结果仍然写入 x0。由于 x0 是零寄存器,其值始终为 0,因此这条指令不会改变任何状态。 NOP 指令的实现方式有很多,包括使用 ADDI x0, x0, 0、NOP、NOP.N 等等。
示例代码
其他指令
RISC-V 还是存在其他指令的,可以参考 RISC-V 指令集
设计实现
设计目的
构建一个简单的 RISC-V 处理器,实现向目的寄存器写入数读取数据、并进行加减法运算并回写入目的寄存器。
设计框架

框架模块介绍及设计
1. PC-Reg(程序计数器寄存器)
作用
保存当前指令的地址,用于跳转指令跳转。
功能
在每个时钟周期更新,指向下一条指令。遇到跳转或分支时,PC 会被更新为新的目标地址。
实现
设计细节
1、当复位信号无效(rst 为高电平)时,PC 会在每个时钟上升沿增加 4。 2、增加 4 是因为 RISC-V 的指令是 32 位(4 字节)对齐的,因此每条指令占用 4 字节的地址空间。
2. IF(取指阶段)
作用
用于取指,根据 PC 的值,从 ROM 中读取指令。
功能
根据 PC 中的地址,从 ROM 或指令缓存中读取指令,并将其传递给下一阶段(IF-ID)。
实现
| |
设计细节
1、将 PC 的值直接作为 ROM 的访问地址。 2、将 ROM 中读取的指令直接传递给 ID 阶段。
3. IF-ID(取指-译码寄存器)
作用
用于寄存器文件读写、指令译码、取指、分支判断等。
功能
在流水线中传递指令,确保指令在译码阶段被正确处理。
实现
| |
设计细节
1、将指令和地址传递给 ID-EX 阶段。 2、当复位信号无效(rst 为高电平)时,指令和地址会一直保持不变。 3、dff_set 模块:用于寄存器文件,实现寄存器文件的功能。 4、INST_NOP 为空指令,不做任何处理。
4. ID(译码阶段)
作用
解析指令并读取寄存器值。
功能
将指令分解为操作码、寄存器编号和立即数等,并从寄存器文件(Regs)中读取操作数。同时,识别指令类型(如算术、分支、加载/存储等)。
实现
| |
设计细节
1、opcode、rd、funct3、rs1、rs2、funct7、imm:这些信号通过位切片从 inst_i 中提取,分别表示指令的操作码、目标寄存器地址、功能码 3、源寄存器 1 地址、源寄存器 2 地址、功能码 7 和立即数字段。 2、根据 opcode、funct3 等信号,确定指令类型和操作类型。 3、根据指令类型和操作类型,确定目标寄存器地址、源寄存器 1 地址和源寄存器 2 地址。 4、取 imm 的最高位作为符号位,用于符号扩展。 5、控制好 reg_wen 信号,控制是否写入目标寄存器。
5. ID-EX(译码-执行寄存器)
作用
暂存译码后的指令和操作数,供 EX 阶段使用。
功能
在流水线中传递译码结果,确保执行阶段能正确获取数据。
实现
| |
设计细节
1、将指令和地址传递给 EXE 阶段。 2、当复位信号无效(rst 为高电平)时,指令和地址会一直保持不变。 3、dff_set 模块:用于寄存器文件,实现寄存器文件的功能。
6. EXE(执行阶段)
作用
执行指令,计算结果并写入目标寄存器。
功能
根据指令类型进行算术运算、逻辑运算、地址计算等。对于分支指令,计算目标地址并决定是否跳转。
实现
| |
设计细节
1、通过 reg_wen_o 信号控制是否需要将结果写回寄存器文件。 2、对于 I 类型指令,立即数(op2_i)已经在译码阶段完成符号扩展。 3、将计算结果和目标寄存器地址传递给写回阶段,支持流水线操作。 4、其余细节可看 ID(译码阶段)。
7. Regs(寄存器文件)
作用
存储 CPU 中的寄存器值。
功能
在 ID 阶段提供操作数,在 WB 阶段写回结果。RISC-V 通常有 32 个通用寄存器(x0-x31)。
实现
| |
设计细节
1、使用 for 循环将所有寄存器初始化为 0。 2、目标寄存器地址 reg_waddr_i 不能为 0(即不能写入零寄存器 x0)。 3、在写回阶段,如果目标寄存器地址与写回地址相同,直接输出写入数据(避免读取旧值)。
读取到旧值的原因

当第一个指令回写时,第二个指令的写回结果还没有写入寄存器,此时第二个指令的读操作会读取到第一个指令的写回结果。
8. ROM(只读存储器)
作用
存储 CPU 的指令。
功能
在 IF 阶段,根据 PC 地址提供指令。ROM 是只读的,指令在运行时不可修改。
实现
设计细节
RISC-V 架构要求指令是 4 字节对齐的,即每条指令的地址必须是 4 的倍数。故此处 PC 右移 2 位,即可获得指令的地址。
9. dff_set
作用
设置时钟边沿触发的 DFF。
功能
根据输入信号 rst 和 clk,设置时钟边沿触发的 DFF。
实现
| |
10. 宏定义文件
defines.v
| |
模块连接
RISC-V-CORE
| |
CORE-ROM-TOP
| |
仿真与仿真文件设计
指令文件设计
文件内容
指令解读
指令 1:
指令 2:
指令 3:
指令 4:
仿真文件设计
仿真文件内容
| |
开箱测试
ModelSim
此处省略 ModelSim 使用,直接看结果!
结果

Vivado
此处省略 Vivado 使用,直接看结果!
结果

波形

请看红色框里面代表着数据可以读取写入,并做出正确的运算。
RTL 电路
top 模块

rom 模块

riscv 模块

pc-reg 模块

if 模块

if_id 模块

id 模块

id_ex 模块

exe 模块

regs 模块

设计总结
希望大家能够喜欢,并且能够学习到设计 CPU 的思路,并逐渐提升设计 CPU 的能力。 若觉得本文对您产生了帮助,请转发给更多人,谢谢! 下面进入感谢环节!
感谢
感谢 外瑞罗格 UP 的教程与开源! 感谢 liangkangnan 的 RISC-V_tiny 项目的开源,这对我产生设计 RISC-V 的想法有了极大吸引!