本页是关于 FIFO 的五页系列教程中的第四页。本页介绍一种应对可能发生溢出(overflow)情况的方法。
引言
在很多使用 FIFO 的应用中,根本无法控制到达数据的流动。例如,在数据采集(data acquisition)应用中,采集到的数据被逻辑直接写入 FIFO;如果 FIFO 已经是 full(满),这些数据就会丢失。在这类应用中,FIFO 的读取方必须足够快地消费其中的数据,以使 FIFO 永远不会变 full(满)。
但如果 FIFO 变成了 full(满),数据的连续性就会被破坏,因为发生了数据丢失:FIFO 会忽略后续的写入尝试,因此 FIFO 中的内容是不正确的。这种情况称为溢出。
溢出是某种故障的结果,所以应当尽量避免。然而,如果溢出还是发生了,那么及时检测到这一点并尽量减小损失仍然十分重要。下面提出一种应对策略。
改进型 FIFO
主要工作始终应该是防止溢出发生。但如果这仍然失败了,那么剩下的唯一能做的事,就是确保没有错误的数据被从 FIFO 中读出。换句话说,从 FIFO 读出的所有数据都是连续且正确的。
建议的解决方案是一种改进型 FIFO,它有以下两点不同:
- 当发生溢出(即 FIFO 变 full(满))后,在 FIFO 被复位之前,不能再向其中写入任何数据。
- 当溢出之后 FIFO 变为 empty(空)时,FIFO 会报告:在复位发生之前它将一直保持 empty(空)。我们把这种情况称为 EOF。
第一个特点保证了从改进型 FIFO 读出的数据始终连续且正确:一旦数据的连续性被破坏,FIFO 就拒绝写入。第二个特点(EOF)则向使用 FIFO 的逻辑发出“出了故障”的信号,使该逻辑有机会重新启动数据流。
EOF 这个名字来自 End of File(文件结束):这种改进型 FIFO 非常适合基于 Xillybus IP 核(IP core)的数据采集应用。在这类应用中,Xillybus IP 核把数据传输到计算机;计算机上的一个简单程序利用标准文件 I/O API 从 FIFO 读取数据。换句话说,该程序以通常的方式打开一个文件并从中读取数据。FIFO 的 EOF 会让这个文件表现得就像读到了普通文件的末尾一样,这是对“再也没有数据可读”这一事实的自然响应。
Verilog 实现
下面是一个 Verilog 模块示例,它实现了上面提出的改进型 FIFO:
module eof_fifo
(
input rst,
input wr_clk,
input rd_clk,
input [31:0] din,
input wr_en,
input rd_en,
output [31:0] dout,
output full,
output empty,
output eof
);
reg rst_sync;
reg rst_cross;
reg fifo_has_been_full;
reg fifo_has_been_nonfull;
reg has_been_full_cross;
reg has_been_full;
assign ok_to_write = !rst_sync && !full && !fifo_has_been_full;
assign eof = empty && has_been_full;
always @(posedge wr_clk)
begin
if (!full)
fifo_has_been_nonfull <= 1;
else if (rst_sync)
fifo_has_been_nonfull <= 0;
if (full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (rst_sync)
fifo_has_been_full <= 0;
end
// Clock domain crossing logic: asynchronous -> wr_clk
always @(posedge wr_clk)
begin
rst_cross <= rst;
rst_sync <= rst_cross;
end
// Clock domain crossing logic: wr_clk -> rd_clk
always @(posedge rd_clk)
begin
has_been_full_cross <= fifo_has_been_full;
has_been_full <= has_been_full_cross;
end
fifo fifo_ins
(
.rst(rst),
.wr_clk(wr_clk),
.rd_clk(rd_clk),
.din(din),
.wr_en(wr_en && ok_to_write),
.rd_en(rd_en),
.dout(dout),
.full(full),
.empty(empty)
);
endmodule
可以很明显地看出,这个模块由一个标准 FIFO 的实例化(instantiation)加上一些额外的逻辑组成。请注意,这个模块的端口几乎与标准 FIFO 的端口完全一样,唯一的区别是:改进型 FIFO 多了一个名为 @eof 的端口。
还需要注意 @din 和 @dout 的位宽是 32 位。如果你希望 FIFO 使用不同的位宽,唯一需要修改的就是模块开头的这两个端口声明。
改进型 FIFO 带有一个 @full(满)端口,而这个端口不一定是必需的。向 FIFO 写入数据的逻辑完全可以忽略它:如果 FIFO 确实变 full(满)了,反正你也做不了什么。无论怎样,FIFO 都会忽略之后试图写入的数据。在大多数应用中,知道 FIFO 已经变 full(满)并没有太大帮助:最好是等到 @eof 变高,然后重新启动整套机制。
防止溢出后的写入操作
如前所述,这个模块以标准 FIFO 为基础。除一个端口之外,标准 FIFO 的所有端口都直接连接到 eof_fifo 的端口上,这个例外就是 @wr_en。它被接到了 “wr_en && ok_to_write” 上。因此很明显,@ok_to_write 的作用是在 FIFO 曾经变 full(满)之后停止写入操作。这个 wire 的定义如下:
assign ok_to_write = !rst_sync && !full && !fifo_has_been_full;
从这个表达式可以看出,有三种情况会阻止写入操作:
- 当 FIFO 被复位时
- 当 FIFO 已经是 full(满)时
- 当 FIFO 在过去的某个时候曾经 full(满)过
前两个条件不言自明。让我们把注意力放在第三个条件上,它由 @fifo_has_been_full 来表示:
always @(posedge wr_clk)
begin
if (!full)
fifo_has_been_nonfull <= 1;
else if (rst_sync)
fifo_has_been_nonfull <= 0;
if (full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (rst_sync)
fifo_has_been_full <= 0;
end
当 FIFO 曾经 full(满)时,@fifo_has_been_full 为高。当 @full(满)和 @fifo_has_been_nonfull 同时为高时,这个寄存器会变为高。
第一部分并不奇怪:@full 连接的是 FIFO 的 “full” 端口。但为什么需要 @fifo_has_been_nonfull?原因在于,FIFO 在复位期间往往会一直保持其 “full” 输出为高,这个特性的目的是告诉应用逻辑:FIFO 还没有准备好接收数据。@fifo_has_been_nonfull 的作用,就是防止在这种情况下把 @fifo_has_been_full 错误地置为高。
当 FIFO 被复位时,@fifo_has_been_full 会变回低电平。换句话说,在发生溢出之后,复位 FIFO 是让改进型 FIFO 恢复正常运行的唯一方法。请注意,@rst 是一个异步复位(asynchronous reset)信号,因此有必要添加逻辑,以生成一个属于正确时钟域的复位信号。这个信号就是 @rst_sync,它是 @rst 的一个副本。
产生 EOF
当下面两个条件同时满足时,@eof 端口为高:
- FIFO 中的数据已被全部读出:FIFO 是 empty(空)的。
- 不会再向 FIFO 写入数据:由于 FIFO 曾经 full(满)过,@fifo_has_been_full 保持为高,因此 @ok_to_write 会一直保持低电平,直到 FIFO 被复位。
上述条件在 Verilog 代码中表示为:
assign eof = empty && has_been_full;
注意,这个表达式依赖于 @has_been_full 而不是 @fifo_has_been_nonfull:@eof 和 @empty(空)都属于 @rd_clk 的时钟域,而 @fifo_has_been_nonfull 属于 @wr_clk 的时钟域。因此,@fifo_has_been_nonfull 需要通过一次时钟域跨越(clock domain crossing)被复制到 @rd_clk 的时钟域中,这个副本就是 @has_been_full。
所以 @eof 的含义是:“不仅 FIFO 现在是 empty(空),而且在你复位它之前,它也不会再被填满。”
结论
在很多应用中,不可能保证永远不会发生溢出。如果这种事件不可接受,那么可以在它发生时尽量减小损失:策略是让溢出之前已经写入的数据正常通过,在此之后不再允许新的数据写入。最终 FIFO 会变成 empty(空)。当这一刻到来时,FIFO 通过 @eof 端口报告:“不会再有任何数据到来了”。
这种方法保证了从 FIFO 读出的数据是连续的:不会因为 FIFO 已满而丢失中间部分的数据。因此它很适合数据采集应用。
本页展示了如何实现一个基于这种策略的改进型 FIFO。这种改进型 FIFO 可以直接替换标准 FIFO 使用。唯一重要的区别是:使用 FIFO 的逻辑必须关注 @eof 端口——当这个端口变为高电平时,逻辑必须复位 FIFO,并重新启动数据流。
至此,本系列关于 FIFO 的第四页就结束了。下一页将展示如何把“标准 FIFO”改造成 FWFT FIFO(反之亦然),以及如何改善时序。