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学习者,后台回复【指令添加】即可获取全部工程文件。