2. FIFO核心接口:写时钟、读时钟、写使能、读使能、写数据、读数据、满标志、空标志

好,咱们直接切入正题。FIFO这东西,说白了就是个数据缓冲池。你往里面写,我从里面读。但要让这个池子不出乱子,你得先搞清楚它的八个核心接口信号。

我刚开始学FIFO的时候,总觉得接口多,记不住。后来发现,其实就两拨人:写侧和读侧。每侧四个信号,一共八个。咱们一个一个捋。

2.1 写侧接口:谁在往里塞数据?

写侧负责把数据存进FIFO。它由四个信号组成:

信号名 方向 说明
wr_clk 输入 写时钟,所有写操作都跟着它走
wr_en 输入 写使能,高电平有效,表示你要写数据了
wr_data 输入 写数据总线,宽度由设计决定
full 输出 满标志,告诉写侧:别写了,我装不下了

wr_clk 是写侧的时钟源。所有写相关的逻辑——比如写指针递增、写数据寄存——都在这个时钟的上升沿触发。我见过有人把wr_clk和rd_clk接成同一个时钟,结果异步FIFO变成了同步FIFO,功能虽然没错,但失去了跨时钟域的意义。你想想看,如果两个时钟完全同源,那还叫什么异步FIFO?

wr_en 是写使能。它必须和wr_clk同步。写数据的时候,wr_en拉高一个周期,数据就被写入当前写指针指向的地址。这里有个坑:wr_en不能在full=1的时候拉高。我曾经在一个项目中,因为没检查full标志,导致数据覆盖了还没读走的内容。嗯,那一次debug花了我整整一个下午。

wr_data 就是你要存进去的数据。宽度可以是8位、16位、32位,甚至更宽。我个人的习惯是,如果数据宽度不是2的幂次,比如24位,我会在FIFO外部做位宽转换,而不是让FIFO本身去处理非对齐的数据。

full 是满标志。它告诉写侧:FIFO已经满了,不能再写了。如果wr_en在full=1时仍然有效,数据会被丢弃,或者覆盖旧数据——这取决于你的设计策略。我建议你永远不要在full=1时写数据,除非你明确知道自己在做什么(比如做丢弃策略)。

2.2 读侧接口:谁在往外拿数据?

读侧负责从FIFO里取出数据。同样四个信号:

信号名 方向 说明
rd_clk 输入 读时钟,所有读操作跟着它走
rd_en 输入 读使能,高电平有效,表示你要读数据了
rd_data 输出 读数据总线,从FIFO里取出来的数据
empty 输出 空标志,告诉读侧:别读了,里面没东西

rd_clk 是读时钟。它和wr_clk可以是不同频率、不同相位的时钟。这就是异步FIFO的核心价值——跨时钟域传输。我记得有一次做多时钟域项目,wr_clk是100MHz,rd_clk是75MHz,两个时钟完全异步。如果不用异步FIFO,数据根本传不过去。

rd_en 是读使能。它和rd_clk同步。当rd_en拉高时,当前读指针指向的数据被送到rd_data总线上,同时读指针递增。注意:rd_en不能在empty=1时拉高。我曾经犯过一个低级错误:在empty=1时还去读数据,结果读出来的全是垃圾值。那一次仿真结果让我怀疑人生,后来才发现是读使能没和空标志做与逻辑。

rd_data 是读数据输出。它通常会在rd_en有效后的下一个时钟周期稳定输出。有些FIFO设计是组合逻辑输出,有些是寄存器输出。我个人偏好寄存器输出,因为时序更好控制,尤其是在高频设计中。

empty 是空标志。它告诉读侧:FIFO里没有有效数据了。如果rd_en在empty=1时拉高,读出的数据是无效的。嗯,这里要注意:空标志的生成有延迟。在异步FIFO中,空标志需要跨时钟域同步,所以它不会瞬间反映FIFO的真实状态。这个延迟会导致一些微妙的问题,咱们后面会详细讲。

2.3 核心逻辑:满和空是怎么判断的?

满和空是FIFO最重要的两个状态。它们的判断逻辑其实很简单:

  • 空标志:写指针 == 读指针
  • 满标志:写指针 == 读指针 + FIFO深度

但这里有个细节:指针是循环的。所以你不能直接用等于号判断,得用格雷码或者二进制比较。我习惯用格雷码做跨时钟域同步,然后用二进制做比较。为什么?因为格雷码相邻状态只变化一位,跨时钟域同步时不容易出错。

举个例子,假设FIFO深度是16:

// 伪代码示例
if (wr_ptr_gray == rd_ptr_gray_sync) 
    empty = 1;
else 
    empty = 0;

if (wr_ptr_gray == (rd_ptr_gray_sync ^ 0b1000)) 
    full = 1;
else 
    full = 0;

这里 0b1000 是深度16的一半,用来判断满状态。为什么是深度的一半?因为格雷码的循环特性,满状态时写指针比读指针多走了整个深度,但格雷码比较时需要用异或来翻转最高位。

我刚开始做FIFO时,满标志的判断总是搞错。后来画了个指针轨迹图,才彻底明白。你想想看,写指针追着读指针跑,当它追上时就是满;当读指针追上写指针时就是空。就这么简单。

2.4 避坑指南:接口设计中的常见问题

⚠️ 注意:
  • wr_en和rd_en不能同时有效:在同步FIFO中,如果写和读同时发生,你得决定是先写后读,还是先读后写。我建议统一采用「先写后读」策略,避免数据冲突。
  • 空标志的同步延迟:异步FIFO中,空标志需要从写时钟域同步到读时钟域。这个同步过程至少需要两个读时钟周期。所以读侧看到空标志时,FIFO里可能还有数据。这就是为什么读使能要谨慎。
  • 满标志的同步延迟:同理,满标志从读时钟域同步到写时钟域也有延迟。写侧看到满标志时,FIFO可能还没满。所以写使能也要留有余量。
💡 我的经验:

我一般会在FIFO深度上留10%~20%的余量。比如我需要存100个数据,我会选深度128的FIFO。这样即使标志有延迟,也不会溢出或读空。这个习惯帮我避免了好几次流片回来的尴尬。

2.5 知识体系图:八个接口的关系

下面这张图展示了八个接口信号之间的逻辑关系。你可以看到,写侧和读侧通过FIFO存储体连接,满标志和空标志分别反馈给写侧和读侧。

FIFO 存储体 写侧逻辑 wr_clk wr_en wr_data ← full (反馈) 读侧逻辑 rd_clk rd_en rd_data → ← empty (反馈) 写数据 读数据 满标志 空标志 FIFO核心接口关系图 写侧(橙色)→ FIFO存储体 → 读侧(绿色) 图例: 写数据路径 读数据路径 标志反馈路径

从这张图可以清楚看到:写侧只管往FIFO里塞数据,读侧只管往外拿数据。满标志和空标志是它们之间的「交通信号灯」。写侧看到红灯(满)就停,读侧看到红灯(空)也停。就这么简单。

好了,八个核心接口就讲到这里。记住:写侧四个信号,读侧四个信号,满和空是反馈。搞清楚了这些,FIFO的基本框架你就掌握了。下一节咱们会深入讲指针和地址生成,那才是FIFO设计的精髓所在。

可可精品资料库