4、空满标志生成逻辑:同步FIFO的空满判断、异步FIFO的空满判断(格雷码比较)、几乎空/几乎满标志
空满标志,是FIFO的灵魂。
你想想看,如果读空的时候你还去读,读到的就是垃圾数据;写满的时候你还去写,要么覆盖旧数据,要么直接丢包。我在项目中遇到过不止一次,芯片功能仿真全过,一上板子就挂,最后定位到——空满标志慢了半拍,导致数据被覆盖。嗯,这坑我踩过,今天咱们就把它填平。
4.1 同步FIFO的空满判断
同步FIFO,读写时钟是同一个。说白了,就是同一个时钟域下的指针比较。
判断空满,核心就一句话:比较读指针和写指针是否相等。
- 空标志:读指针追上写指针,两者完全相等。此时FIFO里没数据了,读操作必须暂停。
- 满标志:写指针追上读指针,两者也相等。但注意——这里有个陷阱:空和满都是指针相等,怎么区分?
区分空满的经典做法:给指针加一位额外位(wrap-around bit)。
比如深度为16的FIFO,地址位用4位,指针用5位。最高位用来标记是否绕了一圈。
- 空:指针完全相等(包括最高位)
- 满:指针低4位相等,但最高位相反
我个人习惯用这种方式,简单可靠,逻辑综合出来也不大。代码写起来也很直观:
// 同步FIFO空满判断
assign empty = (w_ptr == r_ptr);
assign full = (w_ptr[WIDTH] != r_ptr[WIDTH]) &&
(w_ptr[WIDTH-1:0] == r_ptr[WIDTH-1:0]);
你看,就这么几行。但要注意,这里的比较是组合逻辑,如果FIFO深度很大,比较器会有点延迟。我建议在高速设计中,给空满信号加一级寄存器打一拍,避免毛刺。
4.2 异步FIFO的空满判断(格雷码比较)
异步FIFO就麻烦多了。读写时钟不同,指针跨时钟域传递,直接比较二进制指针?那会出大问题。
为什么?因为二进制指针多位同时变化,跨时钟域采样时可能采到中间态。比如指针从0111变到1000,4位同时翻转,采样结果可能是0000、1001、0110……什么妖魔鬼怪都有。
核心原则:跨时钟域传递指针,必须用格雷码。
格雷码每次只变化1位,跨时钟域采样时最多只有1位不确定,不会出现乱码。
格雷码比较空满,和二进制思路类似,但有个细节:
- 空判断:格雷码指针完全相等。这个简单,和同步FIFO一样。
- 满判断:格雷码指针的高2位相反,其余位相等。
为什么会这样?我解释一下。格雷码的满条件,本质上是写指针比读指针多绕了一圈。在格雷码空间里,绕一圈后最高位和次高位都会翻转。所以判断满的时候,要看高2位。
代码示例:
// 异步FIFO空满判断(格雷码)
assign empty = (w_ptr_gray == r_ptr_gray_sync);
assign full = (w_ptr_gray[WIDTH] != r_ptr_gray_sync[WIDTH]) &&
(w_ptr_gray[WIDTH-1] != r_ptr_gray_sync[WIDTH-1]) &&
(w_ptr_gray[WIDTH-2:0] == r_ptr_gray_sync[WIDTH-2:0]);
我曾经在一个项目中,异步FIFO的深度是8,地址位3位,指针用4位。满判断写成了只比较最高位,结果仿真没问题,上板子偶尔丢数据。查了三天,才发现是满条件写错了。嗯,从那以后,我每次写异步FIFO都会画个格雷码表,手动验证一遍。
4.3 几乎空/几乎满标志
实际项目中,光有满和空还不够。比如你要做流水线处理,数据快满了得提前通知上游停一停,否则等真正满了再停,中间还有几拍数据会丢。
几乎空(Almost Empty)和几乎满(Almost Full)就是干这个的。
实现方式有两种:
- 基于深度差:计算写指针和读指针的差值,当差值小于某个阈值时置几乎空,大于某个阈值时置几乎满。
- 基于指针位置:直接比较指针是否到达某个预设位置。
我个人更推荐第一种,因为灵活。阈值可以配置,适应不同场景。
代码示例(同步FIFO):
// 计算FIFO中的数据量
assign fifo_count = (w_ptr - r_ptr) & ((1 << (WIDTH+1)) - 1);
// 几乎空:数据量小于阈值
assign almost_empty = (fifo_count <= ALMOST_EMPTY_THRESHOLD);
// 几乎满:数据量大于等于(深度 - 阈值)
assign almost_full = (fifo_count >= (FIFO_DEPTH - ALMOST_FULL_THRESHOLD));
避坑指南:
我曾经在异步FIFO中直接用二进制差值做几乎空/几乎满,结果跨时钟域同步时出了大问题。正确的做法是:把读写指针同步到同一时钟域后,再计算差值。或者,用格雷码指针做差值计算,但要注意格雷码的加减法比较麻烦,通常的做法是转回二进制再算。
另外,阈值的选择也有讲究。太小了起不到预警作用,太大了又容易误触发。我一般建议:
- 几乎空阈值:2~4拍,取决于读延迟
- 几乎满阈值:FIFO深度减去2~4拍,取决于写延迟
你想想看,如果读延迟是3拍,几乎空阈值设成1,那等几乎空信号出来,数据已经读空了,根本来不及反应。所以阈值一定要大于等于读写路径的延迟。
4.4 知识体系图
下面这张图,把空满标志的生成逻辑串起来了。我建议你保存下来,写代码前先看一眼。
总结一下:同步FIFO用二进制指针加额外位区分空满;异步FIFO必须用格雷码跨时钟域同步,满判断看高2位;几乎空/几乎满用深度差实现,阈值要留够余量。
这些逻辑,我建议你写完后用形式验证工具跑一遍,或者至少写个定向测试用例,把边界条件都覆盖到。空满标志一旦出错,整个数据通路就废了。
核心要点回顾:
- 同步FIFO:二进制指针 + 额外位区分空满
- 异步FIFO:格雷码跨时钟域 + 高2位判断满
- 几乎空/几乎满:深度差比较,阈值 > 路径延迟
- 所有空满标志建议打一拍输出,避免组合逻辑毛刺
可可精品资料库