从零开始学RISC-V之指令模板

发布于 2021-04-02 02:22


从零开始学RISC-V之指令模板背景介绍一个BJP来了根据定义解码分析指令行为修改具体代码算术运算一个又一个的顶层仿真结果及分析

背景介绍

上一篇中,我们知道了第一条RISC-V指令的实现过程。其实可以从中提炼出一份RISC-V指令设计的常规模板。该模板可以针对绝大部分的RISC-V指令,以此为基础,XF100CPU的设计工作就只是模板的具体展开,即变成了纯粹的体力劳动。假如我们现在需要实现指令A,那么只需要按照如下步骤来实现:

    • 该指令属于什么类型的指令,是运算指令系统指令还是跳转指令

    • 该指令是否需要使用数据寄存器,是使用1个,2个,还是3个,它们的索引值是多少

    • 该指令是否需要使用立即数,该立即数在指令编码中的排布方式是怎样的,是有符号数还是无符号数

    • 该指令是否需要保存相关的结果,是保存到存储器,还是保存到数据寄存器

    • 该指令是否会产生错误,如果产生错误,是否需要重新填充流水线

  • 根据所获取的信息,分析该指令具体的行为,在脑海中构想出该指令从取指到执行到最终从流水线中退出的整个过程。

  • 修改具体的模块代码,包括但不限于:

    • decode模块:将指令A相关的信息输出

    • alu模块:执行具体的运算、跳转、冲刷流水线等操作。如果需要写回,则应该在此处生成写回的控制电路

    • csr模块:系统指令执行模块,读写对应csr寄存器

    • wbck模块:将指令A的运算结果写回到寄存器中

下面以一个常见的jmp指令为例,介绍设计模板的使用。


一个BJP来了

我们从rv32ui-p-add.dum文件中,找到第一条指令,如下图所示:

该指令的PC是0x80000000,指令编码为0x1480006f。这些信息都是我们要使用的。开始按模板执行:

根据定义解码

根据指令码的低7位 (0x6f),我们判断出这是一条JAL,如下图所示:

那么通过这个表,就可以回答第一个步骤中的各个问题:

  • 该指令属于什么类型的指令

    • 这是一条跳转指令,用于执行不同的程序流

  • 该指令是否需要使用数据寄存器

    • 不需要,该指令使用立即数参与计算跳转的目的地址

  • 该指令是否需要使用立即数

    • 需要,在该指令中,指令码的第31位作为立即数的第20位,指令码的第19到第12位作为立即数的第19到第12位,指令码的第20位作为立即数的第11位,指令码的第30到21位作为立即数的第10到第1位。

    • 根据sepc中对于JAL指令的介绍(全文搜索JAL关键字即可),可知该指令所使用的立即数是有符号数,且最低位应该补0。

  • 该指令是否需要保存相关的结果

    • 需要,该指令会将其紧接着的下一条指令的PC地址,保存到目的寄存器中

    • 目的寄存器的索引值是0,本意是将紧接着的下一条指令的PC地址保存到0号寄存器的。但是需要注意的是,RISC-V规定,0号寄存器用于永久保存0值,只能读,不能写。因此此处实际上不需要写回

  • 该指令是否会产生错误

    • 会,对于本项目来讲,分支预测采用最最简单的预测机制,即预测所有的分支跳转永远不跳(非常差劲的预测手段,但仍旧满足项目需求),即其下一条指令就是该指令紧邻的下一条指令(有点拗口)。因此如果当前指令实际跳转的目的地址,不等于其紧邻的下一条指令的PC的话,就会产生预测错误,此时就要重新填充流水线

分析指令行为

通过上述分析,我们大致可以了解到当前这条JAL指令在流水线中的行为了。这条指令在执行时,需要根据当前信息计算一个目的地址(trgt_addr),该目的地址是当前JAL指令的PC值与指令编码中的立即数之和。同时,该指令还要计算一个预测地址(prdt_addr),该地址是当前JAL指令的PC值加4。如果这两个地址相等,则表明预测成功,不需要冲刷流水线,否则表明预测失败,使用错误的预测地址取指的指令都是无效指令,需要将其废除,并从正确的跳转地址处取指。

修改具体代码

指令译码

////////////////////////////
// 指令译码实现,仅支持部分指令
module xf100_exu_decode  (
 // 与上层接口,此处是指与IFU的接口
 input [31:0]  i_dec_pc,
 input [31:0]  i_dec_instr,
 // 指令的译码信息,输送到下游模块
 output             o_dec_add,
 output             o_dec_jal,
 output [4:0]       o_dec_rs1_idx,
 output             o_dec_rs1en,
 output [4:0]       o_dec_rs2_idx,
 output             o_dec_rs2en,
 output [4:0]       o_dec_rd_idx,
 output             o_dec_rd_wen,
 output             o_dec_imm_en,
 output [31:0]      o_dec_imm,
 output [31:0]      o_dec_pc

);


// 根据指令码的第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)
           ;
// 根据指令码的第1部分,判定是否属于JAL指令
wire jal = (i_dec_instr[6:0]   == 7'b1101111);

// 根据指令的第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];
// 根据具体指令类型,生成是否需要源操作寄存器和目的寄存器
wire rs1en = add_op;
wire rs2en = add_op;
wire rdwen = add_op
          | jal;
//根据指令信息,解析出立即数,当前使用有符号数,因此最高位需要用符号位扩展补齐。
wire [31:0] imm = {{11{i_dec_instr[31]}},i_dec_instr[31],i_dec_instr[19:12],i_dec_instr[20],i_dec_instr[30:21],1'b0};
// 根据指令类型,解析出是否需要使用立即数
wire imm_en = jal;

//将解码结果输出到下游模块
assign  o_dec_add     = add_op;
assign  o_dec_jal     = jal;
assign  o_dec_rs1_idx = rs1_idx;
assign  o_dec_rs1en = rs1en;
assign  o_dec_rs2_idx = rs2_idx;
assign  o_dec_rs2en = rs2en;
assign  o_dec_rd_idx  = rd_idx ;
assign  o_dec_rd_wen  = rdwen;
assign  o_dec_imm  = imm ;
assign  o_dec_imm_en  = imm_en ;
assign  o_dec_pc  = i_dec_pc ;

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,
// 译码模块输入的立即数与PC信息
 input [31:0] i_alu_imm,
 input [31:0] i_alu_pc,
// 译码模块输入的译码信息
 input  i_add_op,
 input  i_jal_op,
// 输出到IFU模块的流水线冲刷信息
 output o_jal_flush_req,
 output [31:0] o_jal_flush_pc,
// 指令写回相关信息,需要输送到写回控制模块
 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)
                    | ({32{i_jal_op}} & i_alu_pc)
                    ;
wire [32:0] adder_rs2 = ({32{i_add_op}} & i_alu_rs2)
                    | ({32{i_jal_op}} & 32'h4)
                    ;
// 直接的加法器,简单,粗暴
wire [32:0] alu_adder_res = adder_rs1 + adder_rs2;
// 计算实际的跳转地址
wire [31:0] jal_adder_rs1 = ({32{i_jal_op}} & i_alu_pc);
wire [31:0] jal_adder_rs2 = ({32{i_jal_op}} & i_alu_imm);
wire [32:0] jal_trgt_addr = jal_adder_rs1 + jal_adder_rs2;


// 如果实际的跳转地址和预测的跳转地址不相符,则实际的跳转地址需要送回到IFU,IFU模块以此为新的取指地址,重新取指。
wire jal_flush_req = jal_trgt_addr != alu_adder_res;
wire [31:0] jal_flush_pc = jal_trgt_addr;


assign o_jal_flush_req = jal_flush_req;
assign o_jal_flush_pc = jal_flush_pc;
// 写回所必需的信息,直接转发
assign o_alu_wdat = alu_adder_res;
assign o_alu_rd_idx = i_alu_rd_idx;
assign o_alu_rd_wen = i_alu_rd_wen;

endmodule

一个又一个的顶层

当EXU相关的子模块设计完成后,需要将其按指令执行的顺序,依次例化起来(也就是连起来)。

////////////////////////////
module xf100_exu  (
 input         i_exu_valid,
 output        i_exu_ready,
 input [31:0]  i_exu_pc   ,
 input [31:0]  i_exu_instr,

 output        o_exu_flush_req,
output [31:0] o_exu_flush_pc,


 input   clk   ,
 input   rst_n  
);

assign i_exu_ready = 1'b1;
///////////////////////////////
// decode the input instr.
wire        dec_add    ;
wire        dec_jal    ;
wire [4:0]  dec_rs1_idx;
wire        dec_rs1en;
wire [4:0]  dec_rs2_idx;
wire        dec_rs2en;
wire [4:0]  dec_rd_idx ;
wire        dec_rd_wen ;
wire [31:0]  dec_imm ;
wire        dec_imm_en ;
wire [31:0]  dec_pc ;
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_jal     (dec_jal    ),
 .o_dec_rs1_idx (dec_rs1_idx),
 .o_dec_rs1en   (dec_rs1en),
 .o_dec_rs2_idx (dec_rs2_idx),
 .o_dec_rs2en   (dec_rs2en),
 .o_dec_rd_idx  (dec_rd_idx ),
 .o_dec_rd_wen  (dec_rd_wen ),
 .o_dec_imm_en  (dec_imm_en ),
 .o_dec_pc      (dec_pc ),
 .o_dec_imm     (dec_imm )
);

///////////////////////////////
// get integer-reg from regfile
wire [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 instr
wire [31:0] alu_o_wdat;
wire [4:0] alu_o_rd_idx;
wire       alu_o_rd_wen;

wire [31:0]  jal_o_flush_pc ;
wire         jal_o_flush_req;

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_alu_imm  (dec_imm),
 .i_alu_pc   (dec_pc),
 .i_add_op   (dec_add),
 .i_jal_op   (dec_jal),


 .o_jal_flush_req (jal_o_flush_req),
 .o_jal_flush_pc  (jal_o_flush_pc),

 .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)
);

assign o_exu_flush_req = jal_o_flush_req;
assign o_exu_flush_pc  = jal_o_flush_pc ;


///////////////////////////////
// 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

当一切准备就绪,就是仿真开启的时刻。跑起来吧......


仿真结果及分析

仿真环境不需要更新,直接在之前的基础上运行即可。会看到如下波形:

上图中,当PC(波形图第一行)指示为8000000时,对于指令码为1480006f的指令,指令译码模块将其识别为JAL指令,并且识别出该指令需要立即数参与运算,经过计算发现实际需要跳转的目的地址是80000148,并不是预测的80000004,因此需要产生流水线冲刷信号jal_flush_req,同时将正确的跳转地址送到IFU模块。从波形中可以看出,当前JAL指令的下一条指令的PC正是我们需要跳转的地址80000148,表明处理器按照既定程序流运行,整个设计符合预期。

下一步,我们将继续一类特殊指令的设计,主角是存储器。

本文来自网络或网友投稿,如有侵犯您的权益,请发邮件至:aisoutu@outlook.com 我们将第一时间删除。

相关素材