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

RISC-V加法指令的实现(Ⅱ)

上期回顾

本文首发于公众号:FPGA学习者,关注公众号,获取更多资料与内容。
上期说到,要实现加法指令,需要编写以下八个模块:
在这里插入图片描述
先总体看一下,该部分的模块连接的框图如下:
在这里插入图片描述Yx_risc-v(名称根据自身喜好命名)内核内部模块连接框图为:
在这里插入图片描述


一、正片开始——编写各个模块

①pc_reg模块

在这里插入图片描述
pc_reg模块主要用来根据时钟信号产生指令地址,因为ROM中一般每个存储空间有8位,所以四个存储空间才能完全存放一个指令信号,故,该模块的指令地址信号每时钟下进行加4操作。代码如下:


//该模块主要产生指令地址信号
module pc_reg(
  input    wire       clk    ,
  input   wire       rst_n    ,
  output   reg [31:0]   pc_o      //输出的指令地址
);

  always@(posedge clk or negedge rst_n)begin
    if(~rst_n)begin
      pc_o <= 32'b0;
    end
    else begin
      pc_o <= pc_o + 3'd4;
    end
  end
endmodule
//复位的时候,指令地址指向第0个指令寄存器,ROM的最开始
//其余时刻,不断加4,因为每个指令占用4个空间

②if模块

在这里插入图片描述
if(instruction fetch)取指模块,将输入的指令地址,送至ROM中,ROM根据指令地址产生相应的指令信号送入if取指模块,if再将指令信号和指令地址信号输出至下一个模块。代码如下:


//取指模块,从ROM中取出指令信号,组合逻辑
module ifeth(    //由于if为verilog关键字,所以使用ifetch
  //from pc_reg
  input   wire [31:0]   pc_addr_i    ,
  //from rom
  input   wire [31:0]   rom_inst_i    ,      //取出的命令,rom返回的指令
  //to rom
  output   wire [31:0]   if2rom_addr_o  ,      //将指令地址给rom,rom返回指令
  //to if_id
  output   wire [31:0]   inst_addr_o    ,      //将指令地址送入下一个模块打拍
  output   wire [31:0]   inst_o            //将指令送入下一个模块打拍

);
  assign if2rom_addr_o = pc_addr_i;
  assign inst_addr_o = pc_addr_i;
  assign inst_o = rom_inst_i;
endmodule

③rom模块

在这里插入图片描述 rom模块根据取指模块给出的地址,将相应的指令返回给取指模块,代码如下:

//存放指令的ROM
module rom(
  input   wire   [31:0]   inst_addr_i,
  output   reg  [31:0]   inst_o
);
  reg [31:0] rom_mem [0 : 4095];      //4096个32bit空间
  //这里一次性实现命令的32位了,和原本想法略有不符
  //所以下面要一次进行加一的操作,所以才右移4位。
  always@(*)begin
    inst_o = rom_mem[inst_addr_i >> 2];  //右移两位,缩小四倍,实现每次加一操作
  end
endmodule

对于rom模块,可以使用8位的rom,然后再进行逻辑处理;此处使用的比较简单,直接为32位rom,将本来步进加4的地址信号,右移两位(除以4)实现步进加1操作。

④if_id模块

在这里插入图片描述
对于if_id模块,实现简单的打一拍操作,防止出现时序违例。代码如下:

//时序逻辑,进行打拍
`include  "defines.v"
module if_id(
  input   wire         clk      ,
  input   wire         rst_n      ,
  input   wire [31:0]   inst_i      ,
  input   wire [31:0]   inst_addr_i  ,
  output   wire [31:0]   inst_addr_o,        //打一拍之后的地址输出
  output   wire [31:0]   inst_o            //打一拍之后的指令输出
);
  //例化D触发器,实现打一拍的逻辑
  dff_set #(32) dff1(clk,rst_n,`INST_NOP,inst_i,inst_o);
  dff_set #(32) dff2(clk,rst_n,32'b0,inst_addr_i,inst_addr_o);

endmodule

上述代码中包含头文件defines.v,该文件中主要是相关命令的宏定义,该文件获取方式在文末给出。

因为后续还有模块会用到寄存器打拍的操作,所以将D触发器打拍单独写为一个模块,上述if_id模块对其进行了例化。D触发器模块代码如下:

//D触发器模块,将输入的数据打一拍
module dff_set #(
  parameter DW = 32
)
(
  input   wire           clk    ,
  input   wire           rst    ,
  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)begin
      data_o <= set_data;        //置位信号
    end
    else begin
      data_o <= data_i;
    end
  end
endmodule

⑤id模块

在这里插入图片描述
id模块对前级输入的指令信号进行译码,通过译码得出寄存器的地址信号,将该地址信号送至寄存器组,寄存器组返回相应的数据,然后向后级输出操作数1、操作数2、需要回写的目的寄存器地址和写寄存器使能信号。代码相对较长,此处仅放端口定义,完整代码获取方式文末给出。

该模块主要根据操作码和funct3功能码进行命令的判断。

module id(
  //from if_id
  input   wire   [31:0]   inst_i        ,        //前级输入的指令
  input   wire  [31:0]   inst_addr_i    ,        //前级输入的指令地址
  //to regs
  output   reg   [4:0]    rs1_addr_o  ,          //给寄存器组的地址
  output   reg   [4:0]   rs2_addr_o  ,          //给寄存器组的地址
  //from regs
  input   wire  [31:0]   rs1_data_i    ,        //从寄存器中取出的数据
  input   wire  [31:0]   rs2_data_i    ,        //从寄存器中取出的数据
  //to id_ex
  output   reg   [31:0]   inst_o      ,        //输出到后边的指令
  output   reg   [31:0]   inst_addr_o    ,        //输出到后边的指令地址
  output   reg   [31:0]   op1_o      ,        //操作数1
  output   reg   [31:0]   op2_o      ,        //操作数2
  output   reg   [4:0]     rd_addr_o    ,        //目的寄存器的地址,回写的时候用
  output   reg            reg_wen_o            //寄存器使能,写寄存器的时候用
);

⑥regs模块

在这里插入图片描述
寄存器组,一方面接收来自译码模块的地址信号并返回相应的数据信号,另一方面接收来自执行模块的地址信号、数据信号和使能信号。代码如下:


//寄存器组

module regs(
  input   wire       clk        ,
  input   wire      rst_n        ,
  //from id
  input   wire [4:0]   reg1_raddr_i  ,
  input   wire [4:0]   reg2_raddr_i  ,
  //to id
  output   reg [31:0]   reg1_rdata_o  ,
  output   reg [31:0]   reg2_rdata_o  ,
  //from ex                                  //从ex回来的地址和数据和使能信号
  input   wire [4:0]  reg_waddr_i  ,
  input   wire [31:0]  reg_wdata_i  ,
  input           reg_wen_i
);

  integer i;
  reg [31:0] regs [0 : 31];

  always@(*)begin
    if(~rst_n)begin
      reg1_rdata_o = 32'b0;
    end
    else if(reg1_raddr_i == 5'b0)
      reg1_rdata_o = 32'b0;
    else if(reg_wen_i && reg1_raddr_i == reg_waddr_i)    //这个地方主要是在指令冲突的时候
      reg1_rdata_o = reg_wdata_i;                //不需要通过读取回写之后的数据,直接进行判断后由该寄存器内部数据传递即可
    else
      reg1_rdata_o = regs[reg1_raddr_i];
  end
  always@(*)begin
    if(~rst_n)begin
      reg2_rdata_o = 32'b0;
    end
    else if(reg2_raddr_i == 5'b0)
      reg2_rdata_o = 32'b0;
    else if(reg_wen_i && reg2_raddr_i == reg_waddr_i)
      reg2_rdata_o = reg_wdata_i;
    else
      reg2_rdata_o = regs[reg2_raddr_i];
  end
  always@(posedge clk or negedge rst_n)begin
    if(~rst_n)begin                            //复位的时候,对寄存器组进行初始化
      for(i =  0 ; i < 31; i = i + 1)begin
        regs[i] <= 32'd0;    
      end
    end
    else if(reg_wen_i && reg_waddr_i != 5'b0) begin      //不能写入零寄存器
      regs[reg_waddr_i] <= reg_wdata_i;
    end
  end
endmodule

在上述代码的注释处描述的是指令冲突时的操作:如果第二条指令的源寄存器刚好是第一条指令的目的寄存器,(即:第二条指令的源寄存器地址等于第一条指令的目的寄存器地址),那就直接将执行模块得到的数据通过组合逻辑赋值给当前需要读出的源寄存器的数据,同时按照时序逻辑写入regs中去(每次回写都是必须要写的)。可参照下图以及上述程序进行理解上面那段话的含义。
在这里插入图片描述

⑦id_ex模块

在这里插入图片描述
对数据实现打一拍输出。

//进行打一拍输出
`include  "defines.v"
module id_ex(
  input   wire         clk      ,
  input   wire         rst_n      ,
  //from id
  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 ex
  output   wire [31:0]   inst_o    ,          //输出到后边的指令
  output   wire [31:0]   inst_addr_o  ,          //输出到后边的指令地址
  output   wire [31:0]   op1_o    ,          //操作数1
  output   wire [31:0]   op2_o    ,          //操作数2
  output   wire [4:0]     rd_addr_o  ,          //目的寄存器的地址,回写的时候用
  output   wire          reg_wen_o            //寄存器使能,写寄存器的时候用

);
  dff_set #(32) dff1(clk,rst_n,`INST_NOP,inst_i,inst_o);
  dff_set #(32) dff2(clk,rst_n,32'd0,inst_addr_i,inst_addr_o);
  dff_set #(32) dff3(clk,rst_n,32'd0,op1_i,op1_o);
  dff_set #(32) dff4(clk,rst_n,32'd0,op2_i,op2_o);
  dff_set #(5) dff5(clk,rst_n,5'd0,rd_addr_i,rd_addr_o);
  dff_set #(1) dff6(clk,rst_n,1'b0,reg_wen_i,reg_wen_o);

endmodule

⑧ex模块

在这里插入图片描述
ex模块,实现执行和回写两个部分的内容,主要根据操作码、功能码funct3和功能码funct7来判断命令具体是哪一条,然后再执行相应的操作。

将操作后的数据根据地址信号和使能信号,回写到regs寄存器数组中。由于该部分代码较长,仅放端口定义,完整代码获取方式文末给出。


`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
);

好了,至此,所有的模块都绿了,这样就行了嘛?当然不行,要全绿才可以!下面要编写顶层模块将上述除ROM外的所有模块进行例化。

二、顶层模块搭建

在这里插入图片描述
好了,这下全绿了,该部分就是将上述除ROM外的七个模块进行例化,注意之间的连线,一步一步来,尽量不要出错,代码过长,此处仅放置端口定义,完整代码获取方式在文末给出:

//顶层文件
//将除了ROM之外的模块连接起来,ROM为外设,不是内核
`include  "defines.v"
`timescale 1ns/1ns
module Yx_risc_v(
  input   wire       clk      ,
  input   wire       rst_n      ,
  input   wire [31:0] inst_i      ,
  output   wire [31:0] inst_addr_o
);

除此之外,还要编写一个soc顶层文件,例化该内核模块和rom模块,代码如下:

//顶层文件
//`include  "defines.v"
module Yx_risc_v_soc(
  input   wire       clk  ,
  input   wire       rst_n
);
  //Yx_risc_v to rom
  wire [31:0] Yx_risc_v_inst_addr_o;
  //rom to Yx_risc_v
  wire [31:0] rom_inst_o;
  Yx_risc_v Yx_risc_v_inst(
    .clk          (clk                ),
    .rst_n        (rst_n              ),
    .inst_i        (rom_inst_o          ),
    .inst_addr_o    (Yx_risc_v_inst_addr_o  )
);
  rom rom_inst(
    .inst_addr_i      (Yx_risc_v_inst_addr_o  ),
    .inst_o        (rom_inst_o          )
);
endmodule

如此便可形成如下结构:

在这里插入图片描述

三、测试文件编写

或许你正看上面的文章看的津津有味,有没有突然想到,哎?光说从rom中读取指令,那rom中的指令从哪里来呢?

这里使用了一个系统函数:$readmemb,通过该函数将外部.txt文件中的指令读取到rom中即可。
testbench文件编写如下:


`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)
);
  always #10 clk = ~clk;
  initial begin
    clk = 0;
    rst_n = 0;
    #30;
    rst_n = 1'b1;
  end
  //rom 初始值
  initial begin  //第一个参数为文件名,第二个参数为需要写入的寄存器
    $readmemb("inst_data_ADD.txt",tb.Yx_risc_v_soc_inst.rom_inst.rom_mem);
  end
  initial begin
    while(1)begin
      @(posedge clk)
      $display("x27 register value is %d",tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[27]);
      $display("x28 register value is %d",tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[28]);
      $display("x29 register value is %d",tb.Yx_risc_v_soc_inst.Yx_risc_v_inst.regs_inst.regs[29]);
      $display("---------------------------");
      $display("---------------------------");
    end
  end
endmodule

编写的.txt文件如下:


00000000111100000000110110010011
00000001100100000000111000010011
00000001110011011000111010110011

对应的指令为:
在这里插入图片描述这样运算得到的结果应该为40;ModelSim中仿真结果如下:
在这里插入图片描述
结果是正确的,编写的代码应该也问题不大。看看波形吧,如下:
在这里插入图片描述
确实也是分三个时钟周期执行完一条指令。
好,先测到这里,其他详细测试暂时不写了;今天内容有些超量了哈哈。

总结

上述代码,关注公众号:FPGA学习者,后台回复【指令添加】即可获取全部工程文件。

往期精彩

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值