前言
FIFO 满空标志出错,往往不是 RAM 有问题,而是指针回绕、当前/下一状态、同周期读写和跨时钟域处理不严谨。本文从同步 FIFO 到异步 FIFO,结合可综合代码、时序示意和排错清单,讲清满空判断的正确写法与常见误区。
1. 问题通常不是 FIFO 不会存数据
刚开始写 FIFO 时,很多人会觉得逻辑非常简单:
写指针指向下一个写入地址;
读指针指向下一个读取地址;
读写指针相等,FIFO 为空;
写指针追上读指针,FIFO 为满。
实际调试时却经常出现下面这些现象:
FIFO 明明已经写满,
full却晚了一拍;FIFO 已经没有数据,仍然允许继续读一次;
仿真运行几百个周期正常,指针回绕后突然出错;
单独读、单独写都正常,同时读写时数据量统计异常;
同步 FIFO 仿真正常,改成异步 FIFO 后上板偶发丢数;
异步 FIFO 的
empty解除得比较慢,被误认为判断错误。
这些问题的根源通常集中在四个地方:
没有区分“地址相同”和“FIFO 状态相同”;
没有区分当前指针和下一拍指针;
用使能信号代替了真正完成的读写操作;
跨时钟域时直接传递了二进制指针。
先记住一句话:
FIFO 满空判断的本质,不是比较两个 RAM 地址,而是判断读写指针之间的相对位置。
2. 先分清:同步 FIFO 还是异步 FIFO
在写判断逻辑前,必须先确认 FIFO 属于哪一种。
| 类型 | 写时钟 | 读时钟 | 满空判断方式 |
|---|---|---|---|
| 同步 FIFO | 与读时钟相同 | 与写时钟相同 | 计数器或扩展指针 |
| 异步 FIFO | 独立写时钟 | 独立读时钟 | Gray 码指针跨时钟域同步 |
两种 FIFO 的 RAM 结构可能很像,但满空判断不能混用。
同步 FIFO 中,读写逻辑位于同一个时钟域,可以直接使用计数器。
异步 FIFO 中,读写逻辑位于不同的时钟域,不存在一个能够被两个时钟安全修改的公共计数器,因此通常采用:
二进制指针负责地址计算;
二进制指针转换为 Gray 码;
Gray 码经过两级触发器同步到另一个时钟域;
在各自时钟域内判断
full和empty。
一、同步 FIFO 的满空判断
3. 最容易写错的判断:只比较地址位
假设 FIFO 深度为 8,地址范围为:
0、1、2、3、4、5、6、7读写指针都为 3 时,可能有两种完全不同的状态。
情况一:FIFO 为空
读指针:3 写指针:3 有效数据数量:0情况二:FIFO 为满
读指针:3 写指针:3 有效数据数量:8如果只比较 RAM 地址:
assign empty = (wr_ptr == rd_ptr); assign full = (wr_ptr == rd_ptr);显然无法区分满和空。
问题在于,RAM 地址只能表示“指向哪里”,不能表示指针已经绕了多少圈。
4. 方法一:使用计数器判断满空
同步 FIFO 最直观、最适合新人的方法,是增加一个有效数据计数器count。
empty = (count == 0); full = (count == DEPTH);每成功写入一个数据:
count = count + 1;每成功读出一个数据:
count = count - 1;同一拍既读又写:
count 保持不变这里最关键的不是wr_en和rd_en,而是“本次操作是否真的被 FIFO 接受”。
因此应定义两个内部信号:
wr_fire = wr_en && !full; rd_fire = rd_en && !empty;其中:
wr_fire=1:本拍真正完成了一次写入;rd_fire=1:本拍真正完成了一次读取。
后面的指针更新、RAM 操作和计数器更新,都应该使用wr_fire、rd_fire,而不是直接使用外部使能信号。
5. 一个很隐蔽的错误:同时读写时计数器更新错了
很多新人会这样写:
if (wr_fire) count <= count + 1'b1; if (rd_fire) count <= count - 1'b1;单独写入时没有问题,单独读取时也没有问题。
但是,当wr_fire和rd_fire同时为 1 时,同一个always块内对count进行了两次非阻塞赋值。
后面的赋值会覆盖前面的赋值,最终执行的是:
count <= count - 1'b1;而正确结果应该是:
写入一个数据,读取一个数据,有效数据数量不变。正确写法是统一处理:
case ({wr_fire, rd_fire}) 2'b10: count <= count + 1'b1; 2'b01: count <= count - 1'b1; default: count <= count; endcase对应关系如下:
wr_fire | rd_fire | FIFO 数据量变化 |
|---|---|---|
| 0 | 0 | 不变 |
| 0 | 1 | 减 1 |
| 1 | 0 | 加 1 |
| 1 | 1 | 不变 |
6. 可直接使用的同步 FIFO 代码
下面给出一个基础同步 FIFO。
代码支持:
任意大于 1 的 FIFO 深度;
非 2 的整数次幂深度;
同周期读写;
满写保护;
空读保护;
有效数据数量输出。
以下代码为可综合 SystemVerilog。
module sync_fifo #( parameter int DATA_W = 8, parameter int DEPTH = 16 )( input logic clk, input logic rst_n, input logic wr_en, input logic [DATA_W-1:0] wr_data, input logic rd_en, output logic [DATA_W-1:0] rd_data, output logic full, output logic empty, output logic [$clog2(DEPTH+1)-1:0] usedw ); localparam int ADDR_W = (DEPTH <= 2) ? 1 : $clog2(DEPTH); localparam int COUNT_W = $clog2(DEPTH + 1); logic [DATA_W-1:0] mem [0:DEPTH-1]; logic [ADDR_W-1:0] wr_ptr; logic [ADDR_W-1:0] rd_ptr; logic [COUNT_W-1:0] count; logic wr_fire; logic rd_fire; /* * 只有 FIFO 接受操作时,fire 信号才为 1。 */ assign wr_fire = wr_en && !full; assign rd_fire = rd_en && !empty; assign full = (count == DEPTH); assign empty = (count == 0); assign usedw = count; /* * 显式处理指针回绕,因此 DEPTH 不要求必须是 2 的整数次幂。 */ function automatic logic [ADDR_W-1:0] ptr_inc( input logic [ADDR_W-1:0] ptr ); if (ptr == DEPTH - 1) ptr_inc = '0; else ptr_inc = ptr + 1'b1; endfunction always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= '0; rd_ptr <= '0; count <= '0; rd_data <= '0; end else begin /* * 写操作 */ if (wr_fire) begin mem[wr_ptr] <= wr_data; wr_ptr <= ptr_inc(wr_ptr); end /* * 读操作 * * 这里采用寄存器输出方式。 * rd_fire 有效后,数据在时钟沿后更新到 rd_data。 */ if (rd_fire) begin rd_data <= mem[rd_ptr]; rd_ptr <= ptr_inc(rd_ptr); end /* * 有效数据数量更新 */ case ({wr_fire, rd_fire}) 2'b10: count <= count + 1'b1; 2'b01: count <= count - 1'b1; default: count <= count; endcase end end endmodule7. 为什么计数器位宽是$clog2(DEPTH+1)
假设 FIFO 深度为 16。
有效数据数量可能为:
0~16一共需要表示 17 种状态。
如果写成:
logic [$clog2(DEPTH)-1:0] count;当DEPTH=16时,count只有 4 位,只能表示:
0~15无法表示 16,也就无法正常产生满标志。
正确写法是:
logic [$clog2(DEPTH+1)-1:0] count;对于深度 16:
$clog2(17) = 55 位计数器可以表示 0~31,自然能够覆盖 0~16。
8. 满标志为什么经常晚一拍
假设 FIFO 深度为 4,当前已经存了 3 个数据:
count = 3 full = 0此时又写入一个数据。
在时钟沿到来前:
wr_en = 1 full = 0 wr_fire = 1时钟沿到来后:
count = 4 full = 1正确时序如下:
时钟周期 N N+1 N+2 count(沿前) 3 4 4 wr_en 1 1 0 full 0 1 1 wr_fire 1 0 0第 N 拍允许写入第 4 个数据是正确的。
第 N+1 拍full=1,后续写入被阻止。
常见错误是使用当前写指针计算一个寄存器版full,却没有比较写入后的下一指针。这会导致full晚一拍拉高,从而错误接受第 5 次写入。
判断标志时必须明确:
这个标志描述的是当前状态,还是本次操作完成后的下一状态?
9. 方法二:增加一位回绕标志
对于深度为 2 的整数次幂的同步 FIFO,也可以不用计数器,而是给读写指针增加一位。
假设 FIFO 深度为 8:
地址位宽:3 位 扩展指针:4 位指针结构可以理解为:
bit[3] :回绕标志 bit[2:0] :RAM 地址判断方法如下:
assign empty = (wr_ptr == rd_ptr); assign full = (wr_ptr[3] != rd_ptr[3]) && (wr_ptr[2:0] == rd_ptr[2:0]);一般化写法:
assign empty = (wr_ptr == rd_ptr); assign full = (wr_ptr[ADDR_W] != rd_ptr[ADDR_W]) && (wr_ptr[ADDR_W-1:0] == rd_ptr[ADDR_W-1:0]);含义是:
地址位相同、回绕位相同:读写指针位于同一圈,FIFO 为空;
地址位相同、回绕位不同:写指针领先读指针一整圈,FIFO 为满。
这种方法适合深度为:
2、4、8、16、32……如果 FIFO 深度不是 2 的整数次幂,不能简单依赖指针自然溢出,需要显式设计回绕规则。
10. “预留一个位置”的判断法为什么少一个容量
还有一种常见写法:
assign empty = (wr_ptr == rd_ptr); assign full = (wr_ptr_next == rd_ptr);也就是写指针的下一个地址等于读指针时,认为 FIFO 已满。
这种写法本身不一定错误,但它会主动空出一个存储位置。
例如 RAM 实际有 16 个地址:
0~15FIFO 最多只能保存 15 个数据。
这种设计通常称为“预留一个空槽位”。
如果规格要求 FIFO 深度为 16,而实际只能存 15 个数据,就会出现容量不一致。
因此使用这种方法时,要明确区分:
RAM 物理深度:16 FIFO 可用深度:15二、异步 FIFO 的满空判断
11. 异步 FIFO 不能共用一个计数器
异步 FIFO 中存在两个独立时钟:
写时钟 wclk 读时钟 rclk如果让写时钟执行:
count <= count + 1;同时让读时钟执行:
count <= count - 1;就会出现一个寄存器被两个时钟驱动的问题。
这既不能正常综合,也无法保证硬件行为。
所以异步 FIFO 一般不维护跨时钟域共享计数器,而是分别维护:
写时钟域:写指针 读时钟域:读指针然后将指针同步到对方时钟域。
12. 为什么不能直接同步二进制指针
假设一个 4 位二进制指针从 7 变成 8:
7 = 0111 8 = 1000四个位同时发生变化。
真实硬件中,各个位经过组合逻辑和布线的延迟不同,不可能绝对同时到达。
另一个时钟域可能采样到:
0000 0011 1010 1111这些值可能既不是旧值 7,也不是新值 8。
即使每一位都经过两级触发器同步,也不能保证多位总线在同一个周期内保持一致。
两级触发器主要解决的是单比特信号的亚稳态传播问题,并不能自动保证多位二进制总线的一致性。
13. Gray 码为什么适合传递 FIFO 指针
Gray 码相邻两个数之间只有一位发生变化。
二进制转 Gray 码的公式为:
gray = (binary >> 1) ^ binary;例如:
| 二进制 | Gray 码 |
|---|---|
| 000 | 000 |
| 001 | 001 |
| 010 | 011 |
| 011 | 010 |
| 100 | 110 |
| 101 | 111 |
| 110 | 101 |
| 111 | 100 |
从一个合法状态变化到下一个合法状态时,只有一位改变。
因此,即使目标时钟域刚好在跳变附近采样,通常只可能采到:
旧 Gray 码;
新 Gray 码。
这非常适合用于异步 FIFO 的指针同步。
需要注意:
Gray 码降低了多位同时跳变带来的不一致风险,但跨时钟域后仍然需要两级触发器同步。
14. 异步 FIFO 为什么还要保留二进制指针
Gray 码适合同步,但不适合直接计算 RAM 地址和执行加一运算。
因此标准异步 FIFO 通常同时保存两套指针:
binary pointer:地址计算、指针加一 Gray pointer:跨时钟域同步写时钟域:
wbin → wgray → 同步到读时钟域读时钟域:
rbin → rgray → 同步到写时钟域15. 异步 FIFO 的空判断
在读时钟域中:
本地保存当前读 Gray 指针
rgray;接收同步过来的写 Gray 指针
wgray_sync。
当两者相等时,说明读指针已经追上写指针:
empty = (rgray == wgray_sync);如果empty是寄存器输出,通常使用下一状态进行判断:
rbin_next = rbin + rd_fire; rgray_next = (rbin_next >> 1) ^ rbin_next; empty_next = (rgray_next == wgray_sync);然后在读时钟沿更新:
empty <= empty_next;为什么比较rgray_next,而不是当前rgray?
因为如果本拍成功读出了最后一个数据,时钟沿后 FIFO 就应该进入空状态。
16. 异步 FIFO 的满判断
假设 FIFO 深度为:
DEPTH = 2^ADDR_W二进制读写指针需要增加一位回绕位,所以指针总宽度为:
PTR_W = ADDR_W + 1写满的含义是:
写指针比读指针领先一个完整 FIFO 深度对于 Gray 码指针,标准判断方法是:
wgray_next == { ~rgray_sync[PTR_W-1:PTR_W-2], rgray_sync[PTR_W-3:0] }也就是:
同步过来的读 Gray 指针最高两位取反;
其余低位保持不变;
再与下一写 Gray 指针比较。
代码形式如下:
logic [PTR_W-1:0] rgray_full_cmp; assign rgray_full_cmp = { ~rgray_sync[PTR_W-1:PTR_W-2], rgray_sync[PTR_W-3:0] }; assign full_next = (wgray_next == rgray_full_cmp);这里经常有人只取反最高一位:
// 错误示例 assign full_next = (wgray_next == {~rgray_sync[PTR_W-1], rgray_sync[PTR_W-2:0]});这是把二进制扩展指针的判断方式直接套到了 Gray 码上,结果会在特定指针位置产生错误。
对于经典异步 FIFO Gray 指针满判断,需要取反最高两位。
17. 可直接参考的异步 FIFO 代码
下面代码适用于:
DEPTH = 2^ADDR_W ADDR_W >= 2 FIFO 深度至少为 4代码采用:
二进制读写指针;
Gray 码跨时钟域;
两级同步器;
下一状态满空判断;
独立读写时钟。
module async_fifo #( parameter int DATA_W = 8, parameter int ADDR_W = 4 )( input logic wclk, input logic wrst_n, input logic wr_en, input logic [DATA_W-1:0] wr_data, output logic full, input logic rclk, input logic rrst_n, input logic rd_en, output logic [DATA_W-1:0] rd_data, output logic empty ); localparam int DEPTH = 1 << ADDR_W; localparam int PTR_W = ADDR_W + 1; logic [DATA_W-1:0] mem [0:DEPTH-1]; /* * 写时钟域指针 */ logic [PTR_W-1:0] wbin; logic [PTR_W-1:0] wbin_next; logic [PTR_W-1:0] wgray; logic [PTR_W-1:0] wgray_next; /* * 读时钟域指针 */ logic [PTR_W-1:0] rbin; logic [PTR_W-1:0] rbin_next; logic [PTR_W-1:0] rgray; logic [PTR_W-1:0] rgray_next; /* * Gray 指针跨时钟域同步寄存器 */ (* ASYNC_REG = "TRUE" *) logic [PTR_W-1:0] rgray_wq1; (* ASYNC_REG = "TRUE" *) logic [PTR_W-1:0] rgray_wq2; (* ASYNC_REG = "TRUE" *) logic [PTR_W-1:0] wgray_rq1; (* ASYNC_REG = "TRUE" *) logic [PTR_W-1:0] wgray_rq2; logic wr_fire; logic rd_fire; logic full_next; logic empty_next; logic [PTR_W-1:0] rgray_full_cmp; assign wr_fire = wr_en && !full; assign rd_fire = rd_en && !empty; /* * 写指针下一状态 */ assign wbin_next = wbin + wr_fire; assign wgray_next = (wbin_next >> 1) ^ wbin_next; /* * 读指针下一状态 */ assign rbin_next = rbin + rd_fire; assign rgray_next = (rbin_next >> 1) ^ rbin_next; /* * 空判断: * 下一读指针追上同步后的写指针。 */ assign empty_next = (rgray_next == wgray_rq2); /* * 满判断: * 同步后的读 Gray 指针最高两位取反。 */ assign rgray_full_cmp = { ~rgray_wq2[PTR_W-1:PTR_W-2], rgray_wq2[PTR_W-3:0] }; assign full_next = (wgray_next == rgray_full_cmp); /* * 读 Gray 指针同步到写时钟域 */ always_ff @(posedge wclk or negedge wrst_n) begin if (!wrst_n) begin rgray_wq1 <= '0; rgray_wq2 <= '0; end else begin rgray_wq1 <= rgray; rgray_wq2 <= rgray_wq1; end end /* * 写 Gray 指针同步到读时钟域 */ always_ff @(posedge rclk or negedge rrst_n) begin if (!rrst_n) begin wgray_rq1 <= '0; wgray_rq2 <= '0; end else begin wgray_rq1 <= wgray; wgray_rq2 <= wgray_rq1; end end /* * 写时钟域逻辑 */ always_ff @(posedge wclk or negedge wrst_n) begin if (!wrst_n) begin wbin <= '0; wgray <= '0; full <= 1'b0; end else begin if (wr_fire) mem[wbin[ADDR_W-1:0]] <= wr_data; wbin <= wbin_next; wgray <= wgray_next; full <= full_next; end end /* * 读时钟域逻辑 */ always_ff @(posedge rclk or negedge rrst_n) begin if (!rrst_n) begin rbin <= '0; rgray <= '0; rd_data <= '0; empty <= 1'b1; end else begin if (rd_fire) rd_data <= mem[rbin[ADDR_W-1:0]]; rbin <= rbin_next; rgray <= rgray_next; empty <= empty_next; end end endmodule需要说明的是,不同 FPGA 厂商对双时钟 RAM 的推断模板存在差异。
如果综合器没有将mem推断为双口 RAM,可以:
按照厂商推荐模板调整 RAM 描述;
单独例化双口 RAM;
直接使用厂商 FIFO IP。
但无论 RAM 采用哪种实现方式,Gray 指针和满空判断的基本原理不变。
18. 异步 FIFO 的标志为什么会有延迟
异步 FIFO 中,写指针需要经过两级触发器才能进入读时钟域。
因此,写入第一个数据后,读时钟域不会立刻看到写指针变化。
示意如下:
写时钟域: wclk ↑ ↑ ↑ 写入数据 是 wgray G0 → G1 读时钟域: rclk ↑ ↑ ↑ 第一级同步 G0 G1 第二级同步 G0 G0 G1 empty 1 1 0从写入数据到读时钟域解除empty,可能需要两个或更多读时钟周期。
同理,从读出数据到写时钟域解除full,也会有同步延迟。
这种延迟通常是安全的、保守的:
empty多保持一会,只会暂时少读;full多保持一会,只会暂时少写。
它不会造成溢出或下溢。
因此:
异步 FIFO 的满空标志并不保证在另一个时钟域发生变化后立即更新。
如果波形中empty晚两个读时钟周期解除,不要第一时间认为 FIFO 写错了。
19. 异步 FIFO 复位也可能导致满空异常
异步 FIFO 有两个时钟域,也通常有两个复位:
写时钟域复位 wrst_n 读时钟域复位 rrst_n复位后的初始状态应该是:
写指针 = 0 读指针 = 0 full = 0 empty = 1需要特别注意:
19.1 复位释放必须分别同步
异步复位可以异步拉低,但通常应在各自时钟域内同步释放。
也就是:
异步置位,时钟同步撤销否则复位信号可能在时钟沿附近释放,引起寄存器恢复时间或移除时间问题。
19.2 单独复位一个时钟域相当危险
如果写指针已经运行到某个位置,只复位读时钟域:
读指针重新变成 0 写指针保持原值此时两边对 FIFO 状态的理解已经不一致。
除非专门设计了跨时钟域复位握手机制,否则异步 FIFO 复位应被视为:
清空整个 FIFO,读写两侧都要重新初始化。
三、工程中最常见的六类错误
20. 错误一:只比较低地址位
错误代码:
assign empty = (wr_addr == rd_addr); assign full = (wr_addr == rd_addr);结果:
无法区分满和空;
通常在第一次指针回绕后暴露问题。
解决方式:
同步 FIFO 使用计数器;
或给指针增加回绕位;
异步 FIFO 使用扩展指针加 Gray 码。
21. 错误二:使用wr_en、rd_en更新状态
错误代码:
if (wr_en) wr_ptr <= wr_ptr + 1'b1;当 FIFO 已满时,即使数据没有真正写入,写指针仍然会移动。
正确写法:
wr_fire = wr_en && !full; if (wr_fire) wr_ptr <= wr_ptr + 1'b1;读操作同理:
rd_fire = rd_en && !empty;外部使能表示“请求”,fire表示“请求被接受”。
22. 错误三:同一寄存器在一个时序块中被多次赋值
错误代码:
if (wr_fire) count <= count + 1'b1; if (rd_fire) count <= count - 1'b1;同时读写时,后一次赋值覆盖前一次赋值。
正确方式是使用case统一处理。
23. 错误四:当前指针和下一指针混淆
如果标志在时钟沿被寄存,通常需要根据下一状态计算。
异步 FIFO 中应使用:
wgray_next rgray_next而不是只比较当前指针。
否则可能出现:
full晚一拍;empty晚一拍;多接受一次写入;
多接受一次读取。
24. 错误五:把二进制总线直接跨时钟域
错误做法:
always_ff @(posedge rclk) begin wptr_sync1 <= wptr_binary; wptr_sync2 <= wptr_sync1; end两级同步器不能保证多位二进制总线的一致性。
正确方式:
binary pointer ↓ 转换为 Gray pointer ↓ 两级触发器同步 ↓ 在目标时钟域比较25. 错误六:认为异步 FIFO 标志必须立即变化
写时钟域发生变化后,读时钟域需要等待同步器传递指针。
因此:
写入数据后,
empty可能晚几个读时钟周期解除;读出数据后,
full可能晚几个写时钟周期解除。
这种保守延迟通常不是错误。
真正危险的是:
empty提前解除;full提前解除;在未知指针状态下允许非法读写。
四、仿真时应该重点观察什么
26. 同步 FIFO 建议加入的波形信号
至少观察下面这些信号:
clk rst_n wr_en wr_fire wr_data wr_ptr rd_en rd_fire rd_data rd_ptr count full empty不要只看wr_en和rd_en。
真正决定 FIFO 状态的是:
wr_fire rd_fire27. 异步 FIFO 建议加入的波形信号
写时钟域:
wclk wr_en wr_fire wbin wgray rgray_wq1 rgray_wq2 full读时钟域:
rclk rd_en rd_fire rbin rgray wgray_rq1 wgray_rq2 empty通过这些信号,可以清楚看到:
本地二进制指针何时更新;
Gray 指针何时变化;
指针何时通过第一级同步器;
指针何时通过第二级同步器;
满空标志何时更新。
28. 同步 FIFO 建议加入的断言
在仿真环境中,可以增加一些基本断言。
/* * count 不允许超过 FIFO 深度。 */ assert property ( @(posedge clk) disable iff (!rst_n) count <= DEPTH ); /* * empty 必须与 count==0 一致。 */ assert property ( @(posedge clk) disable iff (!rst_n) empty == (count == 0) ); /* * full 必须与 count==DEPTH 一致。 */ assert property ( @(posedge clk) disable iff (!rst_n) full == (count == DEPTH) );还可以监控非法请求:
always_ff @(posedge clk) begin if (rst_n) begin if (wr_en && full) $warning("FIFO overflow request"); if (rd_en && empty) $warning("FIFO underflow request"); end end这里监控的是非法请求,不一定意味着 FIFO 内部已经损坏。
只要内部使用:
wr_fire = wr_en && !full; rd_fire = rd_en && !empty;就能阻止非法操作真正改变状态。
五、推荐的测试场景
29. 不要只测试“先写满,再读空”
至少应覆盖以下场景。
场景一:复位后立即读取
预期:
empty = 1 rd_fire = 0 读指针不移动场景二:连续写到满
预期:
成功写入 DEPTH 个数据 full 拉高 第 DEPTH+1 次写请求被阻止场景三:连续读到空
预期:
成功读取 DEPTH 个数据 empty 拉高 后续读请求被阻止场景四:同时读写
预期:
读写都成功时,数据数量保持不变场景五:多次指针回绕
不能只运行十几个时钟周期。
建议让读写指针至少回绕几十次,检查长时间运行后是否出现:
数据顺序错误;
满空标志错误;
指针错位。
场景六:异步时钟频率不同
例如:
wclk = 100 MHz rclk = 73 MHz不要只使用整数倍时钟。
还应改变两个时钟的初始相位,增加跨时钟域边界条件覆盖。
场景七:随机读写
随机产生:
wr_en rd_en wr_data同时在测试平台中维护一个软件队列。
FIFO 每成功写入一个数据,就向软件队列压入数据;每成功读取一个数据,就与软件队列队首比较。
这是验证 FIFO 数据顺序最有效的方法之一。
六、排错速查表
| 波形现象 | 优先检查 |
|---|---|
| 写满后还能多写一次 | 是否使用下一写指针计算full |
| 读空后还能多读一次 | 是否使用下一读指针计算empty |
| 同时读写时计数器变化 | 是否对count进行了两次非阻塞赋值 |
| 第一次回绕后满空错误 | 是否缺少回绕位 |
| 16 深度只能写 15 个 | 是否使用了预留空槽位方案 |
| 仿真正常,上板偶发错误 | 是否直接跨时钟域传递二进制指针 |
异步 FIFO 写入后empty未立即解除 | 是否属于两级同步器的正常延迟 |
| 复位后偶发错误 | 复位是否在各时钟域同步释放 |
full一直不解除 | 读 Gray 指针是否正确同步到写时钟域 |
empty一直不解除 | 写 Gray 指针是否正确同步到读时钟域 |
| 综合后没有推断出 RAM | RAM 描述是否符合目标器件模板 |
| 数据顺序错误但满空正常 | RAM 读延迟、读数据有效时序是否理解错误 |
七、最后总结
FIFO 满空判断最容易出错的地方,不是代码有多复杂,而是几个概念容易混在一起。
同步 FIFO 中要注意:
地址相同不一定代表 FIFO 为空,也可能代表 FIFO 为满;
推荐使用有效数据计数器,或者增加一位回绕标志;
指针和计数器必须根据
wr_fire、rd_fire更新;同时读写时,数据数量应该保持不变;
非 2 的整数次幂深度需要显式处理指针回绕。
异步 FIFO 中要注意:
二进制指针不能直接跨时钟域;
Gray 码跨时钟域后仍然需要两级同步器;
空判断在读时钟域完成;
满判断在写时钟域完成;
满判断需要对同步读 Gray 指针的最高两位取反;
异步 FIFO 的满空标志存在保守同步延迟;
复位应在各自时钟域同步释放,单侧复位要谨慎。
真正稳定的 FIFO 设计,必须保证下面这条关系始终成立:
FIFO 内有效数据数量 = 成功写入次数 - 成功读取次数这里说的是成功操作次数,而不是外部请求次数。
只要围绕这条关系检查指针、计数器、满空标志和跨时钟域同步,大多数 FIFO 问题都能很快定位。
#FPGA #Verilog #System #Verilog #FIFO 异步FIFO #同步FIFO #Gray码 #CDC #跨时钟域 #数字电路