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里塞数据,读侧只管往外拿数据。满标志和空标志是它们之间的「交通信号灯」。写侧看到红灯(满)就停,读侧看到红灯(空)也停。就这么简单。
好了,八个核心接口就讲到这里。记住:写侧四个信号,读侧四个信号,满和空是反馈。搞清楚了这些,FIFO的基本框架你就掌握了。下一节咱们会深入讲指针和地址生成,那才是FIFO设计的精髓所在。
可可精品资料库