概述
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模块
`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模块
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触发器修改如下:
//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触发器时进行相应的修改:
//对D触发器的置位条件进行了修改
dff_set #(32) dff1(clk,rst_n,hold_flag_i,`INST_NOP,inst_i,inst_o);
④id模块
从这个图可以看出,B型指令和R型指令译码类似,不同之处在于B型指令没有目的寄存器,所以也不需要回写信号,至于跳转地址时所需要的立即数处理,放在执行模块中去。
R型指令的译码
……………………………………
//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型指令的译码
……………………………………
//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模块主要是确定跳转地址的产生,以及跳转使能信号的产生。新定义三个输出信号,并且不要忘记在其他不要用到跳转信号的时候将该三处信号置零。
`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};
上述代码中:
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;意思是,将立即数加上原有指令地址,赋给新的指令地址,此处立即数便相当于偏移地址信号。
………………………………………
//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指令:
没有源寄存器、只有目的寄存器和立即数,则译码过程为:
//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偏移地址进行跳转。则执行过程为:
`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文件编写如下:
`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)上期回顾一、指令的基本结构二、流水线冲刷机制三、模块更改过程四、指令测试总结往期精彩所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复