FPGA通用FIFO设计

摘要介绍

对于fifo的设计,相关的资料有很多,但是针对一些特殊的应用,网上就比较少了。所以
本文给出的fifo具有通用性,适用于各行各业,各种场景。其关键点在于如何实现输入输出位宽不一致,如何处理深度不是2的幂的情况,实现溢出警告处理,数据指针功能,first word full through功能。

本文适合对fifo有一定熟悉程度的工程师阅读,进阶学习。

FIFO整体设计结构

在这里插入图片描述
fifo最基本的模块有:

  • 写地址指针计数模块,写到读指针格雷码转换,同步电路
  • 读地址指针计数模块,读到写指针格雷码转换,同步电路
  • 空满标志判断模块
  • 双端口ram
    各部分协同工作,一起完成一个异步fifo的功能。

这里不对这些基本的模块做详细论述了,我们将主要讲一些其他特殊的功能,以及实现方法。

1 输入输出位宽不一致

fifo不仅可以完成数据的缓冲,时钟域转换,还可以很轻松地完成数据位宽的转换。
其主要部分是一个输入输出位宽可变的bram,要做到通用信,还必须参数化这些位宽和地址深度。

1.1位宽可变的bram的实现

实现这个ram需要分两种情况:写位宽大于读,读位宽大于写
参数定义

localparam MEM_DW = RAMA_DW>RAMB_DW?RAMB_DW:RAMA_DW;
localparam MEMA_RATIO = RAMA_DW/MEM_DW;
localparam MEMB_RATIO = RAMB_DW/MEM_DW;
localparam MEM_DEPTH = (2**RAMA_DW)*MEMA_RATIO ;
localparam ADDRA_LOW=clogb2(MEMB_RATIO) - 1;
localparam ADDRB_LOW=clogb2(MEMA_RATIO) - 1;

1.1.1 写位宽小于读位宽

对于这种情况下,定义两个mem变量,位宽与写位宽相同。写数据时候轮流写入mem,读数据时候一起读出来。具体实现方法如下:

generate 
	if(MEMA_RATIO ==1)begin
	for (i=0;i<MEMA_RATIO ;i=i+1)begin:ram_inferA
		sdpram #(
			.RAM_WIDTH(MEM_DW),
			.RAM_DEPTH(2**RAMB_AW)
		)ram1(
				.addra(addra[RAMA_AW-1:ADDRA_LOW]),
				.addrb(addrb),
				.dina(dina),
				.clka(clka),
				.clkb(clkb),
				.wea(wea&(addra[ADDRA_LOW-1:0])),
				.enb(enb),
				rstb(rstb),
				.regceb(regceb),
				.doutb(doutb[MEM_DW*(i+1)-1:MEM_DW*i])
			);
	end else begin

1.1.2 写位宽大于读位宽

对于这种情况下,定义一个mem变量,位宽与读位宽相同。写数据时候直接写入mem,读数据时候根据ADDRA_LOW个低位读地址来切换读出的数据。具体实现方法如下:

for (i=0;i<MEMA_RATIO ;i=i+1)begin:ram_inferA
	sdpram #(
		.RAM_WIDTH(MEM_DW),
		.RAM_DEPTH(2**RAMB_AW)
	)ram1(
			.addra(addra),
			.addrb(addrb[RAMB_AW-1:ADDRB_LOW]),
			.dina(dina[MEM_DW*(i+1)-1:MEM_DW*i]),
			.clka(clka),
			.clkb(clkb),
			.wea(wea),
			.enb(enb),
			rstb(rstb),
			.regceb(regceb),
			.doutb(s_ram_data[i])
		);
	assign doutb=s_ram_data[addrb[ADDRB_LOW-1:0]];
endgenerate

1.2 读写指针的比较

由于位宽不相等,在读写指针之间相互运算的时候,操作方法也是有区别的。例如写8bit,读16bit,那么写指针同步到读时钟域的时候必须要除以2,位宽相差4倍就除以4,以此类推。具体代码如下:

rbin_next=(ren&~rempty)?(rbin+1):rbin;
rgray_nex=(rd_dw<=wr_dw)?(rbin_nex[rd_aw:sh_bits]>>1)^rbin_next[rd_aw:sh_bits]:(rbin_next>>1)^rbin_next;

写也是一样的道理

wbin_next=(wen&~wfull)?(wbin+1):wbin;
wgray_nex=(wd_dw<=rd_dw)?(wbin_nex[wr_aw:sh_bits]>>1)^wbin_next[wr_aw:sh_bits]:(wbin_next>>1)^wbin_next;

1.3读写计数

读写计数主要是为了给用户一个具体的数据,方便用户判断已经有多少个数据写入了,或者可读出多少个数据,具体会用在一些固定长度的数据包读写的实例中,但是需要特别注意的是这个读写的计数在同时读写时,有延迟,主要是地址指针异步同步所带来的延迟。
在计算读写计数值时候也需要考虑因位宽不同而导致的误差。
计算写计数时候如果写位宽小于读位宽,则将读位宽乘以比率。
计算读计数时候如果读位宽小于写位宽,则将写位宽乘以比率。
代码:

wr_data_cnt_val = wr_dw <= rd_dw? wbin_next[wr_aw:0] - {wq2_rptr_bin[aw_base:0],{sh_bits{1'b0}}} : wbin_nex[wr_aw:0] - wq2_rptr_bin[aw_base:0];
rd_data_cnt_val = rd_dw <= wr_dw ?{rq2_wptr_bin[aw_base:0],{sh_bits{1'b0}}} - rbin_next[rd_aw:0] :rq2_wptr_bin[aw_base:0]-rbin_next[rd_aw:0];

2 fifo深度不是2的幂

很多时候实际项目中的fifo深度并不是2的幂,如果硬是用2的幂的fifo,那么将会浪费很多硬件资源,为了节省bram或者dram,本章给出一种方法实现任意深度的异步fifo。

2.1 格雷码的对称性

格雷码是具有对称性的,比如深度为三(下文均以3为例),取

二进制格雷码
000000
001001
010011
011010
对称线
100110
101111
110101
111100

仔细观察表格中,格雷码以3和4中间为分界线,最高位相反,上下对称,任意对称的两个数只有一个bit不同。

2.2 计数指针到格雷码的映射

读写地址是连续的0,1,2…;本质上是计数器,在计到规定的深度时回零。如此一来如果这个深度不是2的幂,将会造成在回零点时候格雷码多位发生变化,失去了格雷码的意义。
按照以下的方法可以解决这个问题:
1 读写地址是连续的0,1,2;本质上是计数器,但是需要在判断后两位为depth-1时把最高位取反,然后后面的位清零(因为mem深度就是3)。
举例: 0,1,2,4,5,6,0,1,2,4,5,6 …如此循环计数
2 格雷码转换,对于最高位为0的addr,只需要加上一个数字映射到对应的格雷码,比如0地址对应格雷码是001和110,1地址对应011和111,2地址对应010和101,所以把addr+(depth_ceil-depth)来进行地址的映射,再将映射的地址转换成格雷码,对于最高位为1 的addr不需要加这个数字映射。
3 格雷码同步到彼此的时钟域,将格雷码重新转换为对应的二进制,这个二进制是映射过的二进制,所以应根据最高位是0 减去(depth_ceil-depth),最高位为1不需要减,这样就得到了同步到彼此时钟域的二进制地址(而不是用格雷码直接比较),剩下的空满判断就和同步fifo的判断一样了。

2.3 代码实现

关键代码如下:

//读写地址计数器
    always@(posedge wclk,negedge wrstn)begin
        if(!wrstn)
            waddr <= 0;
        else if(winc&&!(wfull))begin
            if(waddr[addr_width-1:0] == DEPTH-1)
                waddr <= {~waddr[addr_width],{(addr_width){1'b0}}};
            else
                waddr <= waddr+1;
        end
    end
    wire [addr_width:0]waddr_map;
    assign waddr_map = waddr[addr_width]?waddr:(waddr+(DEPTH_CEIL-DEPTH));//映射
    always@(posedge rclk,negedge rrstn)begin
        if(!rrstn)
            raddr <= 0;
        else if(rinc&&!rempty)begin
            if(raddr[addr_width-1:0] == DEPTH-1)
                raddr <= {~raddr[addr_width],{(addr_width){1'b0}}};
            else
                raddr <= raddr+1;
        end
    end
    wire [addr_width:0]raddr_map;
    assign raddr_map = raddr[addr_width]?raddr:(raddr+(DEPTH_CEIL-DEPTH));//映射

此处详细可以参考博客:https://blog.csdn.net/qq_45966855/article/details/130589071

3 溢出警告

警告分为overflow和underflow,前者是由于在fifo满后继续写fifo造成的警告,后者是在fifo为空后继续读造成的。如果出现了警告,则证明上一次的操作是失败的,用户需要根据警告信号采取相应的措施。
代码部分也比较简单
在这里插入图片描述

4 first word full through

First Word Full Through简称FWFT,在fifo读模式中经常用到。由于正常的标准fifo都会有一个到两个节拍的延迟,但是如果是FWFT模式,数据是提前出来的,这样在一些特殊场合中正好需要。
下面结合仿真波形讲解一下
FWFT读模式
在这里插入图片描述
例如fifo中存入的数据分别是34562345,45675678,6789789a …
当配置为FWFT模式时,ren拉高后数据34562345立即有效(已提前准备好),第二个cycle是45675678,后面依次给出其它的数据,不存在延迟。

标准读模式
在这里插入图片描述
例如fifo中存入的数据分别是34562345,45675678,6789789a …
当配置为标准读模式时,ren拉高后数据34562345在一个cycle后才有效,第二个cycle之后是45675678,后面依次给出其它的数据,存在一个时钟的延迟。

FWFT模式实现的关键点
要实现FWFT功能,
首先要在fifo的empty下降沿前,给出fifo内已经写入的第一个数据,这样就能保证第一个数据提前准备好。
其次是要在读使能ren拉高的时候ram的读地址mem_raddr要超前读指针一个数,这样可以保证在读完一个数后下一个数已经在要读之前准备好了。
最后需要注意的是rd_ack信号的产生也是有区别的,rd_ack在FWFT也要跟数据一样提前指示总线上的数据是否可用。
代码:

always @ (posedge rclk )rempty <= rempty_val;
generate
if(fwft_en == 1)begin
	assign mem_addr = rbin[rd_aw-1:0] + !(rempty);
	assign mem_cer = ren & ~rempty | rempty ;
	always @ (posedge rclk or negedge rrst_n)
		if(~rrst_n)
			rd_ack <= 0;
		else if(rd_clr)
			rd_ack <= 0;
		else 
			rd_ack <= ~rempty_val;
end else begin
	assign mem_addr = rbin[rd_aw-1:0] ;
	assign mem_cer = ren & ~rempty ;
	always @ (posedge rclk or negedge rrst_n)
		if(~rrst_n)
			rd_ack <= 0;
		else if(rd_clr)
			rd_ack <= 0;
		else 
			rd_ack <= ren& ~rempty;
end
endgenerate

5 总结

fifo是一个非常基础的IP,就是因为是基础,常用,所以可玩性非常多。在下学识浅薄,有未能详尽之处还请海涵。
如有不足之处或者新的看法,欢迎在评论区给与点评与指导,一定认真听取和采纳。

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
FPGA(现场可编程门阵列)是一种可编程逻辑器件,它可以根据需要进行重新配置,以实现各种不同的功能。串口是一种用于数据传输的通信接口,常用于连接电脑和外部设备。而FIFO(First In First Out,先进先出)是一种存储数据的缓冲区,数据以先进先出的方式进行读写。 FPGA串口转发进FIFO是指将串口接收到的数据通过FPGA进行转发,并将其存储到FIFO缓冲区中。这个过程可以分为以下几个步骤: 首先,需要确定使用的FPGA开发板和串口通信的协议。通常情况下,我们可以选择UART(通用异步收发器)协议来进行串口通信。 然后,在FPGA设计一个接收串口数据的模块。这个模块的功能是接收串口信号,并将其转换为数字信号,以便FPGA进行处理。 接着,设计一个FIFO模块。这个模块的作用是接收来自串口数据接收模块的数据,并以先进先出的方式存储到FIFO缓冲区中。当有新数据到达时,FIFO模块会将其写入到FIFO中。 最后,设计一个发送模块,将FIFO缓冲区中的数据取出,并通过串口发送出去。这个模块的功能是读取FIFO缓冲区中的数据,并将其转换为串口信号,以便发送出去。 通过以上设计,就可以实现FPGA串口转发进FIFO的功能。当有数据通过串口发送过来时,FPGA会将其接收并存储到FIFO中,然后从FIFO中读取数据,并通过串口发送出去。这样就实现了串口数据的转发功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小精灵_2013

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值