2. 读写指针的本质:指针的二进制计数原理,指针回绕(Wrap Around)机制,格雷码与二进制码的对比
2.1 指针到底是什么?
先问大家一个问题:FIFO 的读写指针,本质上是什么?
说白了,就是一个计数器。写指针每写入一个数据就加 1,读指针每读出一个数据也加 1。但这里有个关键点——这个计数器不是无限大的,它受限于 FIFO 的深度。
我刚开始做 FIFO 设计时,总觉得指针就是个地址。后来踩了坑才明白:指针的本质是“模 N 计数器”,N 就是 FIFO 的深度。它记录的是“已经写了/读了多少个数据”,而不是“数据存在哪个地址”。
核心理解: 写指针 = 写入次数 % 深度,读指针 = 读出次数 % 深度。
2.2 二进制计数原理
假设 FIFO 深度为 8,那么指针就是一个 3 位的二进制计数器。从 000 开始,每操作一次加 1:
000 → 001 → 010 → 011 → 100 → 101 → 110 → 111 → 000 → ...
嗯,这里要注意:当计数器从 111 回到 000 时,就发生了回绕(Wrap Around)。
为什么用二进制?因为二进制加法器是数字电路里最成熟的模块,面积小、速度快。我在一个高速 FIFO 项目中试过用格雷码做加法,结果发现加法器比二进制复杂得多,功耗反而上去了。
2.3 回绕机制——FIFO 的灵魂
回绕机制解决了一个核心问题:如何判断 FIFO 是满还是空?
你想想看,如果指针从 000 加到 111 再回到 000,那 000 这个状态到底代表“空”还是“满”?
答案是:加一个额外的标志位。
我习惯的做法是:指针的位宽比深度多 1 位。比如深度 8 的 FIFO,指针用 4 位。低 3 位是真正的地址,最高位用来标记回绕次数。
| 指针值(4位) | 低3位地址 | 回绕次数 | 含义 |
|---|---|---|---|
| 0000 | 000 | 0 | 空(初始状态) |
| 1000 | 000 | 1 | 满(回绕一次后回到地址0) |
| 0101 | 101 | 0 | 正常写入5个数据 |
| 1101 | 101 | 1 | 回绕一次后写入5个数据 |
判断空的条件:读写指针完全相等(包括最高位)。
判断满的条件:读写指针的低 3 位相等,但最高位相反。
我的经验: 回绕标志位一定要用格雷码同步到另一个时钟域。我曾经因为直接用二进制同步,导致满标志判断出错,数据被覆盖了。那一次 debug 花了我整整两天。
2.4 格雷码 vs 二进制码——为什么需要格雷码?
二进制计数器有个致命问题:多位同时翻转。
比如从 011(3)到 100(4),3 个 bit 同时变化。在跨时钟域同步时,采样到的值可能是 000、001、010、100……任何一个中间态。这会导致空/满判断瞬间出错。
格雷码就解决了这个问题:每次只变化 1 个 bit。
| 十进制 | 二进制 | 格雷码 |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
| 5 | 101 | 111 |
| 6 | 110 | 101 |
| 7 | 111 | 100 |
你看,格雷码相邻两个值之间只有 1 位不同。同步时最多错 1 个 bit,不会出现完全错误的值。
注意: 格雷码虽然同步安全,但做加法比二进制复杂。我一般只在跨时钟域同步时用格雷码,FIFO 内部计数还是用二进制。说白了就是:内部用二进制算,同步用格雷码传。
2.5 二进制 ↔ 格雷码转换
转换公式其实很简单:
// 二进制转格雷码
gray = binary ^ (binary >> 1);
// 格雷码转二进制(Verilog 实现)
assign binary[WIDTH-1] = gray[WIDTH-1];
generate
for(i = WIDTH-2; i >= 0; i = i-1) begin
assign binary[i] = gray[i] ^ binary[i+1];
end
endgenerate
我个人习惯把转换逻辑封装成两个独立的模块:bin2gray 和 gray2bin。这样代码复用性高,也不容易出错。
2.6 本章知识体系
下面这张图总结了指针设计的核心逻辑:
2.7 避坑指南
- 不要直接用二进制指针做跨时钟域同步。 我曾经在一个项目中偷懒,结果空标志偶尔会误判为满,导致读操作被阻塞。查了三天才发现是同步时采样到了错误值。
- 格雷码转二进制时注意位宽匹配。 我见过有人把 4 位格雷码转成 3 位二进制,结果地址直接少了一半。
- 回绕标志位一定要参与空满判断。 如果不加最高位,深度 8 的 FIFO 在写入 8 个数据后,指针回到 000,你会误以为 FIFO 是空的——然后继续写入,数据就覆盖了。
嗯,这一章的内容就到这里。指针设计是 FIFO 的基础,理解了二进制计数和回绕机制,后面讲深度扩展、异步 FIFO 同步策略时就会轻松很多。