Rong晔大佬教程学习(3):译码
在讲解指令译码之前,我们首先需要了解指令,如下图所示,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