当前位置: 首页 > news >正文

FPGA FIFO 为什么会多写一拍、少读一拍?从指针回绕到 Gray 码讲透满空判断

前言

FIFO 满空标志出错,往往不是 RAM 有问题,而是指针回绕、当前/下一状态、同周期读写和跨时钟域处理不严谨。本文从同步 FIFO 到异步 FIFO,结合可综合代码、时序示意和排错清单,讲清满空判断的正确写法与常见误区。


1. 问题通常不是 FIFO 不会存数据

刚开始写 FIFO 时,很多人会觉得逻辑非常简单:

  • 写指针指向下一个写入地址;

  • 读指针指向下一个读取地址;

  • 读写指针相等,FIFO 为空;

  • 写指针追上读指针,FIFO 为满。

实际调试时却经常出现下面这些现象:

  1. FIFO 明明已经写满,full却晚了一拍;

  2. FIFO 已经没有数据,仍然允许继续读一次;

  3. 仿真运行几百个周期正常,指针回绕后突然出错;

  4. 单独读、单独写都正常,同时读写时数据量统计异常;

  5. 同步 FIFO 仿真正常,改成异步 FIFO 后上板偶发丢数;

  6. 异步 FIFO 的empty解除得比较慢,被误认为判断错误。

这些问题的根源通常集中在四个地方:

  • 没有区分“地址相同”和“FIFO 状态相同”;

  • 没有区分当前指针和下一拍指针;

  • 用使能信号代替了真正完成的读写操作;

  • 跨时钟域时直接传递了二进制指针。

先记住一句话:

FIFO 满空判断的本质,不是比较两个 RAM 地址,而是判断读写指针之间的相对位置。


2. 先分清:同步 FIFO 还是异步 FIFO

在写判断逻辑前,必须先确认 FIFO 属于哪一种。

类型写时钟读时钟满空判断方式
同步 FIFO与读时钟相同与写时钟相同计数器或扩展指针
异步 FIFO独立写时钟独立读时钟Gray 码指针跨时钟域同步

两种 FIFO 的 RAM 结构可能很像,但满空判断不能混用。

同步 FIFO 中,读写逻辑位于同一个时钟域,可以直接使用计数器。

异步 FIFO 中,读写逻辑位于不同的时钟域,不存在一个能够被两个时钟安全修改的公共计数器,因此通常采用:

  1. 二进制指针负责地址计算;

  2. 二进制指针转换为 Gray 码;

  3. Gray 码经过两级触发器同步到另一个时钟域;

  4. 在各自时钟域内判断fullempty


一、同步 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_enrd_en,而是“本次操作是否真的被 FIFO 接受”。

因此应定义两个内部信号:

wr_fire = wr_en && !full; rd_fire = rd_en && !empty;

其中:

  • wr_fire=1:本拍真正完成了一次写入;

  • rd_fire=1:本拍真正完成了一次读取。

后面的指针更新、RAM 操作和计数器更新,都应该使用wr_firerd_fire,而不是直接使用外部使能信号。


5. 一个很隐蔽的错误:同时读写时计数器更新错了

很多新人会这样写:

if (wr_fire) count <= count + 1'b1; if (rd_fire) count <= count - 1'b1;

单独写入时没有问题,单独读取时也没有问题。

但是,当wr_firerd_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_firerd_fireFIFO 数据量变化
00不变
01减 1
10加 1
11不变

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 endmodule

7. 为什么计数器位宽是$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) = 5

5 位计数器可以表示 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~15

FIFO 最多只能保存 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 码
000000
001001
010011
011010
100110
101111
110101
111100

从一个合法状态变化到下一个合法状态时,只有一位改变。

因此,即使目标时钟域刚好在跳变附近采样,通常只可能采到:

  • 旧 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,可以:

  1. 按照厂商推荐模板调整 RAM 描述;

  2. 单独例化双口 RAM;

  3. 直接使用厂商 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_enrd_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_enrd_en

真正决定 FIFO 状态的是:

wr_fire rd_fire

27. 异步 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

通过这些信号,可以清楚看到:

  1. 本地二进制指针何时更新;

  2. Gray 指针何时变化;

  3. 指针何时通过第一级同步器;

  4. 指针何时通过第二级同步器;

  5. 满空标志何时更新。


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 指针是否正确同步到读时钟域
综合后没有推断出 RAMRAM 描述是否符合目标器件模板
数据顺序错误但满空正常RAM 读延迟、读数据有效时序是否理解错误

七、最后总结

FIFO 满空判断最容易出错的地方,不是代码有多复杂,而是几个概念容易混在一起。

同步 FIFO 中要注意:

  1. 地址相同不一定代表 FIFO 为空,也可能代表 FIFO 为满;

  2. 推荐使用有效数据计数器,或者增加一位回绕标志;

  3. 指针和计数器必须根据wr_firerd_fire更新;

  4. 同时读写时,数据数量应该保持不变;

  5. 非 2 的整数次幂深度需要显式处理指针回绕。

异步 FIFO 中要注意:

  1. 二进制指针不能直接跨时钟域;

  2. Gray 码跨时钟域后仍然需要两级同步器;

  3. 空判断在读时钟域完成;

  4. 满判断在写时钟域完成;

  5. 满判断需要对同步读 Gray 指针的最高两位取反;

  6. 异步 FIFO 的满空标志存在保守同步延迟;

  7. 复位应在各自时钟域同步释放,单侧复位要谨慎。

真正稳定的 FIFO 设计,必须保证下面这条关系始终成立:

FIFO 内有效数据数量 = 成功写入次数 - 成功读取次数

这里说的是成功操作次数,而不是外部请求次数。

只要围绕这条关系检查指针、计数器、满空标志和跨时钟域同步,大多数 FIFO 问题都能很快定位。

#FPGA #Verilog #System #Verilog #FIFO 异步FIFO #同步FIFO #Gray码 #CDC #跨时钟域 #数字电路

http://www.cnnetsun.cn/news/4171770.html

相关文章:

  • 【Python量化实战 #05】财报三大报表看花眼?3 步用 Python 拉齐资产负债表、利润表与现金流
  • 金融大模型安全框架FinHarness:为AI智能体编织实时防护网
  • C++函数模板编译机制解析:从蓝图到实例化的完整过程
  • 统计学习入门:从数据中学习规律,掌握预测与推断的核心方法
  • 橙皮书共读|Hermes Agent(二)深度拆解五大核心支柱:自进化智能体的运行内核
  • 嵌入式系统核心MCU、MPU与SoC深度解析:从概念到实战选型指南
  • AI智能体通信格式基准测试:TOON、TRON与JSON的性能较量
  • AI大模型学习路线:从零基础到求职实战
  • Windows 提权方法与步骤
  • Effective C++ 学习笔记 条款43 学习处理模板化基类内的名称
  • ACM模式训练系统:从解题到工程化交付的实战指南
  • Linux PipeWire深度解析之pw_context_connect调用流程与实战(七十七)
  • 深入解析JavaScript原型链继承:从原理到ES6 Class的底层实现
  • 【MATLAB例程,车联网16】基于V2X通信的干线绿波速度引导控制仿真——多交叉口信号相位信息驱动的车速动态优化,对比无引导的停车次数、总延误、时距轨迹及交叉口通过时间。附下载链接
  • Lucas定理优化实现:大组合数模小质数的高效计算
  • 嵌入式开发工程师转型:从C语言到Linux驱动的系统学习路径与实战指南
  • [论文学习]VIPER-MCP:检测与利用模型上下文协议服务器中的汙点型漏洞
  • 数据流健康度评估与故障传播建模:从系统韧性到应急决策优化
  • 蓝桥杯真题解析:素因子去重算法与质因数分解优化
  • 2026年教育行业客户体验管理系统推荐:AI大模型VOC智能归因与投诉工单自动分类实践
  • 法国公司注册证明(K-bis)全解读:一文看懂法国企业的“身份证”
  • 2056台机器人北京集结,世界人形机器人运动会开赛
  • Visual Studio代码颜色自定义:从显示项到C/C++开发环境优化
  • 生产级MCP落地指南:FastMCP与官方MCP SDK的选型、架构与实战
  • 三维动画如何成为医学设备技术沟通的工程级解决方案
  • LLM代码生成与任务规划中的采样-验证模式:原理、风险与工程实践
  • 广深莞定制纸箱批量采购:综合成本与隐性物流成本核算指南
  • 【框架】日志-SLF4J+Logback
  • 产品说“用户不会这么用“,我的告警群先笑了
  • Java main class搞不懂?新手看完直接开窍,别再懵了