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

背景介绍

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

  • 依据RISC-V-Spec文档对指令A的定义,对其进行解码。从中分解出需要关注的信息,包括但不限于:
    • 该指令属于什么类型的指令,是运算指令系统指令还是跳转指令
    • 该指令是否需要使用数据寄存器,是使用1个,2个,还是3个,它们的索引值是多少
    • 该指令是否需要使用立即数,该立即数在指令编码中的排布方式是怎样的,是有符号数还是无符号数
    • 该指令是否需要保存相关的结果,是保存到存储器,还是保存到数据寄存器
    • 该指令是否会产生错误,如果产生错误,是否需要重新填充流水线
  • 根据所获取的信息,分析该指令具体的行为,在脑海中构想出该指令从取指到执行到最终从流水线中退出的整个过程。
  • 修改具体的模块代码,包括但不限于:
    • decode模块:将指令A相关的信息输出
    • alu模块:执行具体的运算、跳转、冲刷流水线等操作。如果需要写回,则应该在此处生成写回的控制电路
    • csr模块:系统指令执行模块,读写对应csr寄存器
    • wbck模块:将指令A的运算结果写回到寄存器中

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


一个BJP来了

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

5-jmp指令.png

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

根据定义解码

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

5-JAL编码.png

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

  • 该指令属于什么类型的指令
    • 这是一条跳转指令,用于执行不同的程序流
  • 该指令是否需要使用数据寄存器
    • 不需要,该指令使用立即数参与计算跳转的目的地址
  • 该指令是否需要使用立即数
    • 需要,在该指令中,指令码的第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


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


仿真结果及分析

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

5-JAL波形.png

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

下一步,我们将继续一类特殊指令的设计,主角是存储器。
同系列文章首发于微信公众号:ICLiker,愿逢有缘人。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容