之前的理解:让外部不再对FIFO进行写操作,外部需要第一时间获得FIFO已满信息,哪怕信息是滞后的,full和empty同理
下面分三块回答:为什么用格雷码 + 打两拍、为什么满判断在写时钟域、满信号的目的。
一、为什么用写格雷码与打两拍的读格雷码比较?
1. 先看异步 FIFO 的指针在哪
异步 FIFO 有两个时钟域:
- 写时钟域 :写指针
wr_ptr、写数据、写使能 - 读时钟域 :读指针
rd_ptr、读数据、读使能
要判断满,写侧需要知道读指针;要判断空,读侧需要知道写指针。
所以指针必须跨时钟域传递:
rd_ptr从读域 → 写域,用于满判断wr_ptr从写域 → 读域,用于空判断
2. 为什么指针要用格雷码
指针是连续变化的,比如从 7 到 8:
text
二进制:0111 → 1000 (4 位同时变)
格雷码:0100 → 1100 (只有 1 位变)
跨时钟域采样时,如果多位同时变化:
- 采样时刻刚好在变化窗口
- 可能采到新旧混合的中间值
- 比如
0111 → 1000过程中可能采到0000、1111、0110等错误值 - 这会导致空满判断完全错误
格雷码相邻值只变一位,跨域采样时:
- 最多只错一位
- 采到的要么是旧值,要么是新值
- 不会出现多位跳变导致的严重错误
所以指针用格雷码,是为了跨域采样安全。
3. 为什么还要打两拍
即使格雷码只变一位,跨域采样仍可能遇到亚稳态:
- 第一级触发器可能输出中间电平
- 需要第二级触发器让它稳定
- 这就是两级同步器
所以读格雷码进入写域时:
systemverilog
// 读指针格雷码同步到写时钟域
always_ff @(posedge wr_clk or negedge rst_n) begin
if (!rst_n) begin
rd_ptr_gray_sync1 <= '0;
rd_ptr_gray_sync2 <= '0;
end else begin
rd_ptr_gray_sync1 <= rd_ptr_gray;
rd_ptr_gray_sync2 <= rd_ptr_gray_sync1;
end
end
用 rd_ptr_gray_sync2 参与满判断。
4. 为什么同步后是"滞后"的
读指针从读域传到写域,需要:
- 读域寄存器更新:1 拍
- 两级同步器:2 拍
- 组合比较:0 拍
所以写侧看到的读指针,比真实读指针滞后 2~3 拍。
这意味着:
- 写侧以为 FIFO 更满
- 可能提前报
full - 这是保守的,安全,不会溢出
- 代价是损失一点性能
同理,读侧看到的写指针也是滞后的,可能提前报 empty,也是保守安全的。
5. 完整数据流
text
读时钟域 写时钟域
rd_ptr (二进制)
↓
rd_ptr_gray (格雷码)
↓ 跨域
rd_ptr_gray_sync1
↓
rd_ptr_gray_sync2
↓
转回二进制 rd_ptr_bin_sync
↓
与 wr_ptr 比较 → full
写侧满判断的核心:
systemverilog
// 写指针和同步后的读指针比较
// 满:低 N 位相同,最高位不同
assign full = (wr_ptr_bin[N:0] == {~rd_ptr_bin_sync[N], rd_ptr_bin_sync[N-1:0]});
二、为什么在写时钟域做写满判断?
1. 满判断需要什么
判断满,需要比较:
- 写指针:在写时钟域
- 读指针:同步到写时钟域后的值
两者都在写时钟域,所以满判断自然在写时钟域做。
2. 满信号给谁用
full 是给写侧用的:
- 写侧看到
full=1,就停止写 - 防止覆盖还没被读走的数据
- 防止写指针越过读指针
如果满判断放在读时钟域:
- 读侧判断出满,再传给写侧
- 又要跨域同步
- 延迟更大
- 写侧不能第一时间停写
所以满判断必须在写时钟域,就地判断,就地使用。
3. 空判断同理
空判断需要:
- 读指针:在读时钟域
- 写指针:同步到读时钟域后的值
所以空判断在读时钟域做,empty 给读侧用。
4. 总结
| 判断 | 需要的信息 | 在哪个域做 | 信号给谁 |
|---|---|---|---|
| 满 | 写指针 + 同步后的读指针 | 写时钟域 | 写侧 |
| 空 | 读指针 + 同步后的写指针 | 读时钟域 | 读侧 |
一句话:
谁要用,就在谁的域里判断;跨域的信息先同步过来。
三、写满信号产生的目的
你写的第 1 点方向对,我帮你展开和修正:
让外部不再对 FIFO 进行写操作,外部需要第一时间获得 FIFO 已满信息,哪怕信息是滞后的。
更完整的表述:
1. 防止溢出
FIFO 深度有限。如果写侧不停写,读侧来不及读:
- 写指针会追上读指针
- 新数据覆盖旧数据
- 数据丢失
full=1 告诉写侧:别再写了,再写就覆盖了。
2. 流控/反压
full 是一种反压信号:
- 上游看到
full,暂停发送 - 等读侧读走数据,
full撤销 - 上游再继续发
这是握手/流控的基本机制。
3. 第一时间性
full 必须在写时钟域就地产生,因为:
- 写侧每个时钟沿都可能写
- 如果
full晚一拍,就可能多写一次 - 多写一次就可能溢出
所以满判断要在写域组合/时序逻辑里尽快产生。
4. 滞后但保守
由于读指针同步有 2~3 拍延迟:
- 写侧看到的读指针偏旧
- FIFO 可能实际没满,但
full已经拉高 - 这叫保守判断
- 不会溢出,但可能浪费一点带宽
- 这是安全设计,宁可提前停,不可溢出
5. 满信号不是"精确满"
异步 FIFO 的 full 是保守满:
full=1时,FIFO 一定满或快满full=0时,FIFO 不一定不满,可能刚好满但还没同步到- 所以写侧看到
full=0也不能无限写,要靠协议保证
同理,empty 是保守空:
empty=1时,FIFO 一定空或快空empty=0时,不一定真的有数据
6. 修正你的表述
你说:
让外部不再对FIFO进行写操作,外部需要第一时间获得FIFO已满信息,哪怕信息是滞后的
更准确:
满信号的目的,是让写侧在 FIFO 真正溢出之前停止写。由于跨域同步有延迟,满信号可能提前拉高,是保守的。写侧拿到的是"可能满"的信息,宁可早停,不可溢出。
四、一张图总结
text
写时钟域 读时钟域
wr_ptr ──┬── 满判断 ──▶ full ──▶ 写侧停写
│
│ 格雷码
▼
wr_ptr_gray ──跨域──▶ 同步2拍 ──▶ rd_ptr_gray_sync
│
▼
空判断 ──▶ empty ──▶ 读侧停读
▲
│
rd_ptr ──┬── 空判断 ────────────────────────┘
│
│ 格雷码
▼
rd_ptr_gray ──跨域──▶ 同步2拍 ──▶ wr_ptr_gray_sync
│
▼
满判断 ──▶ full
核心:
- 满判断在写域:写指针 + 同步来的读指针
- 空判断在读域:读指针 + 同步来的写指针
- 指针用格雷码:跨域只变一位,安全
- 打两拍:消除亚稳态
- 满/空都是保守的:滞后但安全,宁可早停,不可溢出/下溢
五、自测题
- 异步 FIFO 中,为什么指针要用格雷码而不是二进制?
- 读指针同步到写域,为什么要打两拍?
- 满判断为什么在写时钟域做,而不是读时钟域?
full=1时,FIFO 一定满吗?full=0时,FIFO 一定不满吗?- 满信号滞后会带来什么后果?为什么这种滞后是安全的?
- 空判断需要哪些信息?在哪个域做?
- 如果满判断放在读域,再同步到写域,会有什么问题?
下面按题号给出答案。
1. 异步 FIFO 中,为什么指针要用格雷码而不是二进制?
因为指针要跨时钟域传递,而跨域采样可能发生在指针变化的瞬间。
-
二进制指针 :相邻值可能多位同时变化。
例如
0111 → 1000,4 位同时变。跨域采样时可能采到新旧混合的错误值,比如0000、1111、0110等,导致空满判断完全错误。 -
格雷码指针 :相邻值只变一位。
跨域采样时最多只错一位,采到的要么是旧值,要么是新值,不会出现多位跳变导致的严重错误。
所以格雷码是为了保证指针跨时钟域采样安全,让空满判断不会因为多位同时变化而出错。
2. 读指针同步到写域,为什么要打两拍?
打两拍是为了消除亚稳态。
- 第一级触发器:异步输入可能在 setup/hold 窗口内变化,输出可能进入亚稳态。
- 第二级触发器:让第一级的输出再稳定一个时钟周期,把亚稳态传播概率降到极低。
格雷码只保证"只变一位",但不能消除亚稳态 。
只要跨时钟域采样,就有亚稳态风险,所以必须用两级同步器。
两级同步器的作用:
- 第一级:可能亚稳态。
- 第二级:输出稳定,供本地逻辑使用。
- 大幅提高 MTBF。
3. 满判断为什么在写时钟域做,而不是读时钟域?
因为满判断需要:
- 写指针:在写时钟域。
- 读指针:需要从读时钟域同步到写时钟域。
两者都在写时钟域后,比较最直接、延迟最小。
更重要的是:full 信号是给写侧 用的,写侧看到 full=1 要立即停止写。
如果满判断放在读域:
- 读域先判断满。
- 再把
full同步到写域。 - 又多一级跨域延迟。
- 写侧可能已经多写了一次,导致溢出。
所以满判断必须在写时钟域就地判断、就地使用。
同理,空判断在读时钟域做,因为 empty 是给读侧用的。
4. full=1 时,FIFO 一定满吗?full=0 时,FIFO 一定不满吗?
异步 FIFO 的 full 是保守满。
-
full=1时 :写域看到的读指针是同步过来的,滞后于实际读指针。
写域会高估 FIFO 中已用的空间,所以更容易判满。
因此
full=1时,实际 FIFO 不一定真的满 ,可能读侧已经读走了一些数据,但写域还没看到。但写侧必须停写,因为不能冒险。
-
full=0时 :写域认为没满。由于写域已经高估了已用空间,如果它认为没满,那么实际已用空间只会更少。
所以
full=0时,FIFO 一定实际没满,可以安全写。
一句话:
full=1不一定真满,但不能写;
full=0一定没满,可以写。
同理,empty 也是保守空:
empty=1不一定真空,但不能读。empty=0一定不空,可以读。
5. 满信号滞后会带来什么后果?为什么这种滞后是安全的?
满信号滞后,是指写域看到的读指针比实际读指针旧,导致 full 提前拉高,而不是延迟拉高。
后果:
- 写侧可能过早停止写。
- FIFO 实际还有空间,但写侧认为满了。
- 损失一点性能,浪费一点带宽。
为什么安全:
- 这种滞后是保守的:宁可提前判满,也不延迟判满。
- 如果满信号延迟拉高,写侧会多写,导致写指针追上读指针,覆盖未读数据,造成溢出。
- 提前判满只会少写,不会覆盖数据。
所以:
满信号滞后 = 保守 = 安全,代价是性能略降。
6. 空判断需要哪些信息?在哪个域做?
空判断需要:
- 读指针:在读时钟域。
- 写指针:从写时钟域同步到读时钟域后的值。
所以空判断在读时钟域做。
empty 信号给读侧用:
empty=1时,读侧停止读。empty=0时,读侧可以读。
和满判断对称:
| 判断 | 需要的信息 | 在哪个域做 | 信号给谁 |
|---|---|---|---|
| 满 | 写指针 + 同步后的读指针 | 写时钟域 | 写侧 |
| 空 | 读指针 + 同步后的写指针 | 读时钟域 | 读侧 |
7. 如果满判断放在读域,再同步到写域,会有什么问题?
如果满判断放在读域:
- 读域有读指针。
- 写指针需要从写域同步到读域。
- 同步有延迟,读域看到的写指针是滞后的旧值。
- 读域用旧写指针和当前读指针判断满,会低估已用空间,更不容易判满。
- 判断出的
full会偏晚。 - 再把
full同步到写域,又增加延迟。 - 写侧可能已经多写了,导致溢出。
所以满判断不能放在读域,必须放在写域,用同步过来的读指针。
这样写域看到的是滞后的读指针 ,会高估已用空间,提前判满,保证安全。
一句话:
满判断在写域,用的是"旧的读指针",所以保守;
如果放在读域,用的是"旧的写指针",会误判为不满,导致溢出。
总结表
| 问题 | 核心答案 |
|---|---|
| 为什么用格雷码 | 相邻只变一位,跨域采样安全 |
| 为什么打两拍 | 消除亚稳态,提高 MTBF |
| 满判断在写域 | 写指针在写域,读指针同步过来,就地判断,及时停写 |
| full=1 / full=0 | full=1 不一定真满但不能写;full=0 一定没满可以写 |
| 满信号滞后 | 保守提前判满,损失性能但不会溢出 |
| 空判断在读域 | 读指针在读域,写指针同步过来,就地判断,及时停读 |
| 满判断放读域的问题 | 写指针同步滞后,低估已用空间,判满偏晚,再同步到写域更晚,导致溢出 |