上篇文章我已经详细讲述了FPGA的3种常见存储器的调用方法,链接如下
https://blog.csdn.net/weixin_46188211/article/details/122839616
这篇文章我将依次讲解3种存储器的代码,对这三种存储器的使用进行实战
目录
一、RAM读写
首先,设计一个程序框图,由于既要用到存储器的接收,又要用到存储器的发送,所以我们让存储器接收自己发送的数据,再对数据进行发送:
在这里我使用真双口RAM,首先A口写(数据依次+1,地址依次+1),A口写满之后,然后B口读,B口读空了之后(应该把地址和数据清空),B口写(数据依次+1,地址依次+1),B口写满了之后,A口读,A口读空了之后(应该把地址和数据清空)然后A口写,进行循环。
首先创建一个真双口RAM ,它的数据位宽为16,地址深度为4,把RAM例化进程序
wire clka; //由于clk用assign连接,所以修改为wire型
reg ena;
reg wea;
reg [3:0] addra;
reg [15:0] dina;
wire[15:0] douta;
wire clkb;
reg enb;
reg web;
reg [3:0] addrb;
reg [15:0] dinb;
wire[15:0] doutb;
RAM inst_RAM (
.clka(clka), // input wire clka
.ena(ena), // input wire ena
.wea(wea), // input wire [0 : 0] wea
.addra(addra), // input wire [3 : 0] addra
.dina(dina), // input wire [15 : 0] dina
.douta(douta), // output wire [15 : 0] douta
.clkb(clkb), // input wire clkb
.enb(enb), // input wire enb
.web(web), // input wire [0 : 0] web
.addrb(addrb), // input wire [3 : 0] addrb
.dinb(dinb), // input wire [15 : 0] dinb
.doutb(doutb) // output wire [15 : 0] doutb
);
assign clka=clk;
assign clkb=clk;
实现循环需要使用状态机,状态转移图如下:
这里需要注意的是,在每一次写满或者读空的时候,最好将din和addr清零,保证下次存储时A/B口为空,代码如下:
reg [2:0] state;
parameter IDLE=3'd0;
parameter S0 =3'd1;//A写
parameter S1 =3'd2;//B读
parameter S2 =3'd3;//B写
parameter S3 =3'd4;//A读
parameter maxA=4'd15;//A口地址已满
parameter maxB=4'd15;//B口地址已满
always@(posedge clk)begin
if(reset)begin
ena <=1'd0;
wea <=1'd0;
addra<=4'd0;
dina <=16'd0;
enb <=1'd0;
web <=1'd0;
addrb<=4'd0;
dinb <=16'd0;
end
else begin
case(state)
IDLE:begin
ena <=1'd0;
enb <=1'd0;
state<=S0;
end
S0 :begin//A写
if(addra==maxA)begin
state<=S1;
wea <=1'd0;
addra<=4'd0;
dina <=16'd0;
end
else begin
state<=S0;
ena <=1'd1;
wea <=1'd1;
addra<=addra+4'd1;//地址+1
dina <=dina+16'd1;//数据+1
end
end
S1 :begin//B读
if(addrb==maxB)begin
web <=1'd1;
addrb<=4'd0;
dinb <=16'd0;
state<=S2;
end
else begin
state<=S1;
enb <=1'd1;
web <=1'd0;
addrb<=addrb+4'd1;
end
end
S2 :begin//B写
if(addrb==maxB)begin
state<=S3;
web <=1'd1;
addrb<=4'd0;
dinb <=16'd0;
end
else begin
state<=S2;
web<=1'd1;
dinb<=dinb+16'd1;
addrb<=addrb+4'd1;
end
end
S3 :begin//A读
if(addra==maxA)begin
state<=S0;
wea<=1'd1;
dina<=15'd0;
addra<=1'd0;
end
else begin
state<=S3;
wea<=1'd0;
addra<=addra+4'd1;
end
end
default:begin
state<=IDLE;
end
endcase
end
end
对RAM进行仿真:
A写完了B读
B口读完B口写
B写完了A读
A读完了A写
二、ROM
与RAM类似,首先创建一个ROM
这里与RAM不同的是,需要将ROM初始化,添加初始化文件(即把数据写入ROM)
添加coe文件(在文件名后面后缀.coe即可)
MEMORY_INITIALIZATION_RADIX=16; //表示ROM内容的数据格式是16进制
MEMORY_INITIALIZATION_VECTOR=
1,
2,
3,
4,
5,
6,
7,
8,
9,
a,
b,
c,
d,
e,
f; //每个数据后面用逗号或者空格或者换行符隔开,最后一个数据后面加分号
把.coe文件放在IP核里面, 具体位置在:文件名.srcs\sources_1\ip
若 Design Runs下面的ip核出现“√”说明IP核生成成功
生成IP核完成之后,接下来将ROM例化到程序里面
wire clka; //由于clk用assign连接,所以修改为wire型
reg ena;
reg [3:0] addra;
wire[31:0] douta;
wire clkb;
reg enb;
reg [3:0] addrb;
wire[31:0] doutb;
ROM inst_ROM (
.clka(clka), // input wire clka
.ena(ena), // input wire ena
.addra(addra), // input wire [4 : 0] addra
.douta(douta), // output wire [31 : 0] douta
.clkb(clkb), // input wire clkb
.enb(enb), // input wire enb
.addrb(addrb), // input wire [4 : 0] addrb
.doutb(doutb) // output wire [31 : 0] doutb
);
由于ROM为只读存储器,只能读取ROM,不能往里面写(只有dout没有din)所以程序设计比较简单:
一共有0~9、a~f个数据,A口读前10个数据,B口读后面6个数据,将ROM读空,代码如下:
reg [1:0] state;
parameter IDLE=2'd0;
parameter S0 =2'd1;
parameter S1 =2'd2;
always@(posedge clk)begin
if(reset)begin
ena<=1'd0;
enb<=1'd0;
addra<=4'd0;
addrb<=4'd0;
state<=IDLE;
end
else begin
case(state)
IDLE:begin
ena<=1'd0;
enb<=1'd0;
addra<=4'd0;
addrb<=4'd0;
state<=S0;
end
S0:begin
if(addra==4'd9)begin
ena<=1'd0;
addra<=addra;
state<=S1;
end
else begin
ena<=1'd1;
addra<=addra+4'd1;
state<=S0;
end
end
S1:begin
if(addra==4'd5)begin
enb<=1'd0;
addrb<=addrb;
state<=IDLE;
end
else begin
enb<=1'd1;
addrb<=addrb+4'd1;
state<=S1;
end
end
endcase
end
end
然后我们可以看一下仿真结果,a端口实现了1-9的读取;但是发现在b端口读的时候依然是从1开始的,而不是期待的从a开始,可见ROM的两个端口的读取是互相独立的,不会因为其中一个读走了前面的数据,而影响另一个端口 ,每一个端口是独立的地址,这一点要注意
三、FIFO
FIFO是一种先入先出的存储器,先写入的数据先读出,它没有地址
创建一个fifo,这里我创建的是异步fifo,因为fifo通常用来跨时钟域
接下来我们把fifo例化到程序里面
reg [3:0] din;
reg wr_en;
reg rd_en;
wire almost_full;
wire almost_empty;
wire rd_rst_busy;
wire wr_rst_busy;
FIFO inst_FIFO (
.rst(rst), // input wire rst
.wr_clk(clk), // input wire wr_clk
.rd_clk(clk), // input wire rd_clk
.din(din), // input wire [3 : 0] din
.wr_en(wr_en), // input wire wr_en
.rd_en(rd_en), // input wire rd_en
.dout(dout), // output wire [3 : 0] dout
.full(full), // output wire full
.almost_full(almost_full), // output wire almost_full
.empty(empty), // output wire empty
.almost_empty(almost_empty), // output wire almost_empty
.wr_rst_busy(wr_rst_busy), // output wire wr_rst_busy
.rd_rst_busy(rd_rst_busy) // output wire rd_rst_busy
);
用fifo实现以下功能:先向fifo中写入16‘d0~f,之后再把数据从fifo中读出来,代码如下:
reg [1:0] state;
parameter IDLE=2'd0;
parameter WR=2'd1;//写FIFo
parameter RD=2'd2; //读FIFO
always@(posedge clk)begin
if(reset)begin
state<=IDLE;
din <=4'd0;
wr_en<=1'd0;
rd_en<=1'd0;
end
else begin
case(state)
IDLE:begin
state<=WR;
din <=4'd0;
wr_en<=1'd0;
rd_en<=1'd0;
end
WR:begin
if(!wr_rst_busy)begin//写复位不忙
wr_en<=1'd1;
din<=4'd0;
if(almost_full)begin//写满
wr_en<=1'd0;
din<=din;
state<=RD;
end
else begin
wr_en<=1'd1;
din<=din+4'd1;//写数据+1
end
end
end
RD:begin
if(!rd_rst_busy)begin
rd_en<=1'd1;
if(almost_empty)begin
rd_en<=1'd0;
state<=IDLE;
end
else begin
rd_en<=1'd1;
end
end
end
endcase
end
end
仿真结果如下:
这里fifo从0~f之后又记数了0和1,这是因为fifo的实际深度是18
数据写过程:wr_rst_busy=0,写使能拉高,如果almost_full=1表示写满,写使能拉低;否则输入数据din加1
数据读过程:rd_rst_busy=0,读使能拉高,如果almost_empty=1表示读空,读使能拉低;否则读使能拉高
这篇文章把FPGA里面的三种存储器都做了小工程,完整工程集中在一个文件里,需要的话可以自行下载,已上传csdn