从零开始学RISC-V之初探EXU背景介绍有请EXU登台指令译码寄存器文件算术运算指令写回一个又一个的顶层仿真结果及分析总结一下
背景介绍
EXU主要负责指令的具体执行,一条指令的生命周期,从IFU取指令开始,历经指令解码,执行,写回,访存五个步骤,这其中,除首尾两个步骤由其他模块负责之外,其余步骤均有EXU负责完成。以一个常见的加法指令ADD t5, ra, sp
为例,EXU在完成该指令的执行需要完成如下任务:
指令解码,即完成指令的识别,获取该指令的相关信息,例如:
是否是合法指令:
ADD
属于合法指令。属于什么指令:
ADD
属于ALU
类,即算术运算类指令是否有立即数参与执行:此处没有
是否需要源操作数寄存器:需要,分别需要寄存器
ra
和sp
,对应索引值为1和2(依据riscv-spec,chpt 26,P137)是否需要目的操作寄存器:需要,
t5
,索引值(index)是31
从寄存器文件(regfile)模块获取对应的源操作数,此处需要用到关于源操作数寄存器的相关信息
判断依赖关系,在多级流水线的CPU里,如果当前指令需要某个操作数,且该操作数即将被更新(但是此时还没有完成更新),则表明当前指令与之前的历史指令存在RAW(read after write)依赖关系。依赖关系的判定和解除属于EXU设计的重点之一,由于本项目设计的EXU比较简单,基本不涉及依赖关系的判断。
指令执行,即执行具体的指令操作
结果写回,依据指令的目的操作数信息,将指令执行的结果写回到具体的寄存器中,或者将结果保存到存储器里。
基于上述分析,一个最简单的EXU就呼之欲出了。
有请EXU登台
要实现一条简单的ADD
指令,需要以下几个EXU子模块的协同配合:
exu_decode:指令解码模块,负责解析指令。
exu_regfile:寄存器文件模块,负责保存寄存器相关,源操作数需要从此模块读出,结果需要写回到此模块。
exu_alu:指令执行模块,包含具体的加法器执行单元。
exu_wbck:结果写回模块,负责结果写回
基于上述划分标准,以下将详细介绍各个模块的设计细节。
指令译码
要说明译码的过程,我们需要从编译的dump文件中找到上述例子中的指令所对应的指令码,如下图所示:
上图中,以冒号和空格为分界,第一部分是该指令所在的程序在内存中的地址,即PC。第二部分是该指令的指令码,第三部分是指令的具体内容,即汇编指令。
另一方面,从riscv-spec(定义了riscv指令集的基本框架)上我们发现了,一条有效的ADD指令,必须符合如下图所示的格式:
该格式从左到右,左侧是最高位,bit31,右侧是最低位,bit0。每一个分界线为一个部分,都有各自的含义,具体来讲就是:
指令码的第一部分,即最高的7bit,CODE[31:25],func7部分,必须为0000000
指令码的第二部分,即中间的第一个5bit,CODE[24:20],指定为源操作数寄存器2的索引,可以为任意值
指令码的第三部分,即中间的第二个5bit,CODE[19:15],指定为源操作数寄存器1的索引,可以为任意值
指令码的第四部分,即中间的3bit,CODE[14:12],func3部分,必须为000
指令码的第五部分,即中间的第三个5bit,CODE[11:7],指定为目的寄存器的索引,可以为任意值
指令码的第六部分,即最后的7bit,CODE[6:0],属于opcode部分,必须为0110011
由此可知,只要某个指令码,符合上述六个部分的描述,就被判断为一条ADD指令。指令码和具体的指令是一一对应的关系,不存在两条指令对应同一个指令码的情况,也不存在一个指令有两种指令码的情况。当然,某些伪指令由设计者自定义,因此会存在不同指令码对应某一个伪指令的情况,但是不属于上述讨论情形。指令译码的实现代码如下
// 指令译码实现,仅支持ADD指令module xf100_exu_decode ( // 与上层接口,此处是指与IFU的接口 input [31:0] i_dec_pc, input [31:0] i_dec_instr, // 指令的译码信息,输送到下游模块 output o_dec_add, output [4:0] o_dec_rs1_idx, output [4:0] o_dec_rs2_idx, output [4:0] o_dec_rd_idx, output o_dec_rd_wen );// 根据指令码的第1、4、6部分,判定是否属于ADD指令wire add_op = (i_dec_instr[6:0] == 7'b0110011) & (i_dec_instr[14:12] == 3'b000) & (i_dec_instr[31:25] == 7'b0000000) ; // 根据指令的第2、3、5部分,解析出关于源操作数和目的操作数的信息wire [4:0] rs1_idx = i_dec_instr[19:15];wire [4:0] rs2_idx = i_dec_instr[24:20];wire [4:0] rd_idx = i_dec_instr[11:7];//将解码结果输出到下游模块assign o_dec_add = add_op;assign o_dec_rs1_idx = rs1_idx;assign o_dec_rs2_idx = rs2_idx;assign o_dec_rd_idx = rd_idx ; assign o_dec_rd_wen = add_op;endmodule
寄存器文件
当某个指令需要读写操作数时,就需要访问寄存器文件。访问的依据就是上游译码模块给出的指令信息。其工作原理可以概括为,从指定的寄存器(由源操作数寄存器编号指定)中读取数据,或者将执行结果写到指定的寄存器(由目的寄存器编号指定)。因此就是实现一个二维数组。寄存器文件的实现代码如下:
// 寄存器文件模块实现(局部)module xf100_exu_regfile ( // 读寄存器接口 input [4:0] i_rf_rs1_idx, input [4:0] i_rf_rs2_idx, // 写寄存器接口 input i_rf_wen, input [4:0] i_rf_rdidx, input [31:0] i_rf_wdat, //读出的源操作数 output [31:0] o_rf_rs1, output [31:0] o_rf_rs2, input clk , input rst_n );// 寄存器文件,二维数组形式,每个32bit的数都表示一个寄存器,按照riscv-spec定义,总共有32个寄存器。wire [31:0] rf_r[31:0];// 一个具体的寄存器的实现,其他寄存器可按编号规律展开即可。wire rf_wen_0 = i_rf_wen & (i_rf_rdidx == 0);xf100_gnrl_dfflr #(32) reg0_dfflr (clk, rst_n, rf_wen_0, i_rf_wdat, rf_r[0]);// 二维数组的直接输出assign o_rf_rs1 = rf_r[i_rf_rs1_idx];assign o_rf_rs2 = rf_r[i_rf_rs2_idx];endmodule
算术运算
当加法指令获取到了必需的源操作数和目的操作数之后,就可以进行运算了。此处的加法器由工具指定,代码设计较简单,如下所示:
// 简单的算术运算单元,包含加法器module xf100_exu_alu ( // 译码模块输入的指令写回信息 input [4:0] i_alu_rd_idx, input i_alu_rd_wen, // 寄存器文件模块输入的源操作数值 input [31:0] i_alu_rs1, input [31:0] i_alu_rs2, // 译码模块输入的译码信息 input i_add_op, // 指令写回相关信息,需要输送到写回控制模块 output [31:0] o_alu_wdat, output [4:0] o_alu_rd_idx, output o_alu_rd_wen, input clk , input rst_n );// 操作数在进行运算之前,首先用门控电路控制一下,防止不必要的翻转,降低功耗【但同时会有时序负担】wire [31:0] adder_rs1 = {32{i_add_op}} & i_alu_rs1;wire [32:0] adder_rs2 = {32{i_add_op}} & i_alu_rs2; // 直接的加法器,简单,粗暴assign o_alu_wdat = adder_rs1 + adder_rs2;// 写回所必需的信息,直接转发assign o_alu_rd_idx = i_alu_rd_idx;assign o_alu_rd_wen = i_alu_rd_wen;endmodule
指令写回
加法的结果是必然要保存到寄存器的,这样才能将结果提供给后续其他指令使用。通过分析寄存器文件的设计代码可知,要将结果写回到寄存器,必需要有以下三个信息:
是否要写回寄存器:对于加法器,当然要写回,这个在译码模块就已经知道了。
要写到哪个寄存器:这个在译码模块也已经知道了
要写什么数据:这个在指令执行时就知道了
因此,在本项目中,加法指令的写回,就是在计算完成的时候,将计算结果写回指定的寄存器中。由于相关信息在指令执行的当拍就已经知晓,因此直接转发到寄存器文件模块即可。实现代码如下:
// 写回控制模块module xf100_exu_wbck ( // 写回所需要的信息,来自上游模块 input [4:0] i_alu_wb_idx, input i_alu_wb_en, input [31:0] i_alu_wb_dat, // 写回控制信号,输出到下游模块 output [31:0] o_wbck_rdidx, output o_wbck_wen, output [4:0] o_wbck_wdat, input clk , input rst_n ); // 直接转发 assign o_wbck_rdidx = i_alu_wb_idx; assign o_wbck_wen = i_alu_wb_en ; assign o_wbck_wdat = i_alu_wb_dat;endmodule
一个又一个的顶层
当EXU相关的子模块设计完成后,需要将其按指令执行的顺序,依次例化起来(也就是连起来)。
// 一个简单的exu顶层module xf100_exu ( input i_exu_valid, output i_exu_ready, input [31:0] i_exu_pc , input [31:0] i_exu_instr, input clk , input rst_n );assign i_exu_ready = 1'b1;///// decode the input instr.wire dec_add ;wire [4:0] dec_rs1_idx;wire [4:0] dec_rs2_idx;wire [4:0] dec_rd_idx ;wire dec_rd_wen ;xf100_exu_decode u_xf100_decode ( .i_dec_pc (i_exu_pc ), .i_dec_instr (i_exu_instr), .o_dec_add (dec_add ), .o_dec_rs1_idx (dec_rs1_idx), .o_dec_rs2_idx (dec_rs2_idx), .o_dec_rd_idx (dec_rd_idx ), .o_dec_rd_wen (dec_rd_wen ) );///// get integer-reg from regfilewire [31:0] rf_rs1;wire [31:0] rf_rs2;wire rf_wr_en ;wire [4:0] rf_wr_idx;wire [31:0] rf_wr_dat;xf100_exu_regfile u_xf100_exu_rf ( .i_rf_rs1_idx(dec_rs1_idx), .i_rf_rs2_idx(dec_rs2_idx), .i_rf_wen (rf_wr_en ), .i_rf_rdidx (rf_wr_idx), .i_rf_wdat (rf_wr_dat), .o_rf_rs1 (rf_rs1), .o_rf_rs2 (rf_rs2), .clk (clk ), .rst_n (rst_n) );///// excute the input instrwire [31:0] alu_o_wdat;wire [4:0] alu_o_rd_idx;wire alu_o_rd_wen;xf100_exu_alu u_xf100_exu_alu ( .i_alu_rd_idx(dec_rd_idx), .i_alu_rd_wen(dec_rd_wen), .i_alu_rs1 (rf_rs1), .i_alu_rs2 (rf_rs2), .i_add_op (dec_add), .o_alu_wdat ( alu_o_wdat), .o_alu_rd_idx(alu_o_rd_idx), .o_alu_rd_wen(alu_o_rd_wen), .clk (clk ), .rst_n (rst_n) );///// write back the excuted result.xf100_exu_wbck u_xf100_exu_wbck ( .i_alu_wb_idx(alu_o_rd_idx), .i_alu_wb_en (alu_o_rd_wen), .i_alu_wb_dat(alu_o_wdat), .o_wbck_rdidx(rf_wr_idx), .o_wbck_wen (rf_wr_en ), .o_wbck_wdat (rf_wr_dat), .clk (clk ), .rst_n (rst_n) );endmodule
以及,core顶层:
module xf100_core( input clk , input rst_n , output inst_ram_cs , output inst_ram_wen , output [31:0] inst_ram_din , output [13:0] inst_ram_addr , input [31:0] inst_ram_dout ); // 连接exu模块和ifu模块的信号 wire ifu_o_exu_valid; wire ifu_o_exu_ready; wire [31:0] ifu_o_exu_pc; wire [31:0] ifu_o_exu_instr;xf100_ifu u_xf100_ifu ( .o_ifu_exu_valid(ifu_o_exu_valid), .o_ifu_exu_ready(ifu_o_exu_ready), .o_ifu_exu_pc (ifu_o_exu_pc ), .o_ifu_exu_instr(ifu_o_exu_instr), .ifu2ram_cs (inst_ram_cs ), .ifu2ram_wen (inst_ram_wen ), .ifu2ram_din (inst_ram_dout), .ifu2ram_addr(inst_ram_addr), .clk (clk), .rst_n(rst_n) );xf100_exu u_xf100_ex ( .i_exu_valid(ifu_o_exu_valid), .i_exu_ready(ifu_o_exu_ready), .i_exu_pc (ifu_o_exu_pc ), .i_exu_instr(ifu_o_exu_instr), .clk (clk), .rst_n(rst_n) );endmodule
当一切准备就绪,就是仿真开启的时刻。跑起来吧......
仿真结果及分析
仿真环境不需要更新,直接在之前的基础上运行即可。会看到如下波形:
上图中,当PC(波形图第一行)指示为80000294
时,对于指令码为00208f33
的指令,指令译码模块将其识别为ADD指令,并且识别出该指令需要读取1号和2号寄存器中的数据作为源操作数,由于当前寄存器文件被初始化后未进行任何写操作,因此这两个源操作数都是0,所以加法器的执行结果也是0。加法指令需要将结果写回,目的寄存器是31号寄存器,在加法指令执行的同一周期,其结果被写回到指定寄存器中,整个设计符合预期。
下一步,我们将继续实现其他指令,比如第一条分支跳转指令背后的故事。
总结一下
EXU模块涉及指令的具体执行,按指令执行的一般流程,分为译码,执行,写回以及访存四个步骤,每个步骤由对应的模块完成特定的功能,这是设计模块划分的原则。
EXU的设计比较繁杂,涉及空泡(buble)的消除,依赖关系的判断与解除,数据Fwding的控制,写回的仲裁以及中断异常等等。由于本项目立意就是一个极简版本的CPU,因此或主动或被动的避免设计这些复杂的方面,旨在快速建立起CPU设计框架,降低设计门槛。
没了。