三级流水线 RISC-V 设计(小试牛刀)

从零开始设计一个简易的三级流水线 RISC-V 处理器,包含取指、译码、执行三阶段,以及 Verilog 实现与仿真验证。

何为 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

格式讲解

opcode 操作码,并且标志指令类型。 rd:目标寄存器。 funct3 和 funct7:用于指定具体的操作(如加法、减法等)。 rs1 和 rs2:操作数、目标寄存器。 imm:立即数,用于指定操作数。

指令集类型

R 型指令(寄存器操作)
功能

用于寄存器之间的操作,所有操作数都来自寄存器,结果也写入寄存器。

指令格式

Register-Register 指令

指令讲解

reg 指令

示例代码
1
2
3
4
5
6
7
ADD x1, x2, x3      # x1 = x2 + x3
SUB x4, x5, x6      # x4 = x5 - x6
SLT x7, x8, x9      # 如果 x8 < x9(有符号比较),x7 = 1;否则 x7 = 0
SLTU x10, x0, x11   # 如果 x11 != 0,x10 = 1;否则 x10 = 0(相当于 SNEZ x10, x11)
AND x12, x13, x14   # x12 = x13 & x14
SLL x15, x16, x17   # x15 = x16 << (x17 & 0x1F)
SRA x18, x19, x20   # x18 = x19 >>> (x20 & 0x1F)
I 型指令
功能

用于寄存器与立即数之间的操作,其中一个操作数是立即数(直接嵌入指令中)。 PS:可以初步理解成将常量存入寄存器。

指令格式

Immediate-Register 指令

指令讲解

Immediate 指令

示例代码
1
2
3
4
5
ADDI x1, x2, 10     # x1 = x2 + 10
SLTI x3, x4, 20     # 如果 x4 < 20,x3 = 1;否则 x3 = 0
SLTIU x5, x6, 1     # 如果 x6 == 0,x5 = 1;否则 x5 = 0
ANDI x7, x8, 0xFF   # x7 = x8 & 0xFF
XORI x9, x10, -1    # x9 = ~x10(按位取反)
Load 和 Store 指令
功能

只有 Load 和 Store 指令可以访问存储器。

指令格式

Load_STORE 指令

指令讲解

load_store_open 指令

常见指令
1
2
3
#LOAD demo
LW x1, 8(x2)  # 从内存地址 x2 + 8 处读取一个字(32 位)到 x1
LH x3, -4(x4) # 从内存地址 x4 - 4 处读取一个半字(16 位)到 x3
1
2
3
#Store demo
SW x1, 12(x2)  # 将 x1 的值写入内存地址 x2 + 12 处
SB x5, 0(x6)   #  x5 的低 8 位写入内存地址 x6 + 0 
U 型指令(Upper Immediate 指令)
功能

用于将 20 位立即数加载到寄存器的高位。

指令格式

Upper Immediate 指令

指令讲解

Upper_Immediate_open  指令

示例代码
1
2
3
#用 LUI 构造一个大数:
LUI x1, 0x12345     # x1 = 0x12345000
ADDI x1, x1, 0x678  # x1 = 0x12345000 + 0x678 = 0x12345678
1
2
3
#用 AUIPC 跳转到一个远地址
AUIPC x1, 0x10      # x1 = PC + 0x10000
JALR x0, 0(x1)      # 跳转到 x1 的地址
B 型指令(Branch 指令)
功能

用于条件分支,根据寄存器值的比较结果决定是否跳转。

指令格式

Branch-Register 指令

指令讲解

BEQ 和 BNE: BEQ:如果 rs1 和 rs2 的值相等,就跳转。 BNE:如果 rs1 和 rs2 的值不相等,就跳转。


BLT 和 BLTU: BLT:如果 rs1 的值小于 rs2 的值(有符号数比较),就跳转。 BLTU:如果 rs1 的值小于 rs2 的值(无符号数比较),就跳转。


BGE 和 BGEU: BGE:如果 rs1 的值大于等于 rs2 的值(有符号数比较),就跳转。 BGEU:如果 rs1 的值大于等于 rs2 的值(无符号数比较),就跳转。

示例代码
1
2
3
#BEQ 和 BNE:
BEQ x1, x2, label  # 如果 x1 == x2,跳转到 label
BNE x3, x4, label  # 如果 x3 != x4,跳转到 label
1
2
3
#BLT 和 BLTU:
BLT x5, x6, label  # 如果 x5 < x6(有符号比较),跳转到 label
BLTU x7, x8, label # 如果 x7 < x8(无符号比较),跳转到 label
1
2
3
#BGE 和 BGEU:
BGE x9, x10, label  # 如果 x9 >= x10(有符号比较),跳转到 label
BGEU x11, x12, label # 如果 x11 >= x12(无符号比较),跳转到 label
J 型指令(Jump 指令)
功能

跳转指令

指令格式

jump1 指令 jump2 指令

指令讲解

jump_open 指令

示例代码
1
2
3
4
5
JAL x1, label  # 跳转到 label,并将返回地址(PC + 4)保存到 x1
JAL x0, label  # 跳转到 label,不保存返回地址(相当于伪指令 J label)

JALR x1, x2, 0  # 跳转到 x2 的值对应的地址,并将返回地址(PC + 4)保存到 x1
JALR x0, x3, 8  # 跳转到 x3 + 8 对应的地址,不保存返回地址
NOP 指令
功能

占位:在代码中插入 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 等等。

示例代码
1
2
3
addi x1, x2, 10  # x1 = x2 + 10
nop              # 空操作
addi x3, x4, 20  # x3 = x4 + 20
其他指令

RISC-V 还是存在其他指令的,可以参考 RISC-V 指令集

设计实现

设计目的

构建一个简单的 RISC-V 处理器,实现向目的寄存器写入数读取数据、并进行加减法运算并回写入目的寄存器。

设计框架

project

框架模块介绍及设计

1. PC-Reg(程序计数器寄存器)

作用

保存当前指令的地址,用于跳转指令跳转。

功能

在每个时钟周期更新,指向下一条指令。遇到跳转或分支时,PC 会被更新为新的目标地址。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module pc_reg (
    input wire clk,
    input wire rst,
    // input wire [31:0] pc_in,
    output reg [31:0] pc_out
);
    always @(posedge clk or negedge rst) begin
        if(!rst)
            pc_out <= 32'h0;
        else
            pc_out <= pc_out + 32'd4;
    end

endmodule
设计细节

1、当复位信号无效(rst 为高电平)时,PC 会在每个时钟上升沿增加 4。 2、增加 4 是因为 RISC-V 的指令是 32 位(4 字节)对齐的,因此每条指令占用 4 字节的地址空间。

2. IF(取指阶段)

作用

用于取指,根据 PC 的值,从 ROM 中读取指令。

功能

根据 PC 中的地址,从 ROM 或指令缓存中读取指令,并将其传递给下一阶段(IF-ID)。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module ifetch (
    input wire [31:0] pc_addr_i,        //from pc_reg
    input wire [31:0] rom_inst_i,       ///from rom
    output wire [31:0] if2rom_addr_o,   //to rom
    output wire [31:0] inst_addr_o,     //to idecode
    output wire [31:0] inst_o           //to idecode
);

    assign if2rom_addr_o = pc_addr_i;   //from rom select directives addr

    assign inst_addr_o = pc_addr_i;     //now system running directives addr

    assign inst_o = rom_inst_i;         //now system running directives (from rom)

endmodule
设计细节

1、将 PC 的值直接作为 ROM 的访问地址。 2、将 ROM 中读取的指令直接传递给 ID 阶段。

3. IF-ID(取指-译码寄存器)

作用

用于寄存器文件读写、指令译码、取指、分支判断等。

功能

在流水线中传递指令,确保指令在译码阶段被正确处理。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
`include "defines.v"

module if_id (
    input wire clk,
    input wire rst,
    input wire [31:0] inst_i,
    input wire [31:0] inst_addr_i,
    output wire [31:0] inst_o,
    output wire [31:0] inst_addr_o
);

    dff_set #(32) dff1 (clk ,rst, `INST_NOP, inst_i, inst_o);
    dff_set #(32) dff2 (clk ,rst, 32'b0, inst_addr_i, inst_addr_o);

endmodule
设计细节

1、将指令和地址传递给 ID-EX 阶段。 2、当复位信号无效(rst 为高电平)时,指令和地址会一直保持不变。 3、dff_set 模块:用于寄存器文件,实现寄存器文件的功能。 4、INST_NOP 为空指令,不做任何处理。

4. ID(译码阶段)

作用

解析指令并读取寄存器值。

功能

将指令分解为操作码、寄存器编号和立即数等,并从寄存器文件(Regs)中读取操作数。同时,识别指令类型(如算术、分支、加载/存储等)。

实现
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
`include "defines.v"  // 包含宏定义文件
module idecode (
    //from if_id
    input wire [31:0] inst_i,
    input wire [31:0] inst_addr_i,

    //from regs
    input wire [31:0] rs1_data_i,
    input wire [31:0] rs2_data_i,

    //to regs
    output reg [4:0] rs1_addr_o,
    output reg [4:0] rs2_addr_o,

    //to id_ex
    output reg [31:0] inst_o,
    output reg [31:0] inst_addr_o,
    output reg [31:0] op1_o,
    output reg [31:0] op2_o,
    output reg [4:0]  rd_addr_o,
    output reg        reg_wen
);

    wire [6:0] opcode ;
    wire [4:0] rd ;
    wire [2:0] funct3 ;
    wire [4:0] rs1 ;
    wire [4:0] rs2 ;
    wire [2:0] funct7 ;
    wire [11:0] imm ;

    assign opcode   = inst_i[6:0];
    assign rd       = inst_i[11:7];
    assign funct3   = inst_i[14:12];
    assign rs1      = inst_i[19:15];
    assign rs2      = inst_i[24:20];
    assign funct7   = inst_i[31:25];
    assign imm      = inst_i[31:20];

    always @(*) begin
        inst_o      = inst_i;
        inst_addr_o = inst_addr_i;
        case (opcode)
            `INST_TYPE_I: begin
                case (funct3)
                    `INST_ADDI: begin
                        rs1_addr_o  = rs1;
                        rs2_addr_o  = 5'b0;
                        op1_o       = rs1_data_i;
                        op2_o       = {{20{imm[11]}},imm};    //符号位扩展
                        rd_addr_o   = rd;
                        reg_wen     =1;
                    end

                    default: begin
                        rs1_addr_o  = 5'b0;
                        rs2_addr_o  = 5'b0;
                        op1_o       = 32'b0;
                        op2_o       = 32'b0;
                        rd_addr_o   = 5'b0;
                        reg_wen     =0;
                    end
                endcase
            end


            `INST_TYPE_R_M: begin
                case (funct3)
                    `INST_ADD_SUB: begin
                        rs1_addr_o  = rs1;
                        rs2_addr_o  = rs2;
                        op1_o       = rs1_data_i;
                        op2_o       = rs2_data_i;
                        rd_addr_o   = rd;
                        reg_wen     =1;
                    end

                    default: begin
                        rs1_addr_o  = 5'b0;
                        rs2_addr_o  = 5'b0;
                        op1_o       = 32'b0;
                        op2_o       = 32'b0;
                        rd_addr_o   = 5'b0;
                        reg_wen     = 0;
                    end
                endcase
            end


            default: begin
                rs1_addr_o  = 5'b0;
                rs2_addr_o  = 5'b0;
                op1_o       = 32'b0;
                op2_o       = 32'b0;
                rd_addr_o   = 5'b0;
                reg_wen     =0;
            end
        endcase
    end

endmodule
设计细节

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
`include "defines.v"  // 包含宏定义文件
module id_ex (
    input wire clk,
    input wire rst,
    //frm idecode
    input wire [31:0] inst_i,
    input wire [31:0] inst_addr_i,
    input wire [31:0] op1_i,
    input wire [31:0] op2_i,
    input wire [4:0] rd_addr_i,
    input wire reg_wen_i,
    //to exe
    output wire [31:0]inst_o,
    output wire [31:0]inst_addr_o,
    output wire [31:0]op1_o,
    output wire [31:0]op2_o,
    output wire [4:0]rd_addr_o,
    output wire reg_wen_o
);

    dff_set #(32) dff1 (clk ,rst, `INST_NOP, inst_i, inst_o);
    dff_set #(32) dff2 (clk ,rst, 32'b0, inst_addr_i, inst_addr_o);
    dff_set #(32) dff3 (clk ,rst, 32'b0, op1_i, op1_o);
    dff_set #(32) dff4 (clk ,rst, 32'b0, op2_i, op2_o);
    dff_set #(5) dff5 (clk ,rst, 5'b0, rd_addr_i, rd_addr_o);
    dff_set #(1) dff6 (clk ,rst, 1'b0, reg_wen_i, reg_wen_o);

endmodule
设计细节

1、将指令和地址传递给 EXE 阶段。 2、当复位信号无效(rst 为高电平)时,指令和地址会一直保持不变。 3、dff_set 模块:用于寄存器文件,实现寄存器文件的功能。

6. EXE(执行阶段)

作用

执行指令,计算结果并写入目标寄存器。

功能

根据指令类型进行算术运算、逻辑运算、地址计算等。对于分支指令,计算目标地址并决定是否跳转。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
`include "defines.v"  // 包含宏定义文件
module exe (
    //frm idecode
    input wire [31:0] inst_i,
    input wire [31:0] inst_addr_i,
    input wire [31:0] op1_i,
    input wire [31:0] op2_i,
    input wire [4:0] rd_addr_i,
    input wire reg_wen_i,

    //to regs
    output reg [31:0] reg_wdata_o,
    output reg [4:0] reg_waddr_o,
    output reg reg_wen_o

);

    wire [6:0] opcode ;
    wire [4:0] rd ;
    wire [2:0] funct3 ;
    wire [4:0] rs1 ;
    wire [4:0] rs2 ;
    wire [6:0] funct7 ;
    wire [11:0] imm ;

    assign opcode   = inst_i[6:0];
    assign rd       = inst_i[11:7];
    assign funct3   = inst_i[14:12];
    assign rs1      = inst_i[19:15];
    assign rs2      = inst_i[24:20];
    assign funct7   = inst_i[31:25];
    assign imm      = inst_i[31:20];

    always @(*) begin
        case (opcode)
            `INST_TYPE_I: begin
                    case (funct3)
                        `INST_ADDI: begin
                            reg_wdata_o = op1_i + op2_i;
                            reg_waddr_o = rd_addr_i;
                            reg_wen_o   = 1;
                        end
                        default: begin
                            reg_wdata_o = 32'b0;
                            reg_waddr_o = 5'b0;
                            reg_wen_o   = 0;
                        end
                    endcase
                end

            `INST_TYPE_R_M: begin
                case (funct3)
                    `INST_ADD_SUB: begin
                        if (funct7==7'b000_0000) begin          //ADD
                            reg_wdata_o = op1_i + op2_i;
                            reg_waddr_o = rd_addr_i;
                            reg_wen_o   = 1;
                        end
                        else if (funct7==7'b010_0000) begin     //SUB
                            reg_wdata_o = op1_i - op2_i;
                            reg_waddr_o = rd_addr_i;
                            reg_wen_o   = 1;
                        end
                    end

                      default: begin
                        reg_wdata_o = 32'b0;
                        reg_waddr_o = 5'b0;
                        reg_wen_o   = 0;
                    end
                endcase
            end

            default: begin
                reg_wdata_o = 32'b0;
                reg_waddr_o = 5'b0;
                reg_wen_o   = 0;
            end
        endcase
    end
endmodule
设计细节

1、通过 reg_wen_o 信号控制是否需要将结果写回寄存器文件。 2、对于 I 类型指令,立即数(op2_i)已经在译码阶段完成符号扩展。 3、将计算结果和目标寄存器地址传递给写回阶段,支持流水线操作。 4、其余细节可看 ID(译码阶段)。

7. Regs(寄存器文件)

作用

存储 CPU 中的寄存器值。

功能

在 ID 阶段提供操作数,在 WB 阶段写回结果。RISC-V 通常有 32 个通用寄存器(x0-x31)。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
module regs (
    input wire clk,
    input wire rst,
    //form idecode
    input wire [4:0] reg1_raddr_i,
    input wire [4:0] reg2_raddr_i,

    //from ex back
    input wire [4:0] reg_waddr_i,
    input wire [31:0] reg_wdata_i,
    input wire reg_wen_i,

    //to idecode
    output reg [31:0] reg1_rdata_o,
    output reg [31:0] reg2_rdata_o
);
    reg [31:0] regs [31:0];

    //clear regs
    integer  i;
    always @(posedge clk or negedge rst) begin
        if(!rst) begin
            for (i = 0;i<32 ;i=i+1 ) begin
                regs[i] <= 32'h0;
            end
        end
        else if(reg_wen_i&& reg_waddr_i != 5'b0 ) begin //避免写到0号寄存器
            regs[reg_waddr_i] <= reg_wdata_i;
        end
    end

    always @(*) begin
        if(!rst)
            reg1_rdata_o =32'h0;
        else if(reg1_raddr_i== 5'b0)
            reg1_rdata_o = 32'h0;
        else if(reg_wen_i && reg_waddr_i == reg1_raddr_i) //避免读到旧值
            reg1_rdata_o = reg_wdata_i;
        else
            reg1_rdata_o = regs[reg1_raddr_i];
    end

    always @(*) begin
        if(!rst)
            reg2_rdata_o =32'h0;
        else if(reg2_raddr_i== 5'b0)
            reg2_rdata_o = 32'h0;
        else if(reg_wen_i && reg_waddr_i == reg2_raddr_i) //避免读到旧值
            reg2_rdata_o = reg_wdata_i;
        else
            reg2_rdata_o = regs[reg2_raddr_i];
    end
endmodule
设计细节

1、使用 for 循环将所有寄存器初始化为 0。 2、目标寄存器地址 reg_waddr_i 不能为 0(即不能写入零寄存器 x0)。 3、在写回阶段,如果目标寄存器地址与写回地址相同,直接输出写入数据(避免读取旧值)。

读取到旧值的原因

regs_back

当第一个指令回写时,第二个指令的写回结果还没有写入寄存器,此时第二个指令的读操作会读取到第一个指令的写回结果。

8. ROM(只读存储器)

作用

存储 CPU 的指令。

功能

在 IF 阶段,根据 PC 地址提供指令。ROM 是只读的,指令在运行时不可修改。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module rom (
    input wire [31:0] inst_addr_i,
    output reg [31:0] inst_o
);
    reg [31:0] rom_mem [0:11]; // 12 insts

    always@(*)begin
        inst_o = rom_mem[inst_addr_i>>2];//pc_reg 4width >>2
    end

endmodule
设计细节

RISC-V 架构要求指令是 4 字节对齐的,即每条指令的地址必须是 4 的倍数。故此处 PC 右移 2 位,即可获得指令的地址。

9. dff_set

作用

设置时钟边沿触发的 DFF。

功能

根据输入信号 rst 和 clk,设置时钟边沿触发的 DFF。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
module dff_set #(
    parameter DW = 32
)(
    input wire clk,
    input wire rst,
    input wire [DW-1:0] set_data,
    input wire [DW-1:0] data_in,
    output reg [DW-1:0] data_out
);

    always @(posedge clk or negedge rst) begin
        if(!rst)
            data_out <= set_data;
        else
            data_out <= data_in;
    end

endmodule

10. 宏定义文件

defines.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// I type inst
`define INST_TYPE_I 7'b0010011
`define INST_ADDI   3'b000
`define INST_SLTI   3'b010
`define INST_SLTIU  3'b011
`define INST_XORI   3'b100
`define INST_ORI    3'b110
`define INST_ANDI   3'b111
`define INST_SLLI   3'b001
`define INST_SRI    3'b101

// L type inst
`define INST_TYPE_L 7'b0000011
`define INST_LB     3'b000
`define INST_LH     3'b001
`define INST_LW     3'b010
`define INST_LBU    3'b100
`define INST_LHU    3'b101

// S type inst
`define INST_TYPE_S 7'b0100011
`define INST_SB     3'b000
`define INST_SH     3'b001
`define INST_SW     3'b010

// R and M type inst
`define INST_TYPE_R_M 7'b0110011
// R type inst
`define INST_ADD_SUB 3'b000
`define INST_SLL    3'b001
`define INST_SLT    3'b010
`define INST_SLTU   3'b011
`define INST_XOR    3'b100
`define INST_SR     3'b101
`define INST_OR     3'b110
`define INST_AND    3'b111
// M type inst
`define INST_MUL    3'b000
`define INST_MULH   3'b001
`define INST_MULHSU 3'b010
`define INST_MULHU  3'b011
`define INST_DIV    3'b100
`define INST_DIVU   3'b101
`define INST_REM    3'b110
`define INST_REMU   3'b111

// J type inst
`define INST_JAL    7'b1101111
`define INST_JALR   7'b1100111

`define INST_LUI    7'b0110111
`define INST_AUIPC  7'b0010111
`define INST_NOP    32'h00000013
`define INST_NOP_OP 7'b0000001
`define INST_MRET   32'h30200073
`define INST_RET    32'h00008067

`define INST_FENCE  7'b0001111
`define INST_ECALL  32'h73
`define INST_EBREAK 32'h00100073

// J type inst
`define INST_TYPE_B 7'b1100011
`define INST_BEQ    3'b000
`define INST_BNE    3'b001
`define INST_BLT    3'b100
`define INST_BGE    3'b101
`define INST_BLTU   3'b110
`define INST_BGEU   3'b111

模块连接

RISC-V-CORE

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
module open_rsicv_top (
    input   wire          clk,          // Input clock signal, 1-bit
    input   wire          rst,          // Input reset signal, 1-bit
    input   wire  [31:0]  inst_i,       // Input instruction signal, 32-bit
    output  wire  [31:0]  inst_addr_o   // Output instruction address signal, 32-bit
);

    // Signals for pc_reg to ifetch
    wire [31:0] pc_out_pc_addr_i;      // PC address signal, 32-bit

    // Signals for ifetch to if_id
    wire [31:0] if_inst_o;             // Instruction signal from ifetch, 32-bit
    wire [31:0] if_inst_addr_o;        // Instruction address signal from ifetch, 32-bit

    // Signals for if_id to idecode
    wire [31:0] if_id_inst_o;          // Instruction signal from if_id, 32-bit
    wire [31:0] if_id_inst_addr_o;     // Instruction address signal from if_id, 32-bit

    // Signals for idecode to regs
    wire [4:0]  id_rs1_addr_o;         // rs1 address signal from idecode, 5-bit
    wire [4:0]  id_rs2_addr_o;         // rs2 address signal from idecode, 5-bit

    // Signals for regs to idecode
    wire [31:0] regs_reg1_rdata_o;     // rs1 data signal from regs, 32-bit
    wire [31:0] regs_reg2_rdata_o;     // rs2 data signal from regs, 32-bit

    // Signals for exe to regs
    wire [4:0]  ex_rd_addr_o;          // Write-back address signal from exe, 5-bit
    wire [31:0] ex_rd_data_o;          // Write-back data signal from exe, 32-bit
    wire        ex_reg_wen_o;          // Write-back enable signal from exe, 1-bit

    // Signals for idecode to id_ex
    wire [31:0] id_inst_o;             // Instruction signal from idecode, 32-bit
    wire [31:0] id_inst_addr_o;        // Instruction address signal from idecode, 32-bit
    wire [31:0] id_op1_o;              // Operand 1 signal from idecode, 32-bit
    wire [31:0] id_op2_o;              // Operand 2 signal from idecode, 32-bit
    wire [4:0]  id_rd_addr_o;          // Destination register address signal from idecode, 5-bit
    wire        id_reg_wen;            // Register write enable signal from idecode, 1-bit

    // Signals for id_ex to exe
    wire [31:0] id_ex_inst_o;          // Instruction signal from id_ex, 32-bit
    wire [31:0] id_ex_inst_addr_o;     // Instruction address signal from id_ex, 32-bit
    wire [31:0] id_ex_op1_o;           // Operand 1 signal from id_ex, 32-bit
    wire [31:0] id_ex_op2_o;           // Operand 2 signal from id_ex, 32-bit
    wire [4:0]  id_ex_rd_addr_o;       // Destination register address signal from id_ex, 5-bit
    wire        id_ex_reg_wen_o;       // Register write enable signal from id_ex, 1-bit

    // Instantiation of pc_reg module
    pc_reg pc_reg_inst (
        .clk(clk),                     // Connect to clock signal
        .rst(rst),                     // Connect to reset signal
        .pc_out(pc_out_pc_addr_i)      // Output PC address signal, 32-bit
    );

    // Instantiation of ifetch module
    ifetch ifetch_inst (
        .pc_addr_i      (pc_out_pc_addr_i),  // Input PC address signal, 32-bit
        .rom_inst_i     (inst_i),            // Input ROM instruction signal, 32-bit
        .if2rom_addr_o  (inst_addr_o),       // Output address signal to ROM, 32-bit
        .inst_addr_o    (if_inst_addr_o),    // Output instruction address signal, 32-bit
        .inst_o         (if_inst_o)          // Output instruction signal, 32-bit
    );

    // Instantiation of if_id module
    if_id if_id_inst (
        .clk         (clk),                 // Connect to clock signal
        .rst         (rst),                 // Connect to reset signal
        .inst_i      (if_inst_o),           // Input instruction signal, 32-bit
        .inst_addr_i (if_inst_addr_o),      // Input instruction address signal, 32-bit
        .inst_o      (if_id_inst_o),        // Output instruction signal, 32-bit
        .inst_addr_o (if_id_inst_addr_o)    // Output instruction address signal, 32-bit
    );

    // Instantiation of idecode module
    idecode idecode_inst (
        // Input signals from if_id module
        .inst_i      (if_id_inst_o),       // Input instruction signal, 32-bit
        .inst_addr_i (if_id_inst_addr_o),  // Input instruction address signal, 32-bit

        // Input signals from regs module
        .rs1_data_i  (regs_reg1_rdata_o),  // Input rs1 data signal, 32-bit
        .rs2_data_i  (regs_reg2_rdata_o),  // Input rs2 data signal, 32-bit

        // Output signals to regs module
        .rs1_addr_o  (id_rs1_addr_o),      // Output rs1 address signal, 5-bit
        .rs2_addr_o  (id_rs2_addr_o),      // Output rs2 address signal, 5-bit

        // Output signals to id_ex module
        .inst_o      (id_inst_o),          // Output instruction signal, 32-bit
        .inst_addr_o (id_inst_addr_o),     // Output instruction address signal, 32-bit
        .op1_o       (id_op1_o),           // Output operand 1 signal, 32-bit
        .op2_o       (id_op2_o),           // Output operand 2 signal, 32-bit
        .rd_addr_o   (id_rd_addr_o),       // Output destination register address signal, 5-bit
        .reg_wen     (id_reg_wen)          // Output register write enable signal, 1-bit
    );

    // Instantiation of regs module
    regs regs_inst (
        // Clock and reset signals
        .clk          (clk),                // Input clock signal, 1-bit
        .rst          (rst),                // Input reset signal, 1-bit

        // Read address signals from idecode module
        .reg1_raddr_i (id_rs1_addr_o),      // Input rs1 read address signal, 5-bit
        .reg2_raddr_i (id_rs2_addr_o),      // Input rs2 read address signal, 5-bit

        // Write-back signals from exe module
        .reg_waddr_i  (ex_rd_addr_o),       // Input write-back address signal, 5-bit
        .reg_wdata_i  (ex_rd_data_o),       // Input write-back data signal, 32-bit
        .reg_wen_i    (ex_reg_wen_o),       // Input write-back enable signal, 1-bit

        // Output read data signals to idecode module
        .reg1_rdata_o (regs_reg1_rdata_o),  // Output rs1 data signal, 32-bit
        .reg2_rdata_o (regs_reg2_rdata_o)   // Output rs2 data signal, 32-bit
    );

    // Instantiation of id_ex module
    id_ex id_ex_inst (
        // Clock and reset signals
        .clk         (clk),                 // Input clock signal, 1-bit
        .rst         (rst),                 // Input reset signal, 1-bit

        // Input signals from idecode module
        .inst_i      (id_inst_o),           // Input instruction signal, 32-bit
        .inst_addr_i (id_inst_addr_o),      // Input instruction address signal, 32-bit
        .op1_i       (id_op1_o),            // Input operand 1 signal, 32-bit
        .op2_i       (id_op2_o),            // Input operand 2 signal, 32-bit
        .rd_addr_i   (id_rd_addr_o),        // Input destination register address signal, 5-bit
        .reg_wen_i   (id_reg_wen),          // Input register write enable signal, 1-bit

        // Output signals to exe module
        .inst_o      (id_ex_inst_o),        // Output instruction signal, 32-bit
        .inst_addr_o (id_ex_inst_addr_o),   // Output instruction address signal, 32-bit
        .op1_o       (id_ex_op1_o),         // Output operand 1 signal, 32-bit
        .op2_o       (id_ex_op2_o),         // Output operand 2 signal, 32-bit
        .rd_addr_o   (id_ex_rd_addr_o),     // Output destination register address signal, 5-bit
        .reg_wen_o   (id_ex_reg_wen_o)      // Output register write enable signal, 1-bit
    );

    // Instantiation of exe module
    exe exe_inst (
        // Input signals from id_ex module
        .inst_i      (id_ex_inst_o),       // Input instruction signal, 32-bit
        .inst_addr_i (id_ex_inst_addr_o),  // Input instruction address signal, 32-bit
        .op1_i       (id_ex_op1_o),        // Input operand 1 signal, 32-bit
        .op2_i       (id_ex_op2_o),        // Input operand 2 signal, 32-bit
        .rd_addr_i   (id_ex_rd_addr_o),    // Input destination register address signal, 5-bit
        .reg_wen_i   (id_ex_reg_wen_o),    // Input register write enable signal, 1-bit

        // Output signals to regs module
        .reg_wdata_o (ex_rd_data_o),       // Output write-back data signal, 32-bit
        .reg_waddr_o (ex_rd_addr_o),       // Output write-back address signal, 5-bit
        .reg_wen_o   (ex_reg_wen_o)        // Output write-back enable signal, 1-bit
    );

endmodule

CORE-ROM-TOP

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module mini_riscv_soc (
    input wire clk,
    input wire rst,
    output wire led                          // led vivado rtl 必要有输出端口
);
    // open_risc_v to rom
    wire [31:0] open_risc_v_inst_addr_o;

    //rom to open_risc_v
    wire[31:0] rom_inst_o;
    assign led =1;

open_rsicv_top open_rsicv_top_inst (
    .clk        (clk),                      // Connect to clock signal, 1-bit
    .rst        (rst),                      // Connect to reset signal, 1-bit
    .inst_i     (rom_inst_o),               // Connect to instruction signal, 32-bit
    .inst_addr_o(open_risc_v_inst_addr_o)   // Connect to instruction address signal, 32-bit
);

rom rom_inst (
    .inst_addr_i (open_risc_v_inst_addr_o), // Connect to instruction address signal, 32-bit
    .inst_o      (rom_inst_o)               // Connect to instruction output signal, 32-bit
);


endmodule

仿真与仿真文件设计

指令文件设计

文件内容
1
2
3
4
00000111100000000000110110010011
00000101011100000000111000010011
00000001110011011000111010110011
01000001110011011000111100110011
指令解读

指令 1:

1
2
3
4
5
6
000001111000 00000 000 11011 0010011
opcode  =   0010011             //INST_TYPE_I指令类型
rd      =   11011               //x27
funct3  =   000                 //INST_ADDI指令
rs1     =   00000               //x0
imm     =   000001111000        //imm = 000001111000 --->120

指令 2:

1
2
3
4
5
6
000001010111 00000 000 11100 0010011
opcode  =   0010011             //INST_TYPE_I指令类型
rd      =   11100               //x28
funct3  =   000                 //INST_ADDI指令
rs1     =   00000               //x0
imm     =   000001010111        //imm = 000001010111 --->87

指令 3:

1
2
3
4
5
6
7
0000000 11100 11011 000 11101 0110011
opcode  =   0110011             //INST_TYPE_R_M指令类型
rd      =   11101               //x29
funct3  =   000                 //INST_ADD_SUB指令
rs1     =   11011               //x27
rs2     =   11100               //x28
funct7  =   0000000             //INST_ADD_SUB指令--->ADD

指令 4:

1
2
3
4
5
6
7
0100000 11100 11011 000 11110 0110011
opcode  =   0110011             //INST_TYPE_R_M指令类型
rd      =   11110               //x30
funct3  =   000                 //INST_ADD_SUB指令
rs1     =   11011               //x27
rs2     =   11100               //x28
funct7  =   0000000             //INST_ADD_SUB指令--->SUB

仿真文件设计

仿真文件内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
module mini_riscv_tb;

    reg clk;
    reg rst;

    always #10 clk = ~clk;

    initial begin
        clk <= 1'b1;
        rst <= 1'b0;
        #30;
        rst <= 1'b1;
    end

    //rom 初始化
    initial begin
        $readmemb("D:\\work\\code\\my_riscv\\mini_riscv\\tb\\inst_data_ADD_SUB.txt",mini_riscv_tb.mini_riscv_soc_inst.rom_inst.rom_mem);
    end

    initial begin
        while(1)begin
            @(posedge clk)
            $display("x27 register value is %d",mini_riscv_tb.mini_riscv_soc_inst.open_rsicv_top_inst.regs_inst.regs[27]);
            $display("x28 register value is %d",mini_riscv_tb.mini_riscv_soc_inst.open_rsicv_top_inst.regs_inst.regs[28]);
            $display("x29 register value is %d,this is ADD test!",mini_riscv_tb.mini_riscv_soc_inst.open_rsicv_top_inst.regs_inst.regs[29]);
            $display("x30 register value is %d,this is SUB test!",mini_riscv_tb.mini_riscv_soc_inst.open_rsicv_top_inst.regs_inst.regs[30]);
            $display("---------------------------");
            $display("---------------------------");
        end
    end

    mini_riscv_soc mini_riscv_soc_inst(
        .clk           (clk),
        .rst           (rst)
    );

endmodule
/*
    PS : D:\\work\\code\\my_riscv\\mini_riscv\\tb\\inst_data_ADD_SUB.txt 要换成自己的绝对路径
    mini_riscv_tb.mini_riscv_soc_inst.rom_inst.rom_mem 要根据自己的例化名字指定到rom的mem中
    下面的display 要换成自己的例化名字
 */

开箱测试

ModelSim

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

结果

modelsim

Vivado

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

结果

vivado

波形

wave

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

RTL 电路

top 模块

rtl_top

rom 模块

rom

riscv 模块

riscv

pc-reg 模块

pc-reg

if 模块

ifetch

if_id 模块

if_id

id 模块

idcode

id_ex 模块

id_ex

exe 模块

exe

regs 模块

regs

设计总结

希望大家能够喜欢,并且能够学习到设计 CPU 的思路,并逐渐提升设计 CPU 的能力。 若觉得本文对您产生了帮助,请转发给更多人,谢谢! 下面进入感谢环节!

感谢

感谢 外瑞罗格 UP 的教程与开源! 感谢 liangkangnan 的 RISC-V_tiny 项目的开源,这对我产生设计 RISC-V 的想法有了极大吸引!