从零开始学RISC-V之初探EXU

背景介绍

EXU主要负责指令的具体执行,一条指令的生命周期,从IFU取指令开始,历经指令解码,执行,写回,访存五个步骤,这其中,除首尾两个步骤由其他模块负责之外,其余步骤均有EXU负责完成。以一个常见的加法指令ADD t5, ra, sp为例,EXU在完成该指令的执行需要完成如下任务:

  • 指令解码,即完成指令的识别,获取该指令的相关信息,例如:
    • 是否是合法指令:ADD属于合法指令。
    • 属于什么指令:ADD属于ALU类,即算术运算类指令
    • 是否有立即数参与执行:此处没有
    • 是否需要源操作数寄存器:需要,分别需要寄存器rasp,对应索引值为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 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;

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设计框架,降低设计门槛。

  • 没了。

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

推荐阅读更多精彩内容