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为例),取
二进制 | 格雷码 |
---|---|
000 | 000 |
001 | 001 |
010 | 011 |
011 | 010 |
对称线 | |
100 | 110 |
101 | 111 |
110 | 101 |
111 | 100 |
仔细观察表格中,格雷码以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.youkuaiyun.com/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,就是因为是基础,常用,所以可玩性非常多。在下学识浅薄,有未能详尽之处还请海涵。
如有不足之处或者新的看法,欢迎在评论区给与点评与指导,一定认真听取和采纳。