我是靠谱客的博主 潇洒钻石,最近开发中收集的这篇文章主要介绍和你一起从零开始写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模块


`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)上期回顾一、指令的基本结构二、流水线冲刷机制三、模块更改过程四、指令测试总结往期精彩所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

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

评论列表共有 0 条评论

立即
投稿
返回
顶部