我是靠谱客的博主 潇洒钻石,这篇文章主要介绍和你一起从零开始写RISC-V处理器(3)上期回顾一、指令的基本结构二、流水线冲刷机制三、模块更改过程四、指令测试总结往期精彩,现在分享给大家,希望可以做个参考。

RISC-V指令添加及测试(附工程文件)

文章目录

    • RISC-V指令添加及测试(附工程文件)
  • 上期回顾
  • 一、指令的基本结构
  • 二、流水线冲刷机制
  • 三、模块更改过程
    • ①ctrl模块
    • ②pc_reg模块
    • ③if_id和id_ex模块
    • ④id模块
    • ⑤ex模块
    • ⑦上层例化部分需要重新接线进行连接。
    • ⑧重要总结!
  • 四、指令测试
  • 总结
  • 往期精彩


上期回顾

在这里插入图片描述
上期我们实现了加法指令,并且自己写了三条命令代码进行了一下简单的测试,后续还会继续进行更为规范的测试,即使用官方的指令测试文件来进行指令测试,不过目前还没法用,因为涉及到分支指令和跳转指令;

这一期就来说一说,分支指令及跳转指令的实现。最后,掌握了指令添加的方法后,一大批指令都可照猫画猫的添加了!

【注】本期内容主要说明分支指令bne的添加过程,至于跳转指令jal,lui等都类似,文末给出上一期全部代码以及本期代码;

本文首发于公众号:FPGA学习者,关注公众号,获取更多精彩内容。


一、指令的基本结构

这一期主要来实现BNE指令,先来看指令结构:
在这里插入图片描述在这里插入图片描述
很明显,它属于SB类指令,不过一般称为B(branch)型指令,BNE即(branch not equal),也就是说,在两个寄存器的值不相等的时候,就会执行BNE指令,执行结果是:指令地址加上立即数重新赋值给指令地址寄存器,如下图所示:
在这里插入图片描述
怎么来理解呢?其实就相当于进行了一个指令的跳转,如下图:
在这里插入图片描述
运行到BNE指令后,如果满足条件,则立即跳转到label处执行指令10。
那么,问题来了?
以我们之前写的程序,是一根筋的往前走啊,根本就没有返回到pc_reg的信号,怎么能指示处理器下一步要执行哪个地址的指令呢?所以,这是我们接下来需要考虑的问题。

二、流水线冲刷机制

在含有BNE指令的程序中,程序的运行过程如下图所示:
状态1
在这里插入图片描述状态2
在这里插入图片描述
状态3
在这里插入图片描述
状态4
在这里插入图片描述
执行模块在执行BNE指令时,下一步要进行指令地址的跳转,label就是其偏移地址;但是当前指令2和指令3已经进入了流水线中,尤其是指令2,下一个时钟周期就要执行了,我们不能让他继续执行当前在流水线中的指令,必须予以清除,这就是所谓的流水线冲刷。

要想进行流水线冲刷,并且返回需要跳转的地址信号,还需要引入一个ctrl控制模块,如下图所示:
在这里插入图片描述
此时,if_id和id_ex模块产生NOP指令,冲刷流水线,并且指令跳转到指令10处运行,如下图所示:
在这里插入图片描述
接下来命令从指令10处开始正常执行。
所以,我们需要对之前的程序进行以下修改:

①增加ctrl模块,组合逻辑,三个输入端口,三个输出端口;
②pc_reg模块进行修改,使其可以进行指令跳转;
③if_id模块进行修改,在有冲刷流水线的情况下,产生NOP信号;
④id译码模块添加B型指令的译码过程;
⑤id_ex模块类似于if_id模块,对置位条件进行修改,有跳转的情况下产生NOP命令。
⑥在ex模块进行执行BNE,添加跳转地址和跳转使能信号。
⑦上层例化部分需要重新接线进行连接。

三、模块更改过程

①ctrl模块

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
`include "defines.v" `timescale 1ns/1ns module ctrl( //from ex input wire [31:0] jump_addr_i , input wire jump_en_i , input wire hold_flag_ex_i , //to pc/if_id/id_ex output reg [31:0] jump_addr_o , output reg jump_en_o , output reg hold_flag_o ); always@(*)begin jump_addr_o = jump_addr_i; jump_en_o = jump_en_i; if(jump_en_i || hold_flag_ex_i)begin hold_flag_o = 1'b1; end else begin hold_flag_o = 1'b0; end end endmodule

②pc_reg模块

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module pc_reg( input wire clk , input wire rst_n , input wire[31:0] jump_addr_i , //第二期新加的 input wire jump_en , output reg [31:0] pc_o //输出的指令地址 ); always@(posedge clk or negedge rst_n)begin if(~rst_n)begin pc_o <= 32'b0; end else if(jump_en)begin //新加入 pc_o <= jump_addr_i; //新加入 end else begin pc_o <= pc_o + 3'd4; end end endmodule

③if_id和id_ex模块

这两个模块主要用D触发器进行打拍,更改D触发器的置位条件,在置位情况下产生指定NOP指令即可,所以对D触发器修改如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//D触发器模块,将输入的数据打一拍 //第二期将该寄存器修改 module dff_set #( parameter DW = 32 ) ( input wire clk , input wire rst , input wire hold_flag_i , //第二期新增端口信号 input wire [DW-1 : 0] set_data , //复位时的信号 input wire [DW-1 : 0] data_i , output reg [DW-1 : 0] data_o ); always@(posedge clk or negedge rst)begin if(~rst || hold_flag_i == 1'b1)begin //复位或者进行指令跳转的时候,进行置位信号 data_o <= set_data; //置位信号 end else begin data_o <= data_i; end end endmodule

在另外两个模块中例化D触发器时进行相应的修改:

复制代码
1
2
3
//对D触发器的置位条件进行了修改 dff_set #(32) dff1(clk,rst_n,hold_flag_i,`INST_NOP,inst_i,inst_o);

④id模块

在这里插入图片描述
从这个图可以看出,B型指令和R型指令译码类似,不同之处在于B型指令没有目的寄存器,所以也不需要回写信号,至于跳转地址时所需要的立即数处理,放在执行模块中去。

R型指令的译码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
…………………………………… //R 类指令 `INST_TYPE_R_M: begin case(funct3) `INST_ADD_SUB,`INST_SLL,`INST_SLT,`INST_SLTU,`INST_XOR,`INST_SR,`INST_OR,`INST_AND: //译码过程都是一样的 begin rs1_addr_o = rs1; rs2_addr_o = rs2; op1_o = rs1_data_i; op2_o = rs2_data_i; rd_addr_o = rd; reg_wen_o = 1'b1; end ……………………………………

B型指令的译码

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
…………………………………… //B型指令 //第二期加入 `INST_TYPE_B: begin case(funct3) `INST_BNE,`INST_BEQ,`INST_BEQ,`INST_BNE,`INST_BLT,`INST_BGE,`INST_BLTU,`INST_BGEU ://这些命令格式一样,译码过程也一样 begin rs1_addr_o = rs1; rs2_addr_o = rs2; op1_o = rs1_data_i; op2_o = rs2_data_i; rd_addr_o = 5'b0; //不同之处 reg_wen_o = 1'b0; //不同之处 end ……………………………………

⑤ex模块

ex模块主要是确定跳转地址的产生,以及跳转使能信号的产生。新定义三个输出信号,并且不要忘记在其他不要用到跳转信号的时候将该三处信号置零。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
`include "defines.v" //执行模块 module ex( //from id_ex input wire [31:0] inst_i , input wire [31:0] inst_addr_i , input wire [31:0] op1_i , input wire [31:0] op2_i , input wire [4:0] rd_addr_i , input wire reg_wen_i , //to regs output reg [4:0] rd_addr_o , output reg [31:0] rd_data_o , output reg rd_wen_o , //to ctrl output reg [31:0] jump_addr_o , //第二期加入 output reg jump_en_o , output reg hold_flag_o ); …………………………………… //添加跳转偏移地址 //brench wire [31:0] jump_imm = {{19{inst_i[31]}},inst_i[31],inst_i[7],inst_i[30:25],inst_i[11:8],1'b0};

上述代码中:

复制代码
1
2
wire [31:0] jump_imm = {{19{inst_i[31]}},inst_i[31],inst_i[7],inst_i[30:25],inst_i[11:8],1'b0};

根据该命令的格式计算而来,最终要补满32位。
在这里插入图片描述
然后根据操作码,添加相应的case语句,进行命令的执行,命令的执行,需要根据命令的功能进行编程:
在这里插入图片描述该命令的功能是PC += imm;意思是,将立即数加上原有指令地址,赋给新的指令地址,此处立即数便相当于偏移地址信号。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
……………………………………… //B型指令 //第二期加入 `INST_TYPE_B: begin rd_data_o = 32'd0; //B型指令默认不回写寄存器,大多数的给零的地方都是防止默认情况下出现锁存器 rd_addr_o = 5'd0; rd_wen_o = 1'b0; case(funct3) ………………………………………… `INST_BNE: //不相等跳转的情况 begin jump_addr_o = (inst_addr_i + jump_imm) & {32{(~op1_i_equal_op2_i)}}; jump_en_o = ~op1_i_equal_op2_i; hold_flag_o = 1'b0; //在ctrl中单独给hold end ………………………………………… ……………………………………………

⑦上层例化部分需要重新接线进行连接。

较为简单此处就不贴程序了。

⑧重要总结!

上述框架搭好之后,就是各个命令的添加了,主要是两个部分需要着重注意:

(1)译码模块:
添加译码模块要看指令结构,就是指令的每一位代表什么含义,有没有目的寄存器啊,有没有两个源寄存器啊,是不是立即数的指令啊之类的。
这个部分要把指令译码为统一成op1_1和op2_i,rd_addr_o和reg_wen_o,如果没有其中的某些项,就给其置零传输到下一个执行模块中。例如jal指令:
在这里插入图片描述没有源寄存器、只有目的寄存器和立即数,则译码过程为:

复制代码
1
2
3
4
5
6
7
8
9
10
11
//J型指令 `INST_JAL: begin rs1_addr_o = 5'b0; rs2_addr_o = 5'b0; op1_o = {{12{inst_i[31]}},inst_i[19:12],inst_i[20],inst_i[30:21],1'b0}; op2_o = 32'b0; rd_addr_o = rd; reg_wen_o = 1'b1; end

将立即数放在操作数1中,并且使能回写信号,给出目的寄存器的地址。

(2)执行模块:
执行模块要看指令的功能;就是看前级电路输入的指令、指令地址、操作数1,操作数2,目的寄存器地址等等信号,该指令需要对上述信号做出什么操作?
在这里插入图片描述
比如,jal(jump and link跳转并链接)指令,在目的寄存器写入指令地址+4,下一条指令地址基于imm偏移地址进行跳转。则执行过程为:

复制代码
1
2
3
4
5
6
7
8
9
10
`INST_JAL: begin rd_data_o = inst_addr_i + 32'd4; //写入目的寄存器的数据 rd_addr_o = rd_addr_i; //目的寄存器地址 rd_wen_o = rd_wen_i; //此处为前级传递来的使能信号 jump_addr_o = op1_i + inst_addr_i; //跳转地址 jump_en_o = 1'b1; //跳转使能 hold_flag_o = 1'b0; end

其余指令的添加就比较类似了,文末给出程序文件。

四、指令测试

添加完BNE、LUI、JAL等指令,就可以使用官方测试文件进行指令测试了,为什么是这样呢?又怎样测试呢?
下图是官方的指令测试文件中的一部分:
在这里插入图片描述
每一条指令对应5个文件,我们暂时先关注其中两个文件:

红色框框内为指令测试的反汇编文件,蓝色框框内的文件为指令代码,可以直接读取执行。
关于红色框内的反汇编文件,打开后如下:
在这里插入图片描述
其中有li加载立即数,add加法指令,bne分支指令,后面还有lui指令等等;因为这里测试时用到了,所以前面才需要添加该指令;
第四列的s10,s11,t5,t4,gp等等都对应regs寄存器中的某个寄存器,总共32个:
在这里插入图片描述
虽然上面指令测试的反汇编文件中有很多条指令,但是最终都落脚到三个寄存器身上,
在这里插入图片描述
其中一个便是s11(x27):
在这里插入图片描述
可以看出s11为1说明测试通过,s11为0,说明测试失败;所以最后我们只要读取x27(s11)的值便可知道指令有没有通过。
tb文件编写如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
`timescale 1ns/1ns module tb; reg clk; reg rst_n; Yx_risc_v_soc Yx_risc_v_soc_inst( .clk(clk), .rst_n(rst_n) ); wire x3 = tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[3]; wire x26 = tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[26]; wire x27 = tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[27]; always #10 clk = ~clk; initial begin clk = 0; rst_n = 0; #30; rst_n = 1'b1; end //rom 初始值 initial begin //第一个参数为文件名,第二个参数为需要写入的寄存器 $readmemh("./inst_txt/rv32ui-p-add.txt",tb.Yx_risc_v_soc_inst.rom_inst.rom_mem); //通过 end integer r; initial begin wait(x26 == 32'd1); #200; if(x27 == 32'd1)begin $display("#########################"); $display("#########pass!!!#########"); $display("#########################"); end else begin $display("#########################"); $display("#########fail!!!#########"); $display("#########################"); $display("fail testnum = %2d",x3); for(r = 0; r < 32 ; r = r + 1)begin $display("x%2d register value is %d",r,tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[r]); end end end endmodule

在ModelSim中进行测试,指令通过:
在这里插入图片描述
同样的,我对很多指令进行了添加,添加时注意代码的复用,主要是下图黄色框框中的指令:
在这里插入图片描述
并挨个进行了测试,均通过:
在这里插入图片描述
可能测试方法有点繁琐了,后面再学习其他的便捷的测试方法。

总结

本期程序获取方法:关注公众号:FPGA学习者,后台回复【指令添加】即可获得我编写的以及B站:@外瑞罗格up主编写的程序。
在这里插入图片描述
临近写完本文,才发现我们的程序或许都有点小问题,但是不影响目前指令的测试。

往期精彩

和你一起从零开始写RISC-V处理器(1)
和你一起从零开始写RISC-V处理器(2)

最后

以上就是潇洒钻石最近收集整理的关于和你一起从零开始写RISC-V处理器(3)上期回顾一、指令的基本结构二、流水线冲刷机制三、模块更改过程四、指令测试总结往期精彩的全部内容,更多相关和你一起从零开始写RISC-V处理器(3)上期回顾一、指令内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(64)

评论列表共有 0 条评论

立即
投稿
返回
顶部