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)就是干这个的。

实现方式有两种:

  1. 基于深度差:计算写指针和读指针的差值,当差值小于某个阈值时置几乎空,大于某个阈值时置几乎满。
  2. 基于指针位置:直接比较指针是否到达某个预设位置。

我个人更推荐第一种,因为灵活。阈值可以配置,适应不同场景。

代码示例(同步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 读写时钟相同 二进制指针直接比较 空:指针相等 满:高1位相反 异步FIFO 读写时钟不同 格雷码跨时钟域同步 空:格雷码相等 满:高2位相反 几乎空 / 几乎满 基于深度差比较 阈值需大于延迟

总结一下:同步FIFO用二进制指针加额外位区分空满;异步FIFO必须用格雷码跨时钟域同步,满判断看高2位;几乎空/几乎满用深度差实现,阈值要留够余量。

这些逻辑,我建议你写完后用形式验证工具跑一遍,或者至少写个定向测试用例,把边界条件都覆盖到。空满标志一旦出错,整个数据通路就废了。

核心要点回顾:

  • 同步FIFO:二进制指针 + 额外位区分空满
  • 异步FIFO:格雷码跨时钟域 + 高2位判断满
  • 几乎空/几乎满:深度差比较,阈值 > 路径延迟
  • 所有空满标志建议打一拍输出,避免组合逻辑毛刺

可可精品资料库