使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)

使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)

  • 前期基础知识的了解
    • 如何区分写满
    • FIFO的逻辑框图
  • 双端口RAM的生成
    • RAM IP核的例化
  • 写控制模块与读控制模块的设计
    • 写控制模块
    • 读控制模块
  • 顶层文件的书写
    • 测试文件的书写

前期基础知识的了解

1.当采集数据的时候由慢时钟域到快时钟域,肯定需要一个buffer做缓冲,这样才才能完成时钟域的转换,一般这种情况下都选择FIFO来做缓冲。
2.当读写FIFO的时钟不同那么称为异步FIFO。
3.FIFO就是一个“环形存储器”读操作会把读指针指向下一个读数据,写操作会把写指针指向下一个写数据地址。当读指针追上写指针时称为读空,当写指针追上读地址时称作写满。

如何区分写满

假设存储器的深度为8,那么在表示数据地址的时候三位2进制数就可以了,但是为了区分写满和读空则再增加1位二进制的标志位,放在最前面。

写数据的地址应该在写的时钟域里面产生的,读数据的地址在读的时钟域里面产生,如果切换时钟域将这两个信号简单的拿过来进行比较的话很容易出现亚稳态,这个时候为了避免亚稳态就通过编码地址来进行比较

格雷码:将二进制码从左位第二位起,依次将每一位与左边一位异或,作为对应格雷码在该位的值,最左边一位不变。另一种方法为先将整个数据向右移动一位,然后与原数据按位异或,得到的数据就是我们所要的格雷码。

将格雷码转换为二进制码方法一样。
格雷码的相邻两个数据之间只有一位数据发生了改变,在传输过程中就相当与只有一个数据发生了改变,就等同于打两拍消除亚稳态的作用。

8个深度的存储地址二进制编码     格雷码
0=    4'b0_000       						 //0_000
1=    4'b0_001        						//0_001
2=    4'b0_010        						//0_011
3=    4'b0_011        						//0_010
4=    4'b0_100        						//0_110
5=    4'b0_101        						//0_111
6=    4'b0_110       					   //0_101
7=    4'b0_111        						//0_100
0=    4'b1_000        						//1_100
1=    4'b1_001        						//1_101
2=    4'b1_010        						//1_111
3=    4'b1_011        						//1_110
4=    4'b1_100        						//1_010
5=    4'b1_101       						 //1_011
6=    4'b1_110        						//1_001
7=    4'b1_111        						//1_000

当写满的时候其实就是写比读快了一圈,其实在写第二圈的时候地址的标志位就变为1,所以区分写满的时候可以这样设置,以十进制这么写是非常合理的;**但是转换成格雷码之后则需要前两位都取反**
assign    w_full=(r_addr[2:0] == w_addr[2:0]&&r_addr[3] == (~w_addr[3]))
而读空的话其实就是所有的地址都一样
assign    r_empty=(r_addr[3:0] == w_addr[3:0]);

FIFO的逻辑框图

使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)_第1张图片
这个图里面的waddr指的是二进制码的地址,而wptr是经过格雷码转换之后的地址

rptr就是经过格雷码转换之后的读时钟域的地址,将这个地址再输出到写控制端口,经过延时两拍之后进行比较。

关于延时两拍之后再进行比较会不会出现已经读空或写满但是漏报的情况?
这个是不会的,对于读指针来说的话最差的情况不就是写满,写满的话那么我的写指针在两个时钟信号后是不会变化的
如果是读空的话同样的道理,我的读指针是不会发生变换的,那么你传过来的写指针其实还是和我两个时钟之前的读指针进行比较

格雷码计数器:产生格雷码地址的
使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)_第2张图片
转换格雷码的时候可以看到使用的是加法器后面的地址而不是输出的地址,这是因为在转换格雷码的后面也有一个寄存器,这样的话就可以是得输出到外面的格雷码的地址核二进制的地址保持一致

双端口RAM的生成

先调用一下RAM的IP核
这里的RAM是一个双口的RAM,因为我们设置的FIFO是异步FIFO,所以需要双口的RAM,写与读的时钟不一致。
使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)_第3张图片
M9K就是我们FPGA内部的memeory的资源,这个资源已经存在于我们的FPGA内部了,你用不用他都在那了。
使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)_第4张图片
Byte enable端口:比如一个16位的数据,如果这个端口的的信号是2’b01,就意味着低八位数据写进去了,高八位数据没有写进去。
使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)_第5张图片
在读数据端口不加寄存器,这样就只是延时一拍,给地址就出数据
使用RAM资源生成一个读写不同步的FIFO(包含源程序和测试程序)_第6张图片
这是我们的仿真库
左下角会提示我们使用了多少资源
创建完成之后就要开始例化了,重新创建一个文件把这个ip核给例化一下

RAM IP核的例化

module	fifomem(
		input	wire			w_clk,
		input	wire			r_clk,
		input	wire			w_en,		//来自于FIFO的写控制模块
		//RAM不需要读使能就可以,给一个地址他就出数据
		input	wire			w_full,		//来自FIFO的写控制模块
		input	wire	[7:0]	w_data,		//来自于外部数据源
		input	wire	[8:0]	w_addr,		//来自于我们的FIFO写控制模块
		input	wire			r_empty,
		input	wire	[8:0]	r_addr,		//来自于FIFO的读控制模块
		output	wire	[7:0]	r_data		//读数据从内部ram中读取
		
);

wire	ram_w_en;
assign	ram_w_en = w_en&(~w_full);//这个控制信号都是根据FIFO的逻辑框图来看着写的


dp_ram_512_8	dp_ram_512_8_inst (
	.wraddress ( w_addr[7:0] ),
	.wren ( ram_w_en ),	
	.wrclock ( w_clk ),
	.data ( w_data ),
	.rdclock ( r_clk ),
	.rdaddress ( r_addr[7:0] ),
	.q ( r_data )
	);


endmodule

写控制模块与读控制模块的设计

写控制模块

//这个是用来描述写的控制信号
module	w_ctrl(
		input	wire		w_clk,//写时钟
		input	wire		rst_n, //复位
		input	wire		w_en,	//写使能
		input	wire [8:0]	r_gaddr,//读时钟域过来的格雷码读地址指针
		output	reg			w_full, //写满标志
		output	wire [8:0]	w_addr, //假设这次是256深度的FIFO
		output	wire [8:0]	w_gaddr //写FIFO地址格雷码编码
);
reg		[8:0]	addr;		//从格雷码计算器的图可以看出来最终输出的都是经过寄存器的数据
reg		[8:0]	gaddr;		//从格雷码计算器的图可以看出来最终输出的都是经过寄存器的数据
wire	[8:0]	addr_wire;
wire	[8:0]	gaddr_wire;
reg		[8:0]	r_gaddr_d1,r_gaddr_d2;

//这是将输入的格雷码打两拍,避免亚稳态
always @(posedge w_clk or negedge rst_n)
			if(rst_n == 1'b0)
				{r_gaddr_d2,r_gaddr_d1} <= 18'd0;
			else
				{r_gaddr_d2,r_gaddr_d1} <= {r_gaddr_d1,r_gaddr};


//产生写RAM的地址指针 二进制
assign	addr_wire = addr + ((~w_full)&w_en);

always @(posedge w_clk or negedge rst_n)
			if(rst_n == 1'b0)
				addr <= 9'd0;
			else
				addr <= addr_wire;


assign	w_addr = addr;

//转换格雷码地址
assign	gaddr_wire = (addr_wire>>1)^addr_wire;//^这个符号的意思就是异或
always @(posedge w_clk or negedge rst_n)
		if(rst_n == 1'b0)
				gaddr <= 9'd0;
		else
				gaddr <= gaddr_wire;

assign	w_gaddr = gaddr;
//产生写满标志
always @(posedge w_clk or	negedge	rst_n)
		if(rst_n == 1'b0)
			w_full <= 1'b0;
		//else if({~gaddr_wire[8],gaddr_wire[7:0]} == r_gaddr_d2) //输入比较的话应该拿输入与wire类型的数据进行比较,二进制比较这样就可以
		else if({~gaddr_wire[8:7],gaddr_wire[6:0]} == r_gaddr_d2)
		//格雷码比较是需要把高位和次高位都取反
			w_full<=1'b1;
		else
			w_full<=1'b0;

endmodule

读控制模块

module	r_ctrl(
		input	wire			r_clk,	//读时钟
		input	wire			rst_n,	
		input	wire			r_en,	//读使能
		input	wire	[8:0]	w_gaddr,//写时钟域的写地址指针
		output	reg				r_empty,//读空标志
		output	wire	[8:0]	r_addr,//读时钟域的地址
		output	wire	[8:0]	r_gaddr  //读格雷码的地址

);


reg		[8:0]	addr;
reg		[8:0]	gaddr;
wire	[8:0]	addr_wire;
wire	[8:0]	gaddr_wire;
reg		[8:0]	w_gaddr_d1,w_gaddr_d2;

always	@(posedge r_clk or negedge	rst_n)
			if(rst_n == 1'b0)
				{w_gaddr_d2,w_gaddr_d1} <= 18'd0;
			else
				{w_gaddr_d2,w_gaddr_d1} <= {w_gaddr_d1,w_gaddr};


//读二进制的地址
assign r_addr = addr;
assign	addr_wire = addr + ((~r_empty)&(r_en));
always	@(posedge r_clk or negedge rst_n)
			if(rst_n == 1'b0)
				addr <= 9'd0;
			else
				addr <= addr_wire;

//读格雷码的地址				
assign	r_gaddr = gaddr;

assign	gaddr_wire = (addr_wire>>1)^addr_wire;

always @(posedge r_clk or negedge rst_n)
			if(rst_n == 1'b0)
					gaddr <= 9'd0;
			else
					gaddr <= gaddr_wire;
//读空标志的产生
always	@(posedge r_clk or negedge	rst_n)
			if(rst_n == 1'b0)
					r_empty <= 1'b0;
			else if(gaddr_wire == w_gaddr_d2)
					r_empty <= 1'b1;
			else
					r_empty <=1'b0;
endmodule

顶层文件的书写

module ex_fifo(
		input	wire		w_clk,
		input	wire		r_clk,
		input	wire		rst_n,
		input	wire		w_en,
		input	wire	[7:0] w_data,
		output	wire		w_full,
		input	wire		r_en,
		output	wire	[7:0] r_data,
		output	wire		r_empty


);

wire	[8:0]	r_gaddr;
wire	[8:0]	w_addr;
wire	[8:0]	w_gaddr;
wire	[8:0]	r_addr;

w_ctrl	w_ctrl_inst(
		.w_clk(w_clk),//写时钟
		.rst_n(rst_n), //复位
		.w_en(w_en),	//写使能
		.r_gaddr(r_gaddr),//读时钟域过来的格雷码读地址指针
		.w_full(w_full), //写满标志
		.w_addr(w_addr), //假设这次是256深度的FIFO
		.w_gaddr(w_gaddr) //写FIFO地址格雷码编码
);

fifomem		fifomem_inst(
		.w_clk(w_clk),
		.r_clk(r_clk),
		.w_en(w_en),		//来自于FIFO的写控制模块

		.w_full(w_full),		//来自FIFO的写控制模块
		.w_data(w_data),		//来自于外部数据源
		.w_addr(w_addr),		//来自于我们的FIFO写控制模块
		.r_empty(r_empty),
		.r_addr(r_addr),		//来自于FIFO的读控制模块
		.r_data(r_data)		//读数据从内部ram中读取
		
);

r_ctrl	r_ctrl_inst(
		.r_clk(r_clk),	//读时钟
		.rst_n(rst_n),	
		.r_en(r_en),	//读使能
		.w_gaddr(w_gaddr),//写时钟域的写地址指针
		.r_empty(r_empty),//读空标志
		.r_addr(r_addr),//读时钟域的地址
		.r_gaddr(r_gaddr)  //读格雷码的地址

);

endmodule

可以看到顶层文件其实就是例化前面已经写好的三个模块

测试文件的书写

`timescale	1ns/1ns

module	tb_ex_fifo;
reg		r_clk,w_clk,rst_n;
reg		w_en;
reg	[7:0]	w_data;
reg		r_en;
wire [7:0]	r_data;
wire	w_full;
wire	r_empty;
parameter	CLK_P = 20;

//写的初始化模块
initial		begin
			r_clk=0;
			w_clk=0;
			rst_n=0;
			#200
			rst_n=1;
end

//读的初始化模块
initial		begin
			w_en = 0;
			w_data = 0;
			#300
			write_data();
end

initial		begin
			r_en = 0;
			@(posedge w_full)
			#40;
			read_data();
end

always	# (CLK_P/2)	r_clk =	~r_clk;
always	# (CLK_P/2)	w_clk = ~w_clk;

ex_fifo		ex_fifo_inst(
		.w_clk(w_clk),
		.r_clk(r_clk),
		.rst_n(rst_n),
		.w_en(w_en),
		.w_data(w_data),
		.w_full(w_full),
		.r_en(r_en),
		.r_data(r_data),
		.r_empty(r_empty)


);


task	write_data();
		integer		i;
		begin
			for(i=0;i<258;i=i+1)
			begin
				@(posedge	w_clk);
				w_en = 1'b1;
				w_data = i;
			
			end
			@(posedge w_clk);
			w_en=1'b0;
			w_data = 0;

		end

endtask

task	read_data();
		integer		i;
		begin
			for(i=0;i<256;i=i+1)
				begin
					@(posedge  r_clk);
					r_en = 1'b1;
				end
				@(posedge r_clk);
					r_en = 1'b0;
					w_data = 0;
			
		end

endtask

endmodule

在这个例子里面的读写时钟是一样的,这是便于测试,其实现在就完全可以将其设置为不同频率的时钟

你可能感兴趣的:(教程,FIFO,RAM,verilog,fpga,verilog,RAM,FIFO,Verilog)