2. FIFO的读写指针机制:读指针、写指针的工作原理、空满标志的产生逻辑
好,咱们今天聊聊FIFO最核心的东西——读写指针。说实话,我见过不少工程师,代码写得挺溜,但一问到指针怎么转、空满怎么判,就开始含糊了。这不行啊,指针是FIFO的灵魂,搞不懂它,时序告警排查根本无从下手。
2.1 读写指针的工作原理
先说说指针是啥。说白了,就是两个计数器。
- 写指针(wptr):指向下一个要写入的地址。每写一个数据,它就加1。
- 读指针(rptr):指向下一个要读出的地址。每读一个数据,它也加1。
嗯,这里有个关键点:指针是循环的。FIFO深度是N,那指针就从0跑到N-1,然后回到0。我刚开始做设计时,总觉得这跟普通计数器没啥区别,直到有一次调试一个深度为8的FIFO,发现数据读出来全是乱的……后来才意识到,指针的循环特性决定了空满判断不能简单比大小。
核心要点:读写指针本质上是一个模N的格雷码计数器。为什么用格雷码?因为跨时钟域传输时,格雷码每次只变1位,能大大降低亚稳态风险。这个后面章节会细讲。
你想想看,如果写指针追上了读指针,说明FIFO满了;如果读指针追上了写指针,说明FIFO空了。但问题来了——两个指针都是循环的,你怎么知道谁追上了谁?
2.2 空满标志的产生逻辑
这才是真正的技术活。空满判断,不能简单用「相等」来判断。为什么?因为写指针和读指针相等,既可能是空,也可能是满——这取决于它们是在同一圈还是差了一圈。
我习惯的做法是:给指针加一个额外的位(MSB),用来区分圈数。
// 假设FIFO深度为8,指针位宽为4位(3位地址 + 1位圈数标志)
// 写指针:wptr[3:0]
// 读指针:rptr[3:0]
// 空标志:指针完全相等(包括圈数位)
assign empty = (wptr == rptr);
// 满标志:圈数位不同,但地址位相同
assign full = (wptr[3] != rptr[3]) &&
(wptr[2:0] == rptr[2:0]);
个人经验:我曾经在一个项目中,直接用「wptr - rptr == 深度」来判断满标志。结果呢?跨时钟域采样时,指针值可能跳变,减法结果完全不可控。后来老老实实改用格雷码+额外位的方式,再也没出过问题。
这里有个细节要注意:空满标志的生成,必须用同步后的指针。也就是说,写时钟域要判断空标志,得先把读指针同步过来;读时钟域要判断满标志,得先把写指针同步过来。同步会引入延迟,所以标志会有一定的「惯性」——空了之后不会立刻变满,满了之后不会立刻变空。这其实是好事,能防止边界情况下的误判。
2.3 指针机制的完整流程图
下面这张图,是我自己画的一个FIFO指针流转图。你看一眼就明白了。
避坑指南:我曾经在一个异步FIFO项目中,满标志判断用了组合逻辑直接比较两个时钟域的指针。结果呢?综合时报了一堆跨时钟域路径,时序完全跑不过。记住:空满标志的生成,必须用同步后的指针,而且同步器至少打两拍。
2.4 实际项目中的注意事项
嗯,这里再补充几个我踩过的坑:
- 指针位宽要留余量:深度为N的FIFO,地址位需要log2(N)位,但指针总位宽要加1位圈数标志。比如深度16,地址位4位,指针位宽5位。
- 格雷码转换不能省:跨时钟域传输前,一定要把二进制指针转成格雷码。我见过有人图省事直接传二进制,结果亚稳态导致空满标志乱跳。
- 空满标志的延迟:由于同步器的存在,空标志和满标志会有几个时钟周期的延迟。设计时要把这个延迟考虑进去,尤其是高速场景。
| 场景 | 指针状态 | 空满标志 |
|---|---|---|
| FIFO为空 | wptr == rptr(圈数位相同) | empty = 1, full = 0 |
| FIFO为满 | wptr[MSB] != rptr[MSB],地址位相等 | empty = 0, full = 1 |
| 正常读写 | wptr领先rptr,但未满一圈 | empty = 0, full = 0 |
| 写满后读空 | wptr追回rptr(圈数位相同) | empty = 1, full = 0 |
说白了,FIFO的指针机制就是一场「追逐赛」。写指针在前面跑,读指针在后面追。追上了就是空,差一圈就是满。搞懂这个,FIFO时序告警排查就成功了一半。
我的习惯:每次做FIFO设计,我都会先画一张类似上面的流程图,把指针路径、同步路径、标志生成逻辑标清楚。画完图再写代码,思路清晰得多,也少走很多弯路。
好了,这一节就到这里。记住:指针是FIFO的命根子,空满标志是它的眼睛。眼睛花了,命根子就乱了。下一节咱们聊聊格雷码和跨时钟域同步,那才是真正的技术活。