4. FIFO空满标志的产生:指针比较法,格雷码跨时钟域同步,空满判断的亚稳态处理

好,咱们接着聊FIFO。上一节我们把FIFO的存储体和读写指针搭起来了,但有个关键问题还没解决——你怎么知道FIFO里有没有数据?或者数据是不是已经塞满了?

这就是空满标志要干的事。说白了,空满标志就是FIFO的“水位计”。我当年第一次做SPI控制器时,觉得这玩意儿简单,不就是比较两个指针嘛。结果呢?仿真跑得好好的,上板子就出问题。后来才发现,跨时钟域同步这关没过好。

嗯,咱们一步步来。

4.1 指针比较法的基本原理

先看最简单的场景:读写时钟是同一个。这时候空满判断非常直接。

  • 空标志:读指针追上写指针,即 rd_ptr == wr_ptr
  • 满标志:写指针追上读指针,但多绕了一圈,即 wr_ptr == rd_ptr + FIFO_DEPTH

等等,这里有个坑。你怎么区分“空”和“满”?两种情况下指针都相等啊!

解决办法其实很巧妙——把指针多扩一位。比如FIFO深度是16,地址位需要4位,那指针就用5位。最高位用来标记“绕圈”次数。

  • 写指针比读指针多绕一圈,且低4位相等 → 满
  • 写指针和读指针完全相等 → 空

看个例子:

// 假设FIFO深度为16,指针宽度5位
// 空判断
assign fifo_empty = (rd_ptr == wr_ptr);

// 满判断
assign fifo_full  = (rd_ptr[4] != wr_ptr[4]) && 
                    (rd_ptr[3:0] == wr_ptr[3:0]);

我个人习惯把这种判断写成组合逻辑,因为空满信号需要实时响应。不过要注意,组合逻辑的路径延迟要算清楚,别让关键路径卡了你的时钟频率。

4.2 格雷码跨时钟域同步

现在问题来了。SPI控制器的读时钟和写时钟往往是异步的。比如写时钟是系统总线时钟100MHz,读时钟是SPI时钟10MHz。这时候直接比较两个指针?想都别想,亚稳态会教你做人。

跨时钟域同步的标准做法是:先把指针转换成格雷码,再打两拍同步,最后转回二进制比较。

为什么要用格雷码?因为格雷码相邻两个值只有1位变化。你想想看,如果直接用二进制,多个位同时变化时,同步后可能采到乱七八糟的值。格雷码就安全多了,最多错1位,而且错的那位还不影响空满判断。

核心流程:

  1. 写时钟域:将写指针转为格雷码
  2. 打两拍同步到读时钟域
  3. 读时钟域:将同步后的格雷码转回二进制
  4. 与本地读指针比较,产生空标志

满标志同理,方向反过来。

代码实现大概是这样的:

// 二进制转格雷码
assign wr_gray = wr_ptr ^ (wr_ptr >> 1);

// 跨时钟域同步(读时钟域)
always @(posedge rd_clk or negedge rst_n) begin
    if (!rst_n) begin
        wr_gray_sync1 <= 0;
        wr_gray_sync2 <= 0;
    end else begin
        wr_gray_sync1 <= wr_gray;
        wr_gray_sync2 <= wr_gray_sync1;
    end
end

// 格雷码转二进制
assign wr_ptr_sync = gray_to_bin(wr_gray_sync2);

我曾经在一个项目中偷懒,只打了一拍。结果呢?偶尔会出现FIFO明明没满,却报了满标志,导致写操作被阻塞。查了两天才找到原因——亚稳态导致同步后的值跳变。从那以后,我再也不敢省这一拍了。

4.3 空满判断的亚稳态处理

说到亚稳态,这是跨时钟域设计里绕不开的话题。即使用了格雷码+两拍同步,也不能100%保证不出问题。但我们可以把风险降到可接受的范围。

亚稳态的本质是什么?触发器在采样时,输入信号刚好在建立保持时间窗口内变化,导致输出进入一个不确定的中间状态。这个状态可能持续一段时间,然后才稳定到0或1。

对于空满判断,亚稳态的影响分两种情况:

场景 亚稳态后果 实际影响
空标志误判为非空 读操作可能读到无效数据 数据错误,严重
满标志误判为非满 写操作可能覆盖未读数据 数据丢失,更严重
非空误判为空 读操作被阻塞,性能下降 功能正确,性能损失
非满误判为满 写操作被阻塞,性能下降 功能正确,性能损失

你看,最怕的是“假空”和“假满”。假空让你读到垃圾数据,假满让你丢掉新数据。这两种情况都是设计上不能接受的。

避坑指南:

我曾经在一个高速SPI控制器项目中,为了追求极致的性能,把空满判断的同步链缩短了。结果流片回来,芯片在高温下频繁出现数据错乱。最后定位到是亚稳态导致的空标志误判。修复方案很简单——把同步链从2级改成3级,代价是多了1个时钟周期的延迟。但可靠性提升了一个数量级。

所以我的建议是:宁可多打一拍,不要省这一拍。

4.4 一种更安全的空满判断架构

说了这么多,咱们总结一下。一个可靠的FIFO空满判断模块,应该包含以下要素:

  • 指针扩展:多1位用于区分空和满
  • 格雷码编码:降低跨时钟域同步的出错概率
  • 至少2级同步:打两拍降低亚稳态概率
  • 同步后的格雷码转二进制:用于比较
  • 组合逻辑比较:实时产生空满标志

下面这张图展示了完整的空满判断流程:

写时钟域 (wr_clk) 读时钟域 (rd_clk) 写指针 (wr_ptr) 二进制 → 格雷码 2级同步链 跨时钟域 同步后的格雷码 格雷码 → 二进制 本地读指针 (rd_ptr) 空/满判断

这张图把流程说得很清楚了。写指针在写时钟域转成格雷码,经过2级同步链进入读时钟域,再转回二进制,最后和本地读指针比较,产生空满标志。

小技巧:

如果你用的是FPGA,很多厂商的IP核已经帮你做好了这些。但作为芯片工程师,我建议你还是亲手搭一遍。只有自己踩过坑,才能真正理解为什么格雷码+两级同步是“黄金组合”。

另外,如果你追求极致性能,可以考虑用“推测空满”技术——在同步完成前,根据历史趋势预测空满状态。不过这个实现起来比较复杂,适合对延迟极度敏感的场景。

好了,关于FIFO空满标志的产生,咱们就聊到这儿。核心就三点:指针比较法判断空满、格雷码降低跨时钟域风险、多级同步抑制亚稳态。把这些搞明白了,你的FIFO设计就稳了。