赞
踩
对于fifo的设计,相关的资料有很多,但是针对一些特殊的应用,网上就比较少了。所以
本文给出的fifo具有通用性,适用于各行各业,各种场景。其关键点在于如何实现输入输出位宽不一致,如何处理深度不是2的幂的情况,实现溢出警告处理,数据指针功能,first word full through功能。
本文适合对fifo有一定熟悉程度的工程师阅读,进阶学习。
fifo最基本的模块有:
这里不对这些基本的模块做详细论述了,我们将主要讲一些其他特殊的功能,以及实现方法。
fifo不仅可以完成数据的缓冲,时钟域转换,还可以很轻松地完成数据位宽的转换。
其主要部分是一个输入输出位宽可变的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;
对于这种情况下,定义两个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
对于这种情况下,定义一个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
由于位宽不相等,在读写指针之间相互运算的时候,操作方法也是有区别的。例如写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;
读写计数主要是为了给用户一个具体的数据,方便用户判断已经有多少个数据写入了,或者可读出多少个数据,具体会用在一些固定长度的数据包读写的实例中,但是需要特别注意的是这个读写的计数在同时读写时,有延迟,主要是地址指针异步同步所带来的延迟。
在计算读写计数值时候也需要考虑因位宽不同而导致的误差。
计算写计数时候如果写位宽小于读位宽,则将读位宽乘以比率。
计算读计数时候如果读位宽小于写位宽,则将写位宽乘以比率。
代码:
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];
很多时候实际项目中的fifo深度并不是2的幂,如果硬是用2的幂的fifo,那么将会浪费很多硬件资源,为了节省bram或者dram,本章给出一种方法实现任意深度的异步fifo。
格雷码是具有对称性的,比如深度为三(下文均以3为例),取
二进制 | 格雷码 |
---|---|
000 | 000 |
001 | 001 |
010 | 011 |
011 | 010 |
对称线 | |
100 | 110 |
101 | 111 |
110 | 101 |
111 | 100 |
仔细观察表格中,格雷码以3和4中间为分界线,最高位相反,上下对称,任意对称的两个数只有一个bit不同。
读写地址是连续的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的判断一样了。
关键代码如下:
//读写地址计数器 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
警告分为overflow和underflow,前者是由于在fifo满后继续写fifo造成的警告,后者是在fifo为空后继续读造成的。如果出现了警告,则证明上一次的操作是失败的,用户需要根据警告信号采取相应的措施。
代码部分也比较简单
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
fifo是一个非常基础的IP,就是因为是基础,常用,所以可玩性非常多。在下学识浅薄,有未能详尽之处还请海涵。
如有不足之处或者新的看法,欢迎在评论区给与点评与指导,一定认真听取和采纳。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。