FIFO 指针绕回与扩展位:指针相等时满空如何区分的状态空间图解
专题导航:异步FIFO设计全景。前置要求:已读支柱页与亚稳态与 MTBF,知道满空比较在各自时钟域本地完成、跨域只传格雷码指针。 上一篇:亚稳态与 MTBF | 下一篇:FWFT 与标准 FIFO 读时序对比。 配套实验在工程包 derekzhuo/async-fifo-mini 的
tb/tb_pointer_wraparound.v,深度 4、一条命令复现。
本文要回答的问题
- 读写指针都从 3 绕回 0 之后重新相等,为什么这一次相等不能再解释为「空」?
- 多加的那一位扩展位到底记录了什么信息,为什么加上它满和空就不再混淆?
- 标准设计里指针以格雷码跨域,扩展位规则翻译成格雷判据为什么变成「高两位相反」?
- 不想加位行不行?保留一个槽位不用、改用计数器判满空,代价是什么?
- 深度从 2 到 16,指针状态空间怎么长,为什么所有深度都有同一个歧义?
这张概念卡是全文地图:红色一组是歧义本身——FIFO 指针绕回让「相等」这个信号携带了两种互斥的含义;绿色和蓝色两组是被一位额外信息分开的两个状态。支柱页异步FIFO设计全景给出了满空比较式,本篇追问比较式里那个多出的最高位为什么必须存在。
一、问题:绕回让「相等」一词二义
先看一个深度 4 的 FIFO。寻址只需要 2 位指针:00→01→10→11,下一个写脉冲到来,指针回到 00。复位后连续写 4 个数据,写指针恰好走完一圈停在 00,而读指针一直没动,也是 00——此时 FIFO 装得满满当当,但两个指针相等。把这 4 个数据逐个读走,读指针同样走到 00,指针还是相等,这一回 FIFO 空空如也。同一个观测值 (wptr, rptr) = (00, 00),对应了占用量 4 和占用量 0 两个真值。
问题的根源在于指针做的是模 2^n 运算,它只保留「在环上的位置」,丢掉了「转了几圈」。读写双方共用同一个环,任何一方每绕回一次,位置信息就归零一次,而两者的圈数差无处存放。同步 FIFO、异步 FIFO 都逃不开这个歧义——它与时钟域无关,是环形寻址本身的性质;格雷码和两级打拍解决的是跨域传输问题(见格雷码跨时钟域原理),对这个歧义一句话都插不上。
把深度 4 的全部合法状态沿时间排成一个环,扩展位要区分的就是环上两对撞脸的状态:
读这张图抓住颜色:两个绿色节点都是空、两个红色节点都是满,但绿色的 000/000 和红色的 100/000 若只看低两位,全都长得一模一样。这就是一位圈数信息承载的全部区别:绿色节点双方圈数相同,红色节点写者的圈数比读者多 1。注意状态序列本身是闭合的——FIFO 永远在这六个阶段间循环,没有哪个「空」比另一个更特殊,判据必须在任意一圈都成立。
二、解法一:额外一位 MSB 记录圈数
主流解法是把指针从 n 位加宽到 n+1 位:低 n 位照旧当存储体地址,最高位不参与寻址,只当作圈数标记——写指针每绕回一次翻转一次,读指针同理。于是深度 4 的指针空间从 4 个值扩成 8 个值,地址环被「展开」成一条 0 到 7 的线,同一个地址在两圈里有不同的名字:第 0 圈的槽 0 是 000,第 1 圈的槽 0 是 100。
判定规则随之简单到可以逐字背下来。空:读者在同一圈的同一位置追上写者,扩展指针完全相等,empty = (wptr_ext == rptr_ext)。满:写者套了整整一圈,低 n 位(地址)重合而圈数位相反,full = (wptr_ext == {~rptr_ext[n], rptr_ext[n-1:0]})。复位时双方都是 000,判为空,上电语义天然正确。
异步 FIFO 里指针以格雷码跨域,二进制判据要翻译成格雷语言。格雷码最高位就是二进制最高位,而次高位是二进制最高两位的异或:当二进制只有最高位相反、低位全等时,格雷码恰好表现为最高两位都相反、其余位相同。所以 RTL 里满判定的标准写法是:
// ADDR_WIDTH = n,指针总线宽 n+1
wire [ADDR_WIDTH:0] wbin_next = wbin + (winc & ~wfull);
wire [ADDR_WIDTH:0] wgray_next = (wbin_next >> 1) ^ wbin_next;
// 满:本侧下一拍格雷指针 == 同步过来的读格雷指针,高两位取反
wfull <= (wgray_next == {~rgray_wsync2[ADDR_WIDTH:ADDR_WIDTH-1],
rgray_wsync2[ADDR_WIDTH-2:0]});
// 空:格雷指针完全相等
rempty <= (rgray_next == wgray_rsync2);这段比较式在支柱页出现过,当时把它当结论用;现在可以回答「为什么是高两位而不是最高一位」——多反的那一位,正是圈数位翻转在格雷码里留下的投影。格雷码为什么必须跨域、同步过来的值偏保守,分别在格雷码跨时钟域原理和满空语义与保守延迟里论证。
绕回边界的拍级行为用一张波形收尾。深度 4 的第 4 个写脉冲到来前,寻址位是 3、扩展指针是 011;写沿之后地址回到 0,扩展位翻成 1,格雷码成为 110,满标志当拍拉起。
读这张波形只看同一个写沿上发生的两件事:低两位 3 变 0(绕回),最高位 0 变 1(记圈)。两件事必须在同一拍完成——如果只保留低两位,沿后写指针变成 00,与静止的读指针 00 相等,电路当场失去判断力。扩展位把这一沿的信息多存了一根线,满标志才敢拉起。
三、深度 4 枚举实验:8 次写读遍历全部状态
道理可以推演,歧义必须用穷举坐实。实验取最小的直观深度 4(n=2、指针 3 位),testbench 不实例化完整 FIFO,而是直接对三组行为模型做事件级枚举:①扩展位模型,逐拍打印 {wptr_ext, rptr_ext, full, empty};②无扩展位的 2 位裸指针模型,分别按「相等即空」「相等即满」两种接线策略注错;③n 位指针加占用计数器、只用 3 个槽位的备选模型。连续写满 4 个、逐次读空、再连写 4 个,把第一节状态环上的每个节点都踩一遍。
终端卡逐行对应状态环。第 11 行是满点 A:第 4 写之后 wptr_ext=100、rptr_ext=000,full=1 empty=0,同时打印的格雷码是 wgray=110 rgray=000——高两位恰好双反。第 16 行是双方绕回后的空:两根指针都到 100,full=0 empty=1,与复位空 000/000 用的是同一条相等判据,不需要特判。第 21 行是最容易写错的满点 B:第二圈连写 4 个后写指针自身绕回,wptr_ext=000、rptr_ext=100,此时圈数位关系对调,判据仍然成立,wgray=000 rgray=110。全程扩展位模型误判计数为 0。
四、注错对照:裸指针的两种接线策略都崩
去掉扩展位后,满和空只能由同一个相等信号导出,怎么接线都是错的。注错实验把两种策略都跑了一遍:
[2] 无扩展位注错: 2 位裸指针, full/empty 都只能由 (wn==rn) 导出
复位 wn=00 rn=00 相等=1 | 真值: 空
连写4个后 wn=00 rn=00 相等=1 | 真值: 满(occ=4) 却同时满足空判据
-- 策略E: 相等即空, 写侧无满判据 --
读侧: empty=1, 4 个未读数据存在但读请求被门控 (虚报空)
写侧: full=0 第5写被接受, 槽0 的 A1 被 A5 覆盖; 随后读到 a5 (期望 A1)
-- 策略F: 相等即满 --
复位态 full=1, 首批 4 次写全部被挡, 指针停在 00/00 (启动死锁)策略 E 把相等解释为空:复位态是对的,但写满 4 个后相等再次出现,读侧以为 FIFO 是空的、拒绝读出明明存在的数据,写侧因为没有任何满判据,第 5 写照单全收,槽 0 里尚未读走的 A1 被 A5 当场覆盖,随后读回得到 a5、与期望不符——这正是满空语义与保守延迟所说的漏报满,不可恢复的那一类错误。策略 F 把相等解释为满:写满时判对了,但复位态指针天生相等,上电即满,首批 4 次写全部被挡,FIFO 在空态死锁。一个信号同时承担两个真值,任何接线方式都必然在其中一个状态撒谎。
五、设计与思考:第二条路及其代价
扩展位不是唯一解。另一类工程做法是指针保持 n 位、另设一个占用计数器:每接受一个写 count 加 1,每完成一个读 count 减 1,空判 count==0、满判 count==2^n−1——注意满阈值故意比物理容量少 1,永久保留一个槽位不用。代价与收益都很直白:指针不用加宽,但深度 4 的 FIFO 只能放 3 个数据,浅深下浪费比例高达 25%;深度越大浪费比例越小(深度 1024 时约 0.1%),所以这个方案在深 FIFO 或本就需要占用计数做 almost 标志的设计里并不少见。实验第三组模型实测:count 到 3 后满标志正确拉起,第 4 写被拒绝,全程无歧义,SUMMARY 三行给出全部判决计数:
SUMMARY ext-model 误判=0 (两个满点 wgray 高两位均与对方相反)
SUMMARY naive-E 覆盖=1 读回错配=1; naive-F 复位后饿死写=4
SUMMARY count-model 容量 4->3 浪费 1 项(25%), 拒绝越界写=1, 误判=0两种主流方案放一起决策,维度如下:
| 对比维度 | 方案一:n+1 位扩展指针 | 方案二:n 位指针 + 占用计数器 |
|---|---|---|
| 可用容量 | 全部 2^n 个槽位 | 仅 2^n−1 个,永久浪费 1 槽 |
| 满判据 | MSB 相反、低位相同(格雷码高两位相反) | count == 2^n−1 |
| 空判据 | 扩展指针完全相等 | count == 0 |
| 额外存储 | 两侧各 1 个寄存器位 | 一个 n+1 位双向计数器(含加减仲裁) |
| 异步 FIFO 适配 | 天然适配:指针单向运动,格雷码跨域即可 | 差:count 依赖读写两侧同时钟运算,跨域需额外的握手或指针差逻辑 |
| 工程定位 | 异步 FIFO 标准做法,深浅皆宜 | 同步 FIFO、深 FIFO、需要占用量观测时可选 |
异步场景下方案二还有一处暗坑:占用计数器要在读、写两个时钟沿同时修改,计数器本身成了新的跨域热点,往往还得退回到指针相减的老路,省掉的那一位又从同步逻辑里长回来。因此业界异步 FIFO 几乎一边倒地选扩展位。
为什么不能取巧地把深度设成非 2 的幂、让指针别「恰好」绕回?因为非幂深度需要取模或比较逻辑拦截绕回点,比较器位宽更宽、时序更差,而且只能改变绕回位置、不能消除「追上」与「套圈」的拓扑歧义——圈数信息仍然无处安放。各深度下歧义的形态完全同构,枚举一遍即可看清:
| 地址位 n | 存储深度 | 裸指针状态数 | 指针相等对应的真实占用 | 扩展指针状态数 | 空/满在扩展空间 |
|---|---|---|---|---|---|
| 1 | 2 | 2 | 0 或 2 | 4 | 各唯一 |
| 2 | 4 | 4 | 0 或 4 | 8 | 各唯一 |
| 3 | 8 | 8 | 0 或 8 | 16 | 各唯一 |
| 4 | 16 | 16 | 0 或 16 | 32 | 各唯一 |
规律一行:裸指针空间里每个位置都被空、满两个状态共用,扩展一位后状态数翻倍,两个状态被安放到不同的地址——这就是多一根线买到的全部东西。综合面积上这一位的代价随深度摊薄,支柱页的综合矩阵显示面积第一决定变量是存储总位数而非指针位宽。
代码获取:实验平台在独立公开仓库 derekzhuo/async-fifo-mini 的 tb 目录,本篇对应文件为 tb_pointer_wraparound.v(自包含、不依赖 rtl 目录)。克隆后执行 iverilog -g2012 -s tb_pointer_wraparound -o build/x.vvp tb/tb_pointer_wraparound.v && vvp build/x.vvp 即可复现本文全部输出,本机需安装 Icarus Verilog。
实验环境
- 机器/系统:Mac(Intel)/ macOS,本机原生执行(未使用容器)
- 工具版本:Icarus Verilog 13.0(stable)
- 日期:2026-11-03
- 完整命令:
iverilog -g2012 -s tb_pointer_wraparound -o build/tb_pointer_wraparound.vvp tb/tb_pointer_wraparound.v && vvp build/tb_pointer_wraparound.vvp - 原始记录:本文终端卡与两段日志摘录覆盖 run1.log 全部判决行,实验源码见上述工程包
这一篇在知识体系里的位置
本篇是异步FIFO 专题原理段第四篇。格雷码跨时钟域原理解决跨域值合不合法,满空语义与保守延迟解决误差往哪个方向偏,亚稳态与 MTBF解决为什么打两拍,本篇解决一个更靠前的问题:比较的双方凭什么能表示出满和空——指针必须先有足够的状态空间,前述跨域机制才有正确的语义可传。完整可综合实现照抄异步 FIFO Verilog 模板。下一篇看读接口本身的时序差异:FWFT 与标准 FIFO 读时序对比。
总结
FIFO 指针绕回让 n 位裸指针的相等信号同时对应占用 0 和占用 2^n,环形寻址丢掉的圈数差就是歧义根源。主流解法把指针加宽到 n+1 位:低位寻址、最高位记圈,空判扩展指针全等,满判最高位相反且余位相同,翻译成跨域格雷码即高两位相反、其余相同;深度 4 穷举实验踩遍两个满点与两个空点,格雷值 110/000 与 000/110 精确对上判据,误判为零。对照注错中裸指针「相等即空」导致第 5 写覆盖未读数据、「相等即满」导致上电死锁。备选的占用计数器方案把满阈值压到 2^n−1、永久浪费一个槽,浅深下代价 25%,且计数器在异步场景反成跨域热点,因此扩展位成为异步 FIFO 的标准选择。