一直想找机会聊一聊预读FIFO这个问题,趁着这次机会比较系统的整体讲一讲预读FIFO的一些实现方法,注意事项,以及优化手段。
1、背景知识
FIFO 是一个具有简单概念的存储元件:应用逻辑的一部分在 FIFO 的一侧写入数据。在 FIFO 的另一侧,以相同的顺序(FIFO = First In First Out)从中读取这些数据。不细看逻辑,一个 FIFO 的整体框架如图1所示。通常,数据是存放在MEM中,然后在外围增加逻辑电路,对MEM的指定地址进行读写操作。MEM对于FPGA而言,可选BRAM,DRAM,REG,URAM等,ASIC通常是BRAM或REG,这里的BRAM也即厂商定制的大块RAM。ASIC可以定制任意位宽深度的RAM,FPGA都是统一大小,根据厂商/器件不同,大小可以是9K/10K/18K/20K/36K/288K等等,通过拼接来实现各种位宽组合的RAM。
【配图留空】图1 标准FIFO架构
一个标准的FIFO(以同步FIFO为例),通常是读使能的下一拍可以获取到FIFO内的读数据,其读时序如下:
【配图留空】图2 标准FIFO读时序
上面的这种时序很好理解,当FIFO非空时,只要外部有读使能fifo_ren,就把内部的数据送到读口上。伪代码如下:
// —— 写操作 ——
if (w_en == 1 && count < DEPTH) begin
mem[wptr] <= wdata;
wptr <= wptr + 1;
end
// —— 读操作 ——
if (fifo_ren == 1 && count > 0) begin
dout <= mem[rptr]; // 数据下一拍才出现在读口上
rptr <= rptr + 1;
end
按照正常实现,FIFO似乎总是至少有一拍延迟来自MEM本身的读延迟,除非提前知道外部要读了,内部先把这个数读出来放到读端口,可是内部怎么可能知道接口下一拍要读呢?接下来看预读FIFO的实现。
2、预读FIFO
预读FIFO的时序参照下图,也就是说读使能拉高的当拍就能把数据取走,你还没开口,店员已经把奶茶提前放在取餐台上了。
【配图留空】图3 预读FIFO读时序
2.1 为什么需要预读FIFO?
1)减少读侧延迟 — 数据已就绪,rd_en 更像一个“确认/消耗”信号,适合对延迟敏感的数据通路。
2)与下游模块配合 — 如果下游模块期望“数据有效+使能”的同步握手(如 AXI Stream valid & ready),预读FIFO天然适配:FIFO的 ~empty 作为 valid,下游的 rd_en 作为 ready。
2.2 预读FIFO的实现方案
实现预读FIFO一般有以下两种方法。
方案一:输出寄存器(cache)预取(Prefetch)
标准 FIFO 的 dout 直接连 RAM 输出,所以 r_en 有效后必须等 RAM 读出一拍。FWFT 的做法是在 RAM 和 dout 之间插一个寄存器,让数据提前住进去。
方案二:读地址提前一拍生效
这种方案不额外加预取寄存器,而是让 RAM 的读地址永远比读指针快一步,这样 RAM 输出端口上始终趴着下一个该读的数据。
两种方式都比较常见,但是在使用上有诸多不同,应用限制和注意事项我们在讲方案的时候再比对。
【配图留空】图4 方案二架构:双读指针+读地址2选1
在写侧,和我们常规的FIFO没有什么区别,外部写数据/使能来了,更新指针,同时数据写入到对应指针地址。
在读侧,我们保存两个指针,rptr0和rptr1,MEM的读地址2选1。
rptr0 <= fifo_ren ? rptr0 + 1 : rptr0; // 读指针:当前趴在读口上的数据
rptr1 <= fifo_ren ? rptr1 + 1 : rptr0 + 1; // 始终领先 rptr0 一步
mem_rd_addr = fifo_ren ? rptr1 : rptr0; // MEM 读地址(2选1,组合逻辑)
也就是说,空闲时 rptr1 保持在 rptr0+1,MEM 输出口上始终趴着下一个待读的数据;fifo_ren 拉高的当拍,读地址切换到 rptr1,当前数据从读口取走的同时,MEM 已经在预读下一个数据,下一时刻下个数据就送到了读口上。
另外注意 nempty 的存量管理:这种架构下永远有 1 个数据被预读出 MEM、趴在读口上,nempty 必须按指针差(wptr − rptr0)计算,把这个已经在读口上的数据也算进存量;不能按「MEM 中尚未读出的数据」来算,否则 nempty 会提前一个数据跌落,下游会少读一个数据。
2.3 方案二的局限性及问题
1)最多接受MEM有1拍延迟
大部分情况FPGA是可以接受的,BRAM分布在芯片的各个地方,一般不会变成时序阻塞点(不绝对)。如果需要优化RAM时序,两拍出甚至三拍(例如使用URAM)那么就做不到了。
2)功耗高
所有的读写操作都直接影响MEM,而MEM的读写率和功耗强相关。同时MEM需要一直保持读操作来实现预读——为什么?仔细看架构图,用户读侧给MEM的读使能使用虚线表示,也就是说其实MEM的读使能和外部读使能没有直接连线,之前说过,MEM的读永远比fifo_ren要早才行,不然没办法提前送数据出去。实际上MEM的读有两个可选项,一个接常1,一个接FIFO内部的非空信号nempty。这就是说MEM会长期保持读操作!!
顺便讲一下接常1和非空信号nempty的时序区别,接1说明MEM一直在读,那么0地址的数据在写入之后就已经开始送到读口了,但是如果接非空信号nempty,读数据至少比nempty晚1拍才送到读口,因此会有应用限制:
- CTRL_REN = 1(连nempty时)读使能至少比nempty晚一拍!
- CTRL_REN = 0(常1),可以非空即读(fifo读使能直接连nempty)。
3)存在读写冲突的问题
上述设计在两个场景存在MEM的读写冲突问题。
读使能常1时,FIFO写入第一个数据,在地址0处产生读写冲突。
FIFO内部存在1个数据时,同时写和读,在地址1处产生读写冲突。
具体时序我就不讲了,根据上面的原理算一下读写两侧的MEM addr就行。
这种读写冲突在FPGA上其实没什么关系,几乎所有厂商对读写冲突的处理都是保证数据正确写入,冲突拍的读出数据不保证,和写模式也有关。实际上,这种架构的FIFO不受到读数据错误的影响,因为实际上冲突那拍数据用户是不用的,自行画时序图,所以对FPGA而言不感知这种冲突。但是ASIC不一样,厂家提供的RAM IP行为不统一,可以高度定制(做这种hazard处理终归是要额外处理逻辑的),很可能会出现冲突的时候写入失败,FIFO失效。
【配图留空】图5 方案一架构:同步FIFO与异步FIFO中的cache与MEM
可以发现,这种方案同步FIFO和异步FIFO是有区别的,当然你也可以用异步FIFO的方式实现同步FIFO,但是会有额外的功耗开销。为什么两种架构不同?关键还是时钟不同步,小cache无法同时工作在两个时钟域,因此只能所有的读都通过cache读出,写入是直接写入到MEM。但是同步模式就不一样了,写入的时候就可以先写入到cache中,只要cache深度足够,完全可以不操作MEM。(FIFO的总深度是MEM深度+cache深度。)
2.4 方案一(Cache预取)详细工作流程
同步FIFO
工作流程可以总结为:
1、写数据时:优先写入cache,cache满了之后再写入MEM。
2、读数据时:从cache里面读,同时把MEM的数据补充到cache中,保证cache一直是最新数据。
cache的深度一般可以设置为MEM_Latency+1,这样可以保证寄存输出。极限场景也可以设置为MEM_Latency,这样在连续读的场景可能出现MEM数据直通到读端口(本质就是cache太少,来不及先写入cache再读出来)。
我们关注以下几个问题:
1、数据什么时候写入cache?
1)外部fifo写使能拉高,且cache未满的状态,数据写入cache。
2)cache满,外部fifo写使能和读使能同时拉高,直接替换掉读出的那个数据。
3)MEM非空,外部有fifo读请求,一边读MEM数据,一边往外送cache数据,再把MEM读出的数据写入到cache。
2、何时更新cache的写地址?
cache写使能拉高,对应1中的1)2)两种场景。
MEM读请求为高,对应1中的3)场景。
3、cache的读处理
很简单,直接连到外部fifo读使能,cache的读地址。
4、数据什么时候写入MEM?
除了1中1)2)两种场景,外部只要写入数据,就写入MEM。
5、MEM什么时候读?
只要MEM非空,外部读了,就需要读MEM更新cache数据。这里需要注意的是,MEM读出来的数据是有延迟的,所以需要对cache的写使能/地址打拍处理,和MEM的数据对齐再写入cache。
异步FIFO
思路和同步FIFO基本一致。
- 什么时候写MEM -> 只要外部有写使能,就写入MEM。
- 什么时候读MEM -> 只要外部有读使能且MEM非空,就读MEM。
- 什么时候写cache -> 只要读了MEM。
- 什么时候读cache -> 只要外部有读使能。
上面的这些描述没有考虑读写保护等场景,简化了一下。
2.5 方案适用性总结
整体来说,方案二的适用场景还是广一些。方案一(cache预取)的主要问题在于:
1)额外面积:尤其是数据位宽比较大的时候,cache阵列的面积消耗会比MEM大得多。
2)想不出来了。
3、FIFO的拼接
一般FPGA的硬核IP资源实现,组件不涉及,直接拼RAM就行了。
【配图留空】图6 FIFO拼接:位宽扩展与深度扩展
4、零延迟FIFO 和标准FIFO的互相转换
零延迟FIFO转换成标准FIFO:很简单,把数据打拍出就可以了。
将已有的标准FIFO,转换成零延迟FIFO:可以参考以下代码。
(代码示例展示了一个 module basic_fwft_fifo)
只是增加逻辑替换了标准FIFO的 @rd_en 和 @empty两个信号,即包装成了零延迟FIFO。
5、如何改进时序?
HS CBB中实现FIFO逻辑的代码其实很少,但是一个文件动辄上千行,大部分场景都是为了优化时序或者适配异步复位/同步复位。
同步FIFO
MEM的输入输出都可以增加打拍,MEM整个读延迟会增加,影响cache深度。
cache_pre_wr_ptr_vld 用来控制写地址的翻转。
cache 读端地址使用onehot优化时序,数据切片,减小扇出。
原本的做法是:
cache_rdata = cache_mem[cache_rd_ptr]
优化: cache_rd_ptr 变成 onehot 格式 cache_rd_ptr_onehot,那么每个DEPTH的cache都有自己的1bit onehot读。另外,再把这个 cache_rd_ptr_onehot 复制多份,每一份控制cache里面的一部分数据。具体举例:深度为2,位宽为256的 cache,cache0[127:0] 和 cache0[255:128] 分别由 cache_rd_ptr_onehot[0] 和它的复制份 cache_rd_ptr_onehot_cp[0] 控制,各自完成选择后再拼接成一个完整的读数据。
异步FIFO
增加了格雷码/二进制转换的打拍。
No.71 :预读FIFO(零延迟FIFO或 First Word Fall Through (FWFT) FIFO)实现
https://blog.songshiyu.cn/archives/yu-du-fifo-ling-yan-chi-fifohuo-first-word-fall-through-fwft-fifo-shi-xian
评论