RISC-V CPU课程设计报告【计算机组成原理课设】

博主在ujs大二完成的计算机组成原理课设,内容是RISC-V CPU设计。(当时也是做的快吐血了~~)

完成情况(写在前面)

    在本次计算机组成原理课程设计中,我完成一个基于RISC-V指令集架构的模型CPU。此CPU可以实现I型、R型、S型、B型、U型和J型指令。
    其中I型指令主要是将寄存器中的数据和生成的立即数进行运算,最终将结果存入寄存器中;此外,对于I型指令中的load指令,实现了将从数据存储器中去除的数据存入寄存器中。R型指令主要是将两个寄存器中的值进行运算并将结果存入寄存器中。S型指令主要是sw指令,实现将寄存器中的值送到指定的存储单元的功能,其中存储单元的获取采用的是变址寻址。在B型上,我主要实现了BEQ、BNE、BLT、BGE等指令,分别实现了相等,不相等,小于,大于等于时进行分支转移。最后,在拓展指令上,除了B型指令中的BNE、BLT、BGE指令,我还实现了U型指令中的LUI指令和AUIPC指令,以及L型指令中的JAL指令。LUI指令实现将20位立即数装入寄存器的高20位,低20位置0;AUIPC指令实现将装入任意偏移地址的主存字;最后,JAL指令实现了指令转移时的链接和跳转操作。
    本设计的亮点在于为了实现U型LUI、AUIPC指令和JAL指令的需要,在实验面板的数据通路上增加了相应的数据通路、加法器和多路选择器,符合了RISC指令集的特点和要求:通过增加数据通路中硬件的方式,来保证精简指令集能够不增加指令长度地实现。

摘要

    RISC-V是基于精简指令集计算(RISC)原理建立的开放指令集架构,其设计适用于小型、快速、低功耗的现实情景,且具有:①大多数指令在单周期完毕;②LOAD/STORE结构;③硬布线控制逻辑;④降低指令和寻址方式的种类;⑤固定的指令格式;⑥注重编译的优化的特点。
本次计算机组成原理的课程设计中主要实现了一个较为完整的基于RISC-V指令集的模型CPU,在实验中我理解、熟悉了单台RISC计算机的基本组成原理,掌握了计算机中数据调试方法、运算方法、运算器的组成、存储器系统的结构与功能等等。课设的设计和实验主要依靠Quartus Prime Lite 20.1软件和远程FPGA平台。
    在本次计算机组成原理课程设计中,我完成一个基于RISC-V指令集架构的模型CPU。此CPU可以实现I型、R型、S型、B型、U型和J型指令。
其中I型指令主要是将寄存器中的数据和生成的立即数进行运算,最终将结果存入寄存器中;此外,对于I型指令中的load指令,实现了将从数据存储器中去除的数据存入寄存器中。R型指令主要是将两个寄存器中的值进行运算并将结果存入寄存器中。S型指令主要是sw指令,实现将寄存器中的值送到指定的存储单元的功能,其中存储单元的获取采用的是变址寻址。在B型上,我主要实现了BEQ、BNE、BLT、BGE等指令,分别实现了相等,不相等,小于,大于等于时进行分支转移。最后,在拓展指令上,除了B型指令中的BNE、BLT、BGE指令,我还实现了U型指令中的LUI指令和AUIPC指令,以及L型指令中的JAL指令。LUI指令实现将20位立即数装入寄存器的高20位,低20位置0;AUIPC指令实现将装入任意偏移地址的主存字;最后,JAL指令实现了指令转移时的链接和跳转操作。

1 概述

1.1 设计目的

    1、理解RISC-V的基本指令(包括I型、R型、S型、B型、J型、U指令等等),并且掌握这些指令的运行原理和设计。
    2、熟练Verilog HDL语言的基本语法和模块化设计方法,并且能够使用VerilogHDL语言实现一个简单RISC-V模型CPU的设计。
    3、通过课程设计加深对计算机各功能部件的理解,掌握数据信息流和控制信息流的流动和实现过程,建立起整机概念。
    4、对所学的计算机硬件课程知识进行进一步地系统化,提高硬件设计和动手能力。
    5、培养科学研究的独立工作能力,取得工程设计雨组装调试的实践经验。

1.2 任务要求

    1、用Verilog HDL 语言完成对一个完整RISC-V模型CPU的设计。
    2、完成基本RISC-V单周期指令的设计:包括I型指令,store指令,load指令,R型指令,B型beq指令。
    3、拓展RISC-V单周期指令的设计:包括移位运算指令,比较指令,B型bne、blt、bge指令,U型lui和auipc指令,J型jal指令等。
    4、拓展实现支持5条指令的五级流水线RISC-V的设计。

1.3 实验环境

    1、个人计算机:64位win10操作系统,基于x64的处理器
    2、Verilog HDL程序编写软件:Quartus Prime 20.1 Lite Edition
    3、实验验证平台:江苏大学FPGA虚拟仿真平台

2 RISC-V概述

    RISC-V是一个基于精简指令集原则的开源指令集架构,由CISC复杂指令计算机发展演化而来,2010年始于加州大学伯克利分校的一个项目。针对于CISC指令集日趋庞杂且不但不易实现,还可能降低系统性能的弊病,RISC-V有着“精简指令”的核心设想,即指令系统应当只包含那些使用频率很高的少量指令,并提供一些必要的指令以支持操作系统和高级语言。RISC-V指令集的设计中,主要考虑了小型、快速、低功耗的现实情况的实现,但并没有对特定的微架构做过度的设计。
    RISC-V主要有以下特点:1.完全开源;2.采用精简的指令集。3.大量的通用寄存器;4.多级的指令流水线;5.采用硬布线控制。
    计算机指令是能够被计算机识别并执行的二进制代码,它规定了计算机能完成的某种操作。计算机指令通常由两部分操作:操作码和操作数(地址码):

操作码 地址码

    RISCⅤ指令集采用了基本整数指令集和标准扩展指令集,设计中使用了RV32I基本指令集。指令长度可以扩展。4种基本指令格式:R型,I型,S型,U型;2种立即数编码变形指令格式:B型,J型。
    其中在本次实验中主要设计了R型,I型,S型,B型,U型和J型这几种指令:

2.1 R型指令

    R型指令表示寄存器与寄存器之间的运算操作。
    指令的组成依次是funct7、rs2、rs1、funct3、rd、opcode,总共是32位。其中opcode有7位,用于判断是什么类型的指令,其中R型指令的opcode是0110011。funct7有7位,funct3有3位,funct7和funct3共同用于判断指令是什么类型的运算(ADD,SUB、XOR、OR、AND)。rs1和rs2表示源操作数,rd表示目的操作数。
    其指令格式如下图2-1:
图2-1
    (本课程设计中,rs2,rs1分别代表两个读寄存器的号码,rd代表写寄存器的号码)

2.2 I型指令

    I型指令表示寄存器与立即数之间的运算操作。
    指令的组成依次是imm、rs1、funct3、rd、opcode,总共是32位。其中opcode有7位,用于判断是什么类型的指令,其中I型指令的opcode是0010011。funct3有3位,专用funct3来判断指令是什么类型的运算(ADDI、XORI、ORI、ANDI)。rs1表示源操作数,rd表示目的操作数。
    其指令格式如下图2-2:
图1-2
    (本课程设计中,rs1为寄存器的号码,rd代表写寄存器的号码)
    I型指令有一个特殊的指令,也就是Load指令。Load指令的opcode是0000011,funct3位为010,用于完成将立即数送到目的寄存器中。
    Load指令:lw rd offset(rs1)
    offset(rs1)表示的地址是 M[sext[offset] + x[rs1]] 也就是将offset符号扩展到32位然后加上寄存器x[rs1]的值,找到内存中的该地址取得一个字(w)也就是32位,存储到寄存器x[rd]中。
    其指令格式如下图2-3:
图2-3
    (本课程设计中,rs1为寄存器的号码,rd代表写寄存器的号码)

2.3 S型指令

    S型指令中的sw表示将寄存器中的值送到相对应的存储单元中,存储单元寻址采用变址寻址。
    sw指令的组成依次是imm1、rs2、rs1、funct3、imm2、opcode,总共是32位。其中opcode有7位,用于判断是S型的指令,其中S型指令的opcode是0100011。funct3有3位,专用funct3来区分S型中的不同指令,010表示store类指令。rs1表示基址寄存器,其中存储的是变址寻址的基址,rs2是源寄存器,其中存储的是源操作数。Imm1和imm2组合起来表示立即数,立即数作为偏移量。
    其指令格式如下图2-4:
图2-3

2.4 B型指令

    B型指令表示的是分支转移指令,满足条件后,执行pc跳转,采用相对寻址。
    B指令的组成依次是imm1[12]、imm[10:5]、rs2、rs1、funct3、imm[4:1],imm[11]、opcode,总共是32位。其中opcode有7位,用于判断是否是B型的指令,其中B型指令的opcode是1100011。funct3有3位,专用funct3来区分B型中的不同指令(BEQ、BNE、BLT、BGE、BLTU、BGEU)。rs1、rs2均表示源寄存器表示基址寄存器,具体就是把rs1和rs2进行比较,如果满足条件,则进行相对寻址。Imm[12:0]组合起来表示立即数,立即数作为偏移量,Imm[0]默认是0。
    其指令格式如下图2-5:
图2-5

2.5 U型指令

    考虑到I型、S型、B型指令均只用了立即数的12位,属于小立即数,立即数的表示范围较小;U型指令采用的是立即数的31到12位(即高20位),将立即数的表示范围大大增加。
    B指令的组成依次是imm1[31:12]、rd、opcode,总共是32位。其中opcode有7位,用于判断是否是U型的指令。在本课程设计中,用于LUI指令的opcode为0110111,用于AUIPC指令的opcode为0000111。rd表示写入寄存器的号码,LUI指令将20位立即数装入rd寄存器的高20位,且将其低12位清0;AUIPC指令将20位立即数作为数据的高20位,低12位清0后,将此数据与指令计数器PC中的数值相加后装入rd寄存器,实现了装入任意偏移地址的贮存字的功能。
    其指令格式如下图2-6:
图2-6

2.6 J型指令

    相较于B型指令的有条件转移,J型指令主要用于无条件操作,且其立即数区别前面的指令,采用的是低20位立即数。
    J型指令的组成依次是imm1[20]、imm[10:1]、imm[11]、imm[19:12]、rd、opcode,总共是32位。其中opcode有7位,用于判断是否是B型的指令,在本课程设计中,JAL指令的opcode为0000111。JAL指令实现的功能主要分为两块:一是链接功能,将PC中的数据加4后存入目的寄存器中,即将下一条指令的地址保存到rd寄存器中;二是跳转功能,将立即数与源寄存器rs1中的数据相加后存入程序计数器PC中。Imm[20:0]组合起来表示立即数,立即数作为偏移量。
    其指令格式如下图2-7:
图2-7

3 单周期RISC-V设计与验证

3.1 起点:只有一条addi指令的RISC-V

    addi指令本质上是I型指令的一种,其数据通路主要是由:加法器、程序计数器PC、指令存储器、寄存器堆、运算器ALU组成。
    addi指令实现的功能是将读寄存器中的数与立即数相加,然后将得到了结果数值再存入写寄存器。

(1)立即数生成模块

    首先要得到I型指令addi的立即数,由立即数生成模块实现:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  i_imm = { {20{instr[31]}}, instr[31:20] };  //I型:补码的拓展方式 ,制造出一个32位立即数

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第31到20位是立即数位。需要立即数是32位,又因其是用补码表示,所以拓展立即数需要在高20位和该立即数的最高位一致。
    模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用32{iI_type}} & i_imm来控制立即数按照此种指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
   assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
   assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)                    | ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

    然后就是控制信号的生成模块,在I型的addi指令中,需要生成的控制信号主要只有寄存器堆的oRegwrite信号,另外还有立即数的类型。这两个是通过iopcode控制的,也就是指令中的opcode,当位I型指令的opcode时候:

  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0010011: controls <= 13'b00100_11_00001_1; // I-TYPE

表示立即数的类型是I型。

(3)ALU控制信号生成(ALU译码器)模块

module AluDecoder (
    input logic [1:0] iALUop,
    input logic [6:0] iFunct7,
    input logic [2:0] iFunct3,
    output logic [3:0] oALUctrl  //M0,S1,S0);
case(iALUop) 
            2'b00:   //加法 load,store
                oALUctrl <= ADD;

    ALU译码器模块的主要任务是生成ALUctrl信号传输给运算器ALU,使ALU做出对应此指令的运算。在addi指令中,ALU的任务是将读寄存器中的数据和立即数相加,故ALUctrl信号只需控制ALU做出加法运算。

    在虚拟平台的实现如下图3-1所示:
    汇编指令是:addi x18,x0,6
    对应的机器指令是:00600913
    如图所示,将0号寄存器和立即数6相加后,将结果存入18号寄存器。
RISC-V CPU课程设计报告【计算机组成原理课设】_第1张图片

3.2 实现Ⅰ型运算指令

    因为上一addi指令本质上就是I型指令的一种,故在进行I型指令设计的时候,立即数生成模块延续了addi指令的立即数生成模块。
    因为I型指令能够实现多种功能,如addi、sub、xori、ori等等,所以对应不同的功能,运算器ALU要做出相应的操作,所以要产生不同的主控制信号和ALU控制信号。此外,在数据通路上,亦需要增加新的通路和加多路选择器。

(1)立即数生成模块

    首先要得到I型指令addi的立即数,由立即数生成模块实现:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  i_imm = { {20{instr[31]}}, instr[31:20] };  //I型:补码的拓展方式 ,制造出一个32位立即数

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第31到20位是立即数位。需要立即数是32位,又因其是用补码表示,所以拓展立即数需要在高20位和该立即数的最高位一致。
    模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用32{iI_type}} & i_imm来控制立即数按照此种指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
   assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
   assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)                    | ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

    然后就是控制信号的生成模块,在I型的addi指令中,需要生成的控制信号主要只有寄存器堆的oRegwrite信号,另外还有立即数的类型。这两个是通过iopcode控制的,也就是指令中的opcode,当位I型指令的opcode时候:

  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0010011: controls <= 13'b00100_11_00001_1; // I-TYPE

    表示立即数的类型是I型。

(3)ALU控制信号生成(ALU译码器)模块

  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0010011: controls <= 13'b00100_11_00001_1; // I-TYPE

    ALU译码器模块的主要任务是生成ALUctrl信号传输给运算器ALU,使ALU做出对应此指令的运算。在I型指令中,ALU需要根据不同的指令做出不同的操作,而这不同操作正是依靠指令中的funct3来进行区分的。在addi,andi,ori,xori指令中,ALUctrl信号分别实现控制ALU进行add,and,or,xor的操作。
    此外,在ALU中新增了S1、S0控制信号,来判断运算的类型。还有就是增加了标志位的判断,sign、zero、overflow、carryout标志位的判断。

    在虚拟平台的演示如下图3-2所示:
    执行的汇编语言是:addi x18,x0,5;机器指令是:00500913
    最后需要加上几个多路选择器,能执行更多的指令。
    可以看到,立即数的输出是5,然后多路选择器选择立即数,立即数和寄存器x0出来的0相加,最后将结果送到寄存器x18。
    大概的流程就是:首先pc从指令存储器中去出指令,从指令中可生成立即数,然immtoalu为1,选择立即数生成的部分,然后和RD1中输出的寄存器中的值一起送到alu中,memtoreg为0,选择alu的模块,运算后将数据回送到寄存器中。
RISC-V CPU课程设计报告【计算机组成原理课设】_第2张图片

3.3 实现store指令

    S型指令主要实现的是Store指令,实现将寄存器中的数据存入指定的数据存储器单元中。

(1)立即数生成模块

    首先要得到Store指令addi的立即数,由立即数生成模块实现:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  s_imm = {instr[31:25],instr[11:7]};     //STORE

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第31到25和11到7位是立即数位。
    模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用{32{iS_type}} & s_imm来控制立即数按照S型指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
   assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
   assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)                    | ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

    然后就是控制信号的生成模块,在Store指令中,需要生成的控制信号主要是写数据存储器的MemWhrite信号,另外还有立即数的类型。这两个是通过iopcode控制的,也就是指令中的opcode,当为S型指令的opcode时候:

  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0100011: controls <= 13'b00101_00_00010_0; // Store

    表示立即数的类型是S型。

(3)ALU控制信号生成(ALU译码器)模块

module AluDecoder (
    input logic [1:0] iALUop,
    input logic [6:0] iFunct7,
    input logic [2:0] iFunct3,
    output logic [3:0] oALUctrl  //M0,S1,S0);
case(iALUop) 
            2'b00:   //加法 load,store
                oALUctrl <= ADD;

    ALU译码器模块的主要任务是生成ALUctrl信号传输给运算器ALU,使ALU做出对应此指令的运算。在Store指令中,ALU的任务是将读寄存器中的数据和立即数相加,求得要写入的内存单元的地址,故ALUctrl信号只需控制ALU做出加法运算。
    在虚拟平台的演示如下图3-3所示:
RISC-V CPU课程设计报告【计算机组成原理课设】_第3张图片

    完成的汇编指令是:sw x18,8(x0)
    其机器指令是:01202423
    具体流程:首先在指令存储器中取出指令,由指令生成立即数,把这个立即数作为偏移量。然后从寄存器堆中取出基准寄存器的值,将立即数和基准寄存器的值送到alu中,将运算结果送到数据存储器的A端口,也就是地址端口。同时将寄存器堆中指定寄存器送到存储器相应地址单元。

3.4 实现load指令

    Load指令主要实现的功能是将数据存储器中的指定存储单元的数据存入指定的寄存器中。

(1)立即数生成模块

    Load指令的立即数生成模块与I型指令的相同:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  i_imm = { {20{instr[31]}}, instr[31:20] };  //I型:补码的拓展方式,制造出一个32位立即数

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第31到20位是立即数位。
    模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用{32{iI_type}} & i_imm来控制立即数按照S型指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
   assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
   assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)                    | ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

    然后就是控制信号的生成模块,在Load指令中,需要生成的控制信号主要是写寄存器的RegWhrite信号和控制数据输出给寄存器的MenToReg信号,另外还有立即数的类型。这两个是通过iopcode控制的,也就是指令中的opcode,当为S型指令的opcode时候:

  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0000011: controls <= 13'b00110_00_00001_1;  //load

(3)ALU控制信号生成(ALU译码器)模块

module AluDecoder (
    input logic [1:0] iALUop,
    input logic [6:0] iFunct7,
    input logic [2:0] iFunct3,
    output logic [3:0] oALUctrl  //M0,S1,S0);
case(iALUop) 
            2'b00:   //加法 load,store
                oALUctrl <= ADD;

    ALU译码器模块的主要任务是生成ALUctrl信号传输给运算器ALU,使ALU做出对应此指令的运算。在Load指令中,ALU的任务是将读寄存器中的数据和立即数相加,求得要读出的内存单元的地址,故ALUctrl信号只需控制ALU做出加法运算。
    在虚拟平台的演示如下图3-4所示:
RISC-V CPU课程设计报告【计算机组成原理课设】_第4张图片

    这里的汇编代码是:lw x19,8(x0);其机器指令是:00802983。
    其主要流程就是:首先在指令存储器中取出指令,由指令生成立即数,把这个立即数作为偏移量。然后从寄存器堆中取出基准寄存器的值,将立即数和基准寄存器的值送到alu中,将运算结果送到数据存储器的A端口,也就是地址端口。然后就是选中该地址单元,将该地址单元的内容回送到指定寄存器中去。

3.5 实现R型运算指令

    Load指令主要实现的功能是完成寄存器与寄存器之间的运算,并将结果存入指定寄存器中。

(1)立即数生成模块:

    由于R型指令只涉及两个寄存器中数据的运算,不用到立即数,故R型指令不要求立即数生成模块。

(2)控制信号生成模块(主译码器模块)

    控制信号的生成模块,在R型指令中,需要生成的控制信号主要是写寄存器的RegWrite信号和控制数据输出给寄存器的MemToReg信号,另外还有立即数的类型。这两个是通过iopcode控制的,也就是指令中的opcode,当为R型指令的opcode时候:

  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0110011: controls <= 13'b00000_10_00000_1;  //R-Type

(3)ALU控制信号生成(ALU译码器)模块

module AluDecoder (
    input logic [1:0] iALUop,
    input logic [6:0] iFunct7,
    input logic [2:0] iFunct3,
    output logic [3:0] oALUctrl  //M0,S1,S0);
case(iALUop) 
                    2'b10: //R
                case(iFunct3)
                3'b000:
                    case(iFunct7)
                    7'b0000000:oALUctrl <= ADD;
                    7'b0100000:oALUctrl <= SUB;
                    default:oALUctrl <= 3'bxxx;
                    endcase
                3'b100:oALUctrl <= XOR;
                3'b110:oALUctrl <= OR;
                3'b111:oALUctrl <= AND;
                default:oALUctrl <= 3'bxxx;
                endcase

     ALU译码器模块的主要任务是生成ALUctrl信号传输给运算器ALU,使ALU做出对应此指令的运算。在R型指令中,ALU需要根据不同的指令做出不同的操作,而这不同操作正是依靠指令中的funct3和funct7共同来进行区分的。在add,and,or,xor指令中,ALUctrl信号分别实现控制ALU进行add,and,or,xor的操作。

    在虚拟平台的演示如下图3-5所示:
RISC-V CPU课程设计报告【计算机组成原理课设】_第5张图片

    这里实现的汇编代码是:or x20,x18,x19;对应的机器指令是:01396A33;
    大概执行流程是:首先pc取指令,然后由取得的指令找到两个源寄存器x18,x19,将这两个寄存器的内容送到alu中,immtoalu为0,表示将RD2的值送到alu中,aluctrl为0010,表示执行alu或运算。然后将运算的结果送给寄存器堆,送给目的寄存器x20。

3.6 实现B型指令

    B型是分支转移指令,也就是当满足条件的时候发生偏转,吓一跳指令的地址即pc的值就不再是pc+4,而是pc=pc+偏移量。

(1)立即数生成模块

    Load指令的立即数生成模块与I型指令的相同:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  b_imm = {{20{instr[31]}},instr[7],instr[30:25],instr[11:8],1'b0};   //B型指令的立即数

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第7、30到25、11到8是立即数位。此外,这里立即数的最低位省略了,默认最低位是0,所以B型指令的立即数有13位,需要从第0位拼接到第12位。不足的高位补第13位的数。
模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用{32{iB_type}} & b_imm来控制立即数按照S型指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
 assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
 assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)| ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

然后就是控制信号的生成模块,在Load指令中,需要生成的控制信号主要是使转移地址的偏移量能够与之前地址的值相加的PCjump信号,另外还有立即数的类型。这两个是通过iopcode控制的,也就是指令中的opcode,当为B型指令的opcode时候:

 assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b1100011: controls <= 13'b01000_01_00100_0;  //beq

(3)ALU控制信号生成(ALU译码器)模块

module AluDecoder (
    input logic [1:0] iALUop,
    input logic [6:0] iFunct7,
    input logic [2:0] iFunct3,
    output logic [3:0] oALUctrl  //M0,S1,S0);
case(iALUop) 
       2'b01: oALUctrl <= SUB;  //减法 beq,bne

    ALU译码器模块的主要任务是生成ALUctrl信号传输给运算器ALU,使ALU做出对应此指令的运算。在B型指令中,唯一需要做运算的地方是PC前的一个加法器,而运算器ALU并不需要做任何运算操作,故这里将ALU置为减法运算的状态(但实际上不进行减法操作)。
    此外,所有的条件跳转都是B类型的指令,B型指令主要有以下几种。
    BEQ and BNE分别当rs1和rs2相等和不相等时,转移;否则就不转移。
    BLT and BLTU 分别是有符号数比较和无符号数比较,当rs1 < rs2时候就转移;否则不转移。
    BGE and BGEU 分别是有符号数比较和无符号数比较,当rs1>=rs2时候就转移;否则不转移。

    在虚拟平台的演示如下图3-6所示:
RISC-V CPU课程设计报告【计算机组成原理课设】_第6张图片

    该beq汇编指令是:beq x19,x20,-16;相对应的机器指令是:FF4988E3;
    具体执行的过程是:pc取指令,从指令中生成立即数。然后从寄存器堆中取出RD1、RD2,把两者送给alu,进行减法运算,当标志位的zero为1的时候,发生偏移。将立即数送给指令加法器,完成pc指令的加法。

3.7 U型指令

    U型指令主要实现将大立即数装入寄存器的功能。

(1)立即数生成模块

    Load指令的立即数生成模块与I型指令的相同:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  u_imm = {{instr[31:12], {12{1'b0}}}};   //U-Type

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第31到12位是立即数位。此外,立即数的低十二位均用0补全。
    模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用{32{iU_type}} & u_imm来控制立即数按照S型指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
 assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
 assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)| ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

    ①LUI指令实现将大立即数装入寄存器的功能,故而在LUI指令中,需要生成的控制信号主要是ImmToALU和RegWrite,以及立即数的类型信号。在本课程设计中LUI指令采用的opcode为0110111。

 assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0110111: controls <= 13'b00100_00_01000_1;  //lui  

    ②AUIPC指令实现将程序计数器PC中的地址与大立即数相加后存入寄存器中。故而在原有数据通路的基础上,需要增加由pc到寄存器的数据通路和相应的多路选择器。如下图所示:
RISC-V CPU课程设计报告【计算机组成原理课设】_第7张图片

    其对应控制信号有:控制偏移量与pc相加的PCjump信号和控制寄存器输入来自pc的数据的savePC信号。在本课程设计中LUI指令采用的opcode为0010111。
在虚拟平台的演示如下图3-7-1和3-7-2所示:
如图3-7-1中:执行的是lui x10,87654指令,具体执行的过程是:pc取指令,从指令中生成立即数。然后直接将立即数的内容送到ALU,并继而将数据存入寄存器x10中。(注:由于我用的自己做的jvp面板,显示的绿色数据通路发生错位)
RISC-V CPU课程设计报告【计算机组成原理课设】_第8张图片
    如图3-7-2中:执行的是auipc x5,87654指令,具体执行的过程是:pc取指令,从指令中生成立即数。然后无条件发生转移,将立即数与原来pc中的值相加,并将相加后的结果存入寄存器x5中去。(注:由于我用的自己做的jvp面板,显示的绿色数据通路发生错位)
RISC-V CPU课程设计报告【计算机组成原理课设】_第9张图片

3.8 J型指令

    J型指令亦是实现地址转移的功能,但是在执行J型指令时还将原来pc的值保存入寄存器中,实现链接的功能。

(1)立即数生成模块

    Load指令的立即数生成模块与I型指令的相同:

module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  j_imm = { {instr[31]},{instr[19:12]},{instr[20]},{instr[30:21]},{1'b0},{12{1'b0}} };

    如上代码所示,在此立即数生成模块中,“iInstruction“表示输入的32位的指令,其中第31到12位是立即数位。此外,立即数的低十二位均用0补全。
    模块最后部分,通过输入的iImm_Type信号来表示此指令为何种指令,从而利用{32{iJ_type}} & j_imm来控制立即数按照S型指令要求的格式输出。

 wire iJ_type, iU_type, iB_type, iS_type, iI_type;
 assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
 assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)| ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);

(2)控制信号生成模块(主译码器模块)

    JAL指令实现的功能:①链接:将下一条指令的地址保存到rd寄存器中;②:跳转:PC <- (PC)+立即数。故而在JAL指令中,需要生成的控制信号主要是PCjump、RegWrite、savePC,以及立即数的类型信号。在本课程设计中JAL指令采用的opcode为0000111。

 assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;
case(iOpcode)
      7'b0110111: controls <= 13'b00100_00_01000_1;  //lui 

    类似于auipc指令,jal指令需要将程序计数器PC中的地址与大立即数相加后存入寄存器中。故而在原有数据通路的基础上,需要增加由pc到寄存器的数据通路和相应的多路选择器。如下图所示:
RISC-V CPU课程设计报告【计算机组成原理课设】_第10张图片
    在虚拟平台的演示如下图3-8所示:
    如图3-8中:执行的是jal x5, 87654指令,具体执行的过程是:pc取指令,从指令中生成立即数。然后无条件发生转移,将立即数与原来pc中的值相加,以此作为下一条指令的地址;与此同时,将原来的下一条指令的地址pc存入寄存器x5中去。(注:由于我用的自己做的jvp面板,显示的绿色数据通路发生错位)
RISC-V CPU课程设计报告【计算机组成原理课设】_第11张图片

3.9 遇到的问题与解决方法

    1、R型指令设计完成以后,当我在远程fpga平台上实验时,发现数据通路上的数值显示是正确的,但相应的信号却出现错误显示(主要是该为1的信号却没有显示出来)。我首先想到的原因就是可能是在主译码器模块产生相应的控制型号或cpu模块中相应信号的流通部分出了错,于是回过头去反复检查主译码器模块和cpu模块中相应信号的流通部分。但是不管怎么检查,都没有发现错误。后来我回头一想,如果问题出在控制信号的产生和流通部分,那么整个指令的执行会出现根本性的错误,由此数据通路上的数据应该是错误的显示;但如今数据通路上的数据是正确的。故而不可能是控制信号的产生的问题,所以只可能是使信号显示在jvp上的扫描链部分出了问题。经过我的检查,发现问题出在没有更新扫描链的位宽。由于我做新指令时增加了新的控制信号到扫描链中,但是扫描链的位宽却没有做出相应的增加,导致新增加的信号默认赋值为0。即下列代码中SIZEWS的部分没有及时更新。

localparam SIZEWS = 15;   
wire [SIZEWS-1:0] WS;       
assign WS = { cSavePC, cPCjump, cImmtoALU, cMemtoReg,cMemWrite,cRegWrite,cALUctrl[3:0],cImm_type[4:0]};

    2、关于B指令的扩展beq、blt、bge这几个B型指令,由于这几条指令共用B型指令的opcode,它们的区分是依靠funct3实现的,怎么把这beq、blt、bne、bge这四条指令的区分巧妙地实现。后来我发现可以通过与或门来巧妙地实现这些指令的区分。首先beq必须是运算后标志位zero为0,且funct3为000;bne是运算后标志位zero不为0,且funct3必须为001;blt是运算后标志位sign为1,说明相减小于0,且funct3必须是100;bge是运算后标志位carrout或zero为1,因为bge表示大于等于时发生偏转,且funct3必须为101。对应代码如下:

assign cPCjump = (PCjump & zero)&(funct3 == 3'b000)|(PCjump & ~zero)&(funct3 == 3'b001)|(PCjump & sign)&(funct3 == 3'b100)|(PCjump & (carryOut|zero))&(funct3==3'b101);

    3、由于我设计的指令中存在auipc和jal这样需要把数据从pc传输到寄存器的指令,故而需要在数据通路上做出修改,主要是在pc和寄存器之间加数据通路和多路选择器。在jal指令中,我遇到了困惑。Jal指令实现的功能是:①链接:把下一条指令地址存入寄存器中;②转移:把偏移量与pc相加后作为指令的地址。因为下一条指令的地址是pc+4,故而我很自然地想到可以直接把pc的输入端传输给寄存器,但是因为此指令还实现将pc与偏移量相加的功能,故这里的地址不是pc+4而是pc+偏移量了。后来,我想到的解决方法是将PC的输出传输给寄存器,并且在中间增加一个加法器,加法器的一端是pc,另一端则是常量4。如下图所示:

RISC-V CPU课程设计报告【计算机组成原理课设】_第12张图片

3.10 设计的创新性

    本实验的创新点主要有二:
    1、B型指令中巧妙地用一行代码实现四个B型指令的区分:(上面已详细讲述)

assign cPCjump = (PCjump & zero)&(funct3 == 3'b000)|(PCjump & ~zero)&(funct3 == 3'b001)|(PCjump & sign)&(funct3 == 3'b100)|(PCjump & (carryOut|zero))&(funct3==3'b101);

    2、由于我设计的指令中存在auipc和jal这样需要把数据从pc传输到寄存器的指令,故而需要在数据通路上做出修改,主要是在pc和寄存器之间加数据通路和多路选择器。(上面已详细讲述)
    ①auipc对应数据通路图:
RISC-V CPU课程设计报告【计算机组成原理课设】_第13张图片
    ②jal对应数据通路图:
RISC-V CPU课程设计报告【计算机组成原理课设】_第14张图片

4 总结

4.1 设计总结

    这次课程设计主要是把本学期的几次cpu实验完成的部分作为各个模块组合起来,最终实现一个较完整功能的模型CPU。根据冯诺依曼结构的思想,计算机系统的结构应当是由:存储器、控制器、运算器和输入、输出设备组成。本次计算机组成原理课程设计完成的一个cpu系统基本上就是按照这个思想组合各硬件的,由于考虑到复杂性,本次课设仅实现了cpu系统最核心的三个部分:存储器、控制器、运算器。
    在远程FPGA平台上实验操作时,当我用自己制作的jvp面板操作实验,会出现绿色数据通路指引错位的情况,希望平台开发者能够把数据通路指引的功能也交给用户设计。

4.2 心得体会

    这次实验花了许多时间,也熬了不少夜,主要还是因为一些原理的理解和模块的整合思想需要花费不少精力。
    但是,收获是非常多的,经过这次实验,我了解了如何用 Verilog HDL设计一个具备完整功能的模型CPU,并且亲身实践地完成了,中途遇到好多好多困难,有时候脾气会非常暴躁,但是当我做完了整个 CPU,并且在远程FPGA平台上成功测试了所要求的各种指令,那种成就感是非常强的。
    经过本次实验,然我进一步理解了RISC-V架构的CPU的数据通路,以及各类控制信号,还有基于RISC-V架构的CPU指令的含义,当然还有Verilog硬件描述语言该如何写,它是并行执行的一种语言,与我们传统的C++有着本质区别,与其说是一门语言,更像是一种硬件设计的描述手段。
    当然,这次实验还是有些遗憾的,比如没有把寄存器地址进行划分,以及没有初始化数据寄存器,也没有能够挑战一下流水线的设计。

5 参考文献

    1. 中国RISC-V社区 https://www.china-riscv.com/
    2. RISC-V指令集手册 https://fmrt.gitbooks.io/riscv-spec-v2-cn/content/

6 附录:设计源代码

每一个模块放在一个Verilog文件中。

6.1 CPU模块(核心)

`default_nettype none 
module CPU
 #(
     parameter DATAWIDTH = 32,
     parameter ADDRWIDTH = 32
 )
(
    input  wire iCPU_Reset,
    input  wire iCPU_Clk,
    input  wire [DATAWIDTH-1:0] iReadData,
    output wire [DATAWIDTH-1:0] oWriteData,
    output wire [ADDRWIDTH - 1: 0] oAB,
    output wire oWR,
    // 连接调试器的信号
    output wire [ADDRWIDTH-1:0] oIM_Addr,   //输出给外面指令存储器的取指令地址(pc)
    input  wire [DATAWIDTH-1:0] iIM_Data,   //输入进来给cpu的指令
    output wire [ADDRWIDTH-1:0] oCurrent_PC,
    output wire oFetch,
    input  wire iScanClk,
    input  wire iScanIn,
    output wire oScanOut,
    input  wire [1:0] iScanCtrl
);

  /** The input port is replaced with an internal signal **/
  wire   clk   = iCPU_Clk;
  wire   reset = iCPU_Reset;

  //1、Instruction parts
  logic [31:0] pc, nextPC, offsetPC;    //pc:pc寄存器的输出。nextpc: 即pc的输入。offsetpc为pc增加的值
  logic [31:0] instr_code;          // 指令
  logic cPCjump, PCjump;
  logic [31:0] PCtoReg ;
  assign offsetPC = (cPCjump == 1) ? immData : 4;              /*- 仅支持PC+4,增加分支转移指令时需修改 -*/
  assign nextPC = pc + offsetPC;

  DataReg #(32) pcreg(.iD(nextPC), .oQ(pc), .Clk(clk), .Load(1'b1), .Reset(reset));    //32位pc指令寄存器

  assign oIM_Addr = pc;     //pc传出去给指令存储器
  assign instr_code = iIM_Data;     //外面的指令存储器传进来给cpu
  assign PCtoReg = (op == 7'b0000111 ) ? pc + 4'b0100 : nextPC;

 //下面时R型指令的格式(指令从指令存储器模块来,指令存储器在外面模块)
  logic [6:0] funct7;
  logic [4:0] ra2;
  logic [4:0] ra1;
  logic [2:0] funct3;
  logic [4:0] wa;
  logic [6:0] op;
  assign funct7 = instr_code[31:25];
  assign ra2    = instr_code[24:20];
  assign ra1    = instr_code[19:15]; 
  assign funct3 = instr_code[14:12];
  assign wa     = instr_code[11:7];
  assign op     = instr_code[6:0];
  
  //这些暂时不用:
  
  //2、主译码器
  logic cRegWrite;
  logic cMemtoReg;
  logic cMemWrite;
  logic cImmtoALU;
  logic [4:0] cImm_type;  //J, U, B, S, I type(立即数是哪种类型的?类型对应的位置为1,其余为0)
  logic [1:0] cALUop;
  logic cSavePC;
  MainDecoder mainDecoder(
    .iOpcode(op),
    .oALUop(cALUop),
    .oImm_type(cImm_type),
    .oRegWrite(cRegWrite),
    .oMemWrite(cMemWrite),
    .oMemtoReg(cMemtoReg),
    .oImmtoALU(cImmtoALU),
    .oPCjump(PCjump),
    .oSavePC(cSavePC)
  );
  //相等(beq:000),不相等 (bne:001),大于等于(bge:101),小于(blt:100)
  
  assign cPCjump = (op == 7'b0000111 | op == 7'b0010111) ? PCjump : 
  (PCjump & zero)&(funct3 == 3'b000)|(PCjump & ~zero)&(funct3 == 3'b001)
						|(PCjump & sign)&(funct3 == 3'b100)|(PCjump & (carryOut|zero))&(funct3==3'b101);
  
  
  //3、Alu译码器
    logic [3:0] cALUctrl;
  AluDecoder aluDecoder(
      .iALUop(cALUop),
      .iFunct7(funct7),
      .iFunct3(funct3),
      .oALUctrl(cALUctrl)
  );

  //4、Immediate number generation 立即数生成单元
  logic [31:0] immData;
  ImmeGen  immGen(.iInstruction(instr_code), 
  .iImm_type(cImm_type), .oImmediate(immData));

  //5、Register file 寄存器堆
  logic [31:0] regWriteData, regReadData1, regReadData2, wd, MDO, pcwd;
  
  assign pcwd = (cSavePC)? PCtoReg : regWriteData;
  
  RegisterFile regFile(.Clk(clk), 
  .iWE(cRegWrite), .iWA(wa), .iWD(pcwd), 
    .iRA1(ra1), .oRD1(regReadData1), 
    .iRA2(ra2), .oRD2(regReadData2));
	 
   assign regWriteData = cMemtoReg? memReadData:aluOut ; /*- 仅支持将ALU运算结果写入寄存器堆,需修改 -*/

  //6、ALU 
  logic [31:0] alu_y;
  logic [31:0] aluOut;
  logic sign,zero,overflow,carryOut;           
  assign alu_y = (cImmtoALU)?immData : regReadData2;  /*- 仅支持立即数作为ALU的Y输入,需修改 -*/
  logic [3:0] flag;
  ALU #(32) alu (
      .iX(regReadData1),
      .iY(alu_y),
      .iALUctrl(cALUctrl),
      .oF(aluOut),
      .oFlag(flag)
  );
   assign {sign,zero,overflow,carryOut} = flag; 
 
 //7、data memory

 logic [31:0] memReadData;
 assign oWR = cMemWrite;
 assign oAB = aluOut;
 assign oWriteData = regReadData2;
 assign memReadData = iReadData;

 assign wd = regWriteData;
 assign MDO = memReadData; 

//-----------------------送给调试器的信号-------------------------//
    assign oCurrent_PC = pc;
    assign oFetch = 1'b1;
//    assign cSavePC = 1'b1;

    //送入扫描链的控制信号,需要与虚拟面板的信号框相对应
    localparam SIZEWS = 15;          //SIZEWS 和 SIZEWD的位数算出来写上去
    wire [SIZEWS-1:0] WS;       
    assign WS = { cSavePC, cPCjump, cImmtoALU, cMemtoReg,cMemWrite,cRegWrite,cALUctrl[3:0],cImm_type[4:0]};    //对应:WS2(1位宽),WS1(4位), WS0(5位宽)
    
    //送入扫描链的数据,需要与虚拟面板的数据框相对应
    localparam SIZEWD = 1 + 32*12 + 5*3;     //SIZEWS 和 SIZEWD的位数算出来写上去
    wire [SIZEWD-1:0] WD;
    assign WD = {
		  offsetPC,	//32位 (15)
		  pcwd,		//32位(14)
        PCtoReg,    //32位 (13)
        wd,   //32位  (12)
        MDO,  //32位    (11)
        regReadData2, //32位 (10)
        aluOut,  //32位 (9)
        zero,   //1位  (8)
        immData,      //32位 (7)
        regReadData1, //32位 (6)
        ra2,    //5位  (5)
        ra1,    //5位  (4)  
        wa,     //5位  (3)
        instr_code,//32位 (2)
        pc,     //32位 (1)
        nextPC  //32位 (0)
    };
    //扫描链
    WatchChain #(.DATAWIDTH(SIZEWS+SIZEWD)) WatchChain_inst(
      .DataIn({WS,WD}), 
      .ScanIn(iScanIn), 
      .ScanOut(oScanOut), 
      .ShiftDR(iScanCtrl[1]), 
      .CaptureDR(iScanCtrl[0]), 
      .TCK(iScanClk)
    );

Endmodule

6.2 ALU模块

module ALU
#(parameter N = 32)
(
	input logic [N-1:0] iX, iY,
	input logic[3:0] iALUctrl,
	output logic [N-1:0] oF,
	output logic [3:0] oFlag
);

logic M0, S1, S0;
wire [N-1:0] A, B;
logic [N:0] result;     //输出结果
logic C0;       
assign M0 = iALUctrl[2];
assign S1 = iALUctrl[1];
assign S0 = iALUctrl[0];
assign A = iX;
assign B = (M0==0) ? iY : (~iY);  //M0控制B是否取反
assign C0 = (M0==0) ? 0 : 1;  //M0为0时,做加法;为1时,做减法

always_comb
begin
	case({S1,S0})
		2'b00: result = A + B + C0; //addi
		2'b01: result = iX & iY;    
		2'b10: result = iX | iY;
		2'b11: result = iX ^ iY;
	endcase
end

assign oF = result[N-1:0];
assign oFlag[3] = oF[N-1];
assign oFlag[2] = (oF==0) ? 1 : 0; 
assign oFlag[1] = (~A[N-1]) & ~B[N-1] & oF[N-1] | (A[N-1]) & B[N-1] & ~oF[N-1] ;
assign oFlag[0] = result[N];

endmodule

6.3 立即数生成模块

//立即数产生
module ImmeGen(
   input  logic [31:0] iInstruction,
   input  logic [4:0] iImm_type,
   output logic [31:0] oImmediate
);
   wire [31:0]  instr = iInstruction; // 换一个短点的符号
   wire [31:0]  i_imm = { {20{instr[31]}}, instr[31:20] };  //I型:补码的拓展方式  制造出一个32位立即数
   wire [31:0]  s_imm = {instr[31:25],instr[11:7]};     //STORE
   wire [31:0]  b_imm = {{20{instr[31]}},instr[7],instr[30:25],instr[11:8],1'b0};   //B-Type
   wire [31:0]  j_imm = { {instr[31]},{instr[19:12]},{instr[20]},{instr[30:21]},{1'b0},{12{1'b0}} };
   // wire [31:0]  j_imm = 

   // 使用与或门的方法构成多路数据选择器
   wire iJ_type, iU_type, iB_type, iS_type, iI_type;
   assign {iJ_type, iU_type, iB_type, iS_type, iI_type} = iImm_type;
   assign  oImmediate = ({32{iI_type}} & i_imm)|({32{iS_type}} & s_imm)
                    | ({32{iB_type}} & b_imm) | ({32{iU_type}} & u_imm) | ({32{iJ_type}} & j_imm);      

Endmodule

6.4 主译码器模块

module MainDecoder(
   input  logic [6:0] iOpcode,
   output logic [4:0] oImm_type,  //J, U, B, S, I type
   output logic [1:0] oALUop,
   output logic oRegWrite,
   output logic oMemWrite,
   output logic oMemtoReg,
   output logic oImmtoALU,
   output logic oPCjump,
   output logic oSavePC
);
  localparam K = 13; // 输出的控制信号位数

  logic [K-1:0] controls;   //contrls就是用来赋给一系列控制信号的(为了看得清楚
  assign {oSavePC, oPCjump, oImmtoALU, oMemtoReg, oMemWrite, oALUop[1:0], oImm_type[4:0], oRegWrite} = controls;

  always_comb
    case(iOpcode)
      7'b0010011: controls <= 13'b00100_11_00001_1; // I-TYPE
      7'b0100011: controls <= 13'b00101_00_00010_0; // Store
      7'b0000011: controls <= 13'b00110_00_00001_1;  //load
      7'b0110011: controls <= 13'b00000_10_00000_1;  //R-Type
      7'b1100011: controls <= 13'b01000_01_00100_0;  //beq
      7'b0110111: controls <= 13'b00100_00_01000_1;  //lui  aluop用的00(加法)
      7'b0010111: controls <= 13'b11000_01_01000_1;  //auipc   和beq很像
      7'b0000111: controls <= 13'b11000_01_10000_1;  //jal
      /*- 在下面补充指令译码
        R-Type
        Stroe
        Load
        Branch 
        ...... -*/
      default:    controls <= {K{1'b0}}; // illegal opcode
    endcase

endmodule

6.5	ALU译码器模块
module AluDecoder (
    input logic [1:0] iALUop,
    input logic [6:0] iFunct7,
    input logic [2:0] iFunct3,
    output logic [3:0] oALUctrl  //M0,S1,S0
);
    localparam ADD = 3'B000;
    localparam SUB = 3'B100;
    localparam AND = 3'B001;
    localparam OR = 3'B010;
    localparam XOR = 3'B011;

    always_comb 
        case(iALUop) 
            2'b00:   //加法 load,store
                oALUctrl <= ADD;

            2'b01: oALUctrl <= SUB;  //减法 beq,bne

            2'b10: //R
                case(iFunct3)
                3'b000:
                    case(iFunct7)
                    7'b0000000:oALUctrl <= ADD;
                    7'b0100000:oALUctrl <= SUB;
                    default:oALUctrl <= 3'bxxx;
                    endcase
                3'b100:oALUctrl <= XOR;
                3'b110:oALUctrl <= OR;
                3'b111:oALUctrl <= AND;
                default:oALUctrl <= 3'bxxx;
                endcase

            2'b11:  //根据funct3 I型指令
                case(iFunct3)   
                    3'b000: oALUctrl <= ADD; //addi
                    3'b111: oALUctrl <= AND; //andi
                    3'b110: oALUctrl <= OR;  //ori
                    3'b100: oALUctrl <= XOR; //xori
                    default: oALUctrl <= 3'bxxx;
                endcase
            default: oALUctrl <= 3'bxxx;
        endcase
endmodule

6.6 寄存器堆模块

module RAM
#(  parameter ADDRWIDTH = 6,
	  parameter DATAWIDTH = 32)
(
	input  wire iClk, iWR,
	input  wire [ADDRWIDTH-1:0] iAddress,
  input  wire [DATAWIDTH-1:0] iWriteData,
  output wire [DATAWIDTH-1:0] oReadData
);
  localparam MEMDEPTH = 1<<ADDRWIDTH; //存储器的字数 
  logic [DATAWIDTH-1:0] mem[0:MEMDEPTH-1];
  logic [DATAWIDTH-1:0] read_addr;

  always_ff @(posedge iClk)
  begin
    read_addr <= iAddress;   //读地址锁存,编译器使用FPGA的RAM块生成存储器
    if (iWR)
      mem[iAddress] <= iWriteData;
  end

  assign oReadData = mem[read_addr]; 

  /* initial 为了调试方便可给存储器赋初值,调试成功后将其删除。
      $readmemh("init_data.txt",mem);  // 存储器内容定义在文件中。 */

endmodule
写在后面:博主深谙计算机专业同学课程设计的痛苦,会陆续将自己已完成的课设上传,供大家参考。

你可能感兴趣的:(计算机组成原理课设,risc-v,verilog,systemverilog,fpga开发)