Rong晔大佬教程学习(3):取译码

发布时间 2023-12-13 20:32:02作者: 郭若晨是我的rbq

  在讲解指令译码之前,我们首先需要了解指令,如下图所示,ARM、MIPS、RISCV-v指令集同属于RISC指令集(精简指令集),特别注意的是,相同的一条指令在不同的ISA中译码得到的结果是不同的,这也很好理解,比如“nihao”在拼音中可以翻译为“你好”,就是打招呼的意思,但在英文中这甚至不是一个单词。

   RISCV中的指令主要由以下6种类型:R、I、S、B、U、J,其含义以在图中给出

   上图中opcode表示指令操作码,通过这7位就知道这是一个什么指令;rs1、rs2、rd分别表示源寄存器1、2以及目的寄存器;imm代表立即数;funct3、funct7代表指令对应的功能。同时,仔细观察上图的指令格式可以发现:除了立即数之外其他元素都固定在指令同样的位置(如opcode一直在低7位),这为指令译码提供了便利。

  实际中,通过opcode、funct3、funct7,我们可以实际确认出一条指令(后续的代码设计模块中也是这个逻辑)。RISC-V reference中给出了riscv中所有指令的格式:以ADD指令为例,opcode为0110011说明其为R型(寄存器型)指令,再根据funct3为0x0和funct7为0x00确认。

   以下是本节内容的代码模块:

1.rvseed_defines.v

  和上一节不同的是,该宏定义文件增加了许多定义

// simulation clock period
`define SIM_PERIOD 20 // 20ns -> 50MHz 

// processor numbers
`define CPU_WIDTH 32 // rv32

// instruction memory
`define INST_MEM_ADDR_DEPTH 1024
`define INST_MEM_ADDR_WIDTH 10 // 2^10 = 1024

// register 
`define REG_DATA_DEPTH 32
`define REG_ADDR_WIDTH 5 // 2^5 = 32

// immediate generate
`define INST_I_IMM_WIDTH 12
`define INST_S_IMM_WIDTH 12
`define INST_B_IMM_WIDTH 13
`define INST_U_IMM_WIDTH 32
`define INST_J_IMM_WIDTH 21

// instruction bits select
`define FUNCT3_BASE 12
`define FUNCT7_BASE 25
`define RD_BASE     7
`define RS1_BASE    15
`define RS2_BASE    20

// opcode
`define OPCODE_WIDTH 7
`define INST_TYPE_R  `OPCODE_WIDTH'b0110011 // add/sub/xor/or/and/sll/srl/sra/slt/sltu
`define INST_TYPE_I  `OPCODE_WIDTH'b0010011 // addi/xori/ori/andi/slli/srli/srai/slti/sltiu
`define INST_TYPE_IL `OPCODE_WIDTH'b0000011 // lb/lh/lw/lbu/lhu
`define INST_TYPE_S  `OPCODE_WIDTH'b0100011 // sb/sh/sw
`define INST_TYPE_B  `OPCODE_WIDTH'b1100011 // beq/bne/blt/bge/bltu/bgeu
`define INST_JAL     `OPCODE_WIDTH'b1101111 // jal
`define INST_JALR    `OPCODE_WIDTH'b1100111 // jalr
`define INST_LUI     `OPCODE_WIDTH'b0110111 // lui
`define INST_AUIPC   `OPCODE_WIDTH'b0010111 // auipc
`define INST_TYPE_IE `OPCODE_WIDTH'b1110011 // ecall/ebreak

// funct3
// R-type
`define FUNCT3_WIDTH 3
`define INST_ADD_SUB   `FUNCT3_WIDTH'h0 
`define INST_XOR       `FUNCT3_WIDTH'h4
`define INST_OR        `FUNCT3_WIDTH'h6
`define INST_AND       `FUNCT3_WIDTH'h7
`define INST_SLL       `FUNCT3_WIDTH'h1
`define INST_SRL_SRA   `FUNCT3_WIDTH'h5
`define INST_SLT       `FUNCT3_WIDTH'h2
`define INST_SLTU      `FUNCT3_WIDTH'h3
// I-type 
`define INST_ADDI      `FUNCT3_WIDTH'h0
`define INST_XORI      `FUNCT3_WIDTH'h4
`define INST_ORI       `FUNCT3_WIDTH'h6
`define INST_ANDI      `FUNCT3_WIDTH'h7
`define INST_SLLI      `FUNCT3_WIDTH'h1
`define INST_SRLI_SRAI `FUNCT3_WIDTH'h5 
`define INST_SLTI      `FUNCT3_WIDTH'h2
`define INST_SLTIU     `FUNCT3_WIDTH'h3
// I-type load
`define INST_LB        `FUNCT3_WIDTH'h0
`define INST_LH        `FUNCT3_WIDTH'h1
`define INST_LW        `FUNCT3_WIDTH'h2
`define INST_LBU       `FUNCT3_WIDTH'h4
`define INST_LHU       `FUNCT3_WIDTH'h5
// S-type
`define INST_SB        `FUNCT3_WIDTH'h0
`define INST_SH        `FUNCT3_WIDTH'h1
`define INST_SW        `FUNCT3_WIDTH'h2
// B-type
`define INST_BEQ       `FUNCT3_WIDTH'h0
`define INST_BNE       `FUNCT3_WIDTH'h1
`define INST_BLT       `FUNCT3_WIDTH'h4
`define INST_BGE       `FUNCT3_WIDTH'h5
`define INST_BLTU      `FUNCT3_WIDTH'h6
`define INST_BGEU      `FUNCT3_WIDTH'h7
// I-type environment
`define INST_ECALL     `FUNCT3_WIDTH'h0
`define INST_EBREAK    `FUNCT3_WIDTH'h0

// funct7
`define FUNCT7_WIDTH 7
`define FUNCT7_INST_A  `FUNCT7_WIDTH'h00
`define FUNCT7_INST_B  `FUNCT7_WIDTH'h20


// ALU opcode
`define ALU_OP_WIDTH 4
`define ALU_AND  `ALU_OP_WIDTH'b0000
`define ALU_OR   `ALU_OP_WIDTH'b0001
`define ALU_XOR  `ALU_OP_WIDTH'b0010
`define ALU_ADD  `ALU_OP_WIDTH'b0011
`define ALU_SUB  `ALU_OP_WIDTH'b0100
`define ALU_SLL  `ALU_OP_WIDTH'b0101 // shift left logical
`define ALU_SRL  `ALU_OP_WIDTH'b0110 // shift right logical
`define ALU_SRA  `ALU_OP_WIDTH'b0111 // shift right arith 
`define ALU_SLT  `ALU_OP_WIDTH'b1000 // set less than  
`define ALU_SLTU `ALU_OP_WIDTH'b1001 // set less than (unsigned) 
`define ALU_BLT  `ALU_OP_WIDTH'b1010 // branch less than
`define ALU_BLTU `ALU_OP_WIDTH'b1011 // branch less than (unsigned)
`define ALU_JAL  `ALU_OP_WIDTH'b1100  
`define ALU_JALR `ALU_OP_WIDTH'b1101  

// ALU select soure
`define ALU_SRC_WIDTH 2
`define ALU_SRC_REG     `ALU_SRC_WIDTH'b00 // src1 = reg1, src2 = reg2
`define ALU_SRC_IMM     `ALU_SRC_WIDTH'b01 // src1 = reg1, src2 = imm
`define ALU_SRC_FOUR_PC `ALU_SRC_WIDTH'b10 // src1 = 4,    src2 = pc
`define ALU_SRC_IMM_PC  `ALU_SRC_WIDTH'b11 // src1 = imm,  src2 = pc

// IMM GEN opcode
`define IMM_GEN_OP_WIDTH 3
`define IMM_GEN_I `IMM_GEN_OP_WIDTH'b000 
`define IMM_GEN_S `IMM_GEN_OP_WIDTH'b001
`define IMM_GEN_B `IMM_GEN_OP_WIDTH'b010
`define IMM_GEN_J `IMM_GEN_OP_WIDTH'b011
`define IMM_GEN_U `IMM_GEN_OP_WIDTH'b100

 

2.ctrl.v

  关于这个程序的理解,我已经在注释中详细说明了,参考以下内容:

`include "rvseed_defines.v"

//译码模块,只输入了一个指令,后面的所有输出为译码的结果
module ctrl (
    input      [`CPU_WIDTH-1:0]        inst,       // instruction input

    output reg                         branch,     // branch flag,分支跳转指令
    output reg                         jump,       // jump flag,跳转指令

    output reg                         reg_wen,    // register write enable,寄存器写使能
    output reg [`REG_ADDR_WIDTH-1:0]   reg_waddr,  // register write address,寄存器写地址
    output reg [`REG_ADDR_WIDTH-1:0]   reg1_raddr, // register 1 read address,寄存器1读地址
    output reg [`REG_ADDR_WIDTH-1:0]   reg2_raddr, // register 2 read address,寄存器2读地址
    
    output reg [`IMM_GEN_OP_WIDTH-1:0] imm_gen_op, // immediate extend opcode,立即数扩展操作码

    output reg [`ALU_OP_WIDTH-1:0]     alu_op,     // alu opcode,alu操作码
    output reg [`ALU_SRC_WIDTH-1:0]    alu_src_sel // alu source select flag,alu的数据源选择
);

//一些控制器内部信号的声明
//之前说过指令中opcode、funct3、funct7等的位置,除了立即数之外都是固定的
wire [`OPCODE_WIDTH-1:0] opcode = inst[`OPCODE_WIDTH-1:0];            
wire [`FUNCT3_WIDTH-1:0] funct3 = inst[`FUNCT3_WIDTH+`FUNCT3_BASE-1:`FUNCT3_BASE];
wire [`FUNCT7_WIDTH-1:0] funct7 = inst[`FUNCT7_WIDTH+`FUNCT7_BASE-1:`FUNCT7_BASE]; 
wire [`REG_ADDR_WIDTH-1:0] rd   = inst[`REG_ADDR_WIDTH+`RD_BASE-1:`RD_BASE]; 
wire [`REG_ADDR_WIDTH-1:0] rs1  = inst[`REG_ADDR_WIDTH+`RS1_BASE-1:`RS1_BASE]; 
wire [`REG_ADDR_WIDTH-1:0] rs2  = inst[`REG_ADDR_WIDTH+`RS2_BASE-1:`RS2_BASE]; 


always @(*) begin
//在always块初就对相关寄存器块进行了赋值,所以之后case中没有出现default也没有问题
//之后在case内的赋值会覆盖掉最初的赋值
    branch      = 1'b0;     
    jump        = 1'b0;     //两个标志处于无效状态
    reg_wen     = 1'b0;     //写使能为低
    reg1_raddr  = `REG_ADDR_WIDTH'b0;
    reg2_raddr  = `REG_ADDR_WIDTH'b0;
    reg_waddr   = `REG_ADDR_WIDTH'b0;     //三个默认地址为0
    imm_gen_op  = `IMM_GEN_I;     //立即数扩展默认设置为i型扩展
    alu_op      = `ALU_AND;     //操作码设置为最初的and指令
    alu_src_sel = `ALU_SRC_REG;     //alu数据选择都选了寄存器

//根据opcode、funct3、funct7来决定是哪一条指令
    case (opcode)
        `INST_TYPE_R: begin         //R型语句
            reg_wen     = 1'b1;
            reg1_raddr  = rs1;
            reg2_raddr  = rs2;
            reg_waddr   = rd;
            alu_src_sel = `ALU_SRC_REG;
            case (funct3)
                `INST_ADD_SUB: 
                    alu_op = (funct7 == `FUNCT7_INST_A) ? `ALU_ADD : `ALU_SUB; // A:add B:sub 
            endcase
        end

/*
以add指令为例,根据判断为R型指令,作用是将rs1和rs2的值相加存在目标寄存器中,
那么此时写使能reg_wen为1,读取的两个寄存器的地址分别为rs1和rs2,目标寄存器地址为rd,这些东西在38~43行代码已经规定好了
代码66行是进行ALU的数据源选择,因为alu模块正常来说是只有两个输入源的,这里选择的ALU_SRC_REG意思是两个数据源都是寄存器
代码67行是根据funct3进行选择,INST_ADD_SUB就是0x0的意思(0x是16进制的前缀,这里我迷糊了一下)
进一步的,代码69行需要funct7进行区分,同时对alu_op赋值就是对alu的操作码进行一个配置,
*/
        `INST_TYPE_I: begin
            reg_wen     = 1'b1;
            reg1_raddr  = rs1;
            reg_waddr   = rd;
            alu_src_sel = `ALU_SRC_IMM;
            case (funct3)
                `INST_ADDI: 
                    alu_op = `ALU_ADD; 
            endcase
        end
//前面默认为立即数的扩展,所以这里没有重复写(代码55行)


        `INST_TYPE_B: begin
            reg1_raddr  = rs1;
            reg2_raddr  = rs2;
            imm_gen_op  = `IMM_GEN_B;       //立即数扩展配置,与i型不同,因此需要重新配置
            alu_src_sel = `ALU_SRC_REG;
            case (funct3)
                `INST_BNE: begin
                    branch     = 1'b1;
                    alu_op     = `ALU_SUB;      //这里判断相等与不相等是使用减法
                end
            endcase
        end
//分支跳转指令bne,rs1和rs2不相等则进行一个累加


        `INST_JAL: begin // only jal
            jump        = 1'b1;
            reg_wen     = 1'b1;
            reg_waddr   = rd;
            imm_gen_op  = `IMM_GEN_J;
            alu_op      = `ALU_ADD;
            alu_src_sel = `ALU_SRC_FOUR_PC; //pc + 4,数据源选择需要pc和4
        end
//直接跳转指令jal

/*将立即数左移12位之后存放到目标寄存器中,但左移12位是在立即数扩展的时候完成的
所以这里并不是在ALU中扩展,这里的加法运算是让已经移位过的立即数的值和一个0地址的寄存器相加,然后存放到目标寄存器中
*/
        `INST_LUI: begin // only lui
                reg_wen     = 1'b1;
                reg1_raddr  = `REG_ADDR_WIDTH'b0; // x0 = 0
                reg_waddr   = rd;
                imm_gen_op  = `IMM_GEN_U;
                alu_op      = `ALU_ADD;
                alu_src_sel = `ALU_SRC_IMM; // x0 + imm
        end
    endcase 
end

endmodule

//译码的输出不是现在是什么指令,而是输出相关的控制信号、地址信号分别是什么

  正如我注释的最后一行代码所说,译码的过程实际上就是把指令拆分为我们所需要的有用信息,再根据相应的信息去输出不同的信号,从而让其他部分去执行不同的行为,这一部分的设计几乎都是在条件判断,没有什么复杂度,只要对照着上一个rvseed_defines.v模块把参数一个一个对应起来即可,有两点特别注意:

  1.一个是那个0x0的问题,0x是16进制的前缀,当时看到这里没反应过来

  2.在这一连串case语句判断前就已经初始化了一些内容(例如对于i型指令,我们的立即数扩展imm_gen_op初始化就为i型,所以在该指令处就没有定义),所以程序没有写default也没有关系。


2023-12-13 20:24:53

  在教研室写完这篇文章,落笔,今天一下子把取指和译码给攻克了,虽然中午睡了2h午觉,但还是感觉脑子要被撑爆了,cpu设计真是一个庞大的工程,光是这种最简单的就让我有些吃力了,当然部分原因是理解上的吃力,但我觉得更多的还是后期,去考虑更多的设计中的问题(面积、功耗等)时,对每一个模块的熟悉和掌握程度,现在我只是初步了解,希望未来能做到熟能生巧的地步吧。

 

 


imm_gen_op