news 2026/10/2 1:31:53

Xilinx FIFO读模式深度解析:Standard与FWFT到底差在哪?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Xilinx FIFO读模式深度解析:Standard与FWFT到底差在哪?

早些年我在一个AXI DMA接口上调试Xilinx同步FIFO IP核,波形上看empty已经拉低,rd_en也给了,dout上却不是预期数据,整整调了两天。最后排查下来根本不是逻辑写错,而是FIFO生成时选了默认的Standard读模式,数据在rd_en之后还要再等一拍才出现在dout上。这个经历让我后来每次例化FIFO都会先确认读模式,也想清楚了一件事:Standard和FWFT之间不只是“延迟一拍”的差别,而是读数据约定的本质差异。

这篇博客从两种模式的行为语义讲起,配合完整的Vivado仿真代码,把读时序、valid信号、握手方式、适用场景和工程里的坑一次说清楚。无论是刚开始用FIFO IP核的初学者,还是已经在项目里被读时序坑过的工程师,都能拿来做选型参考。

1. 两种读模式的差异,从一次“读错数据”的bug说起

1.1 事件回顾:empty已经拉低,为什么dout还是老数据

当时模块的逻辑大概是这样的:FIFO的empty信号拉低后,状态机判断“非空”,于是拉高rd_en,然后在下一个时钟上升沿直接采样dout。理论上看很顺,empty有效代表有数据,读使能给了,数据应该跟着出来。但仿真里看到的却是:rd_en拉高的那一拍,dout还是上一个旧值,再等一拍才变成新数据,整个数据流整体慢了一拍,后续所有逻辑全对不上。

这就是Standard模式最典型的行为:rd_en是一个读请求,不是数据有效指示。发出请求后,FIFO会在下一个时钟沿把数据送到dout。如果你以为rd_en有效的那拍就应该采样,那采到的必然是旧值。

与此相对,FWFT模式(First Word Fall Through,首字直通)是另一套约定:只要FIFO非空,第一个数据就会提前出现在dout上,rd_en变成了“当前数据我收下了”的消费信号。这两种模式对应着完全不同的接口握手设计,选择错误往往不会直接报错,而是让整个系统在时序上慢慢“跑偏”,排查起来相当隐蔽。

1.2 Standard与FWFT的本质区别:数据什么时候被放在dout上

Standard模式可以理解成“点菜模型”:你告诉后厨要什么菜(拉高rd_en),后厨开始做(内部读指针移动),菜做好端出来(dout更新),整个过程需要一拍。读取方必须有一个“等数据返回”的状态,不能假设dout在请求同一拍就有效。

FWFT模式则是“自助餐模型”:菜已经摆在台面上(FIFO非空时dout一直有效),你只需要拿走(rd_en有效)。数据不是“读出来的”,而是“天然待在那里等你取”,读取方少了一个请求到响应的等待周期。

这个区别看似很小,但会直接影响状态机怎么设计:

  • Standard模式下,状态机通常是IDLE -> 拉rd_en -> WAIT -> 采样dout -> 处理;
  • FWFT模式下,状态机可以直接跳过等待,看到valid有效就采样,同时拉rd_en确认消费。

1.3 一张表看清四种关键差异

对比维度Standard FIFOFWFT FIFO
数据出现时机rd_en有效后的下一个时钟沿,dout才更新FIFO非空时,首个数据自动出现在dout
rd_en含义读请求,触发一次读操作接受/消费当前数据
valid信号行为读请求后数据有效的那一拍拉高FIFO非空且dout有效时拉高
empty拉低后的行为仍需等待读请求和数据延迟拉低后首个数据同时可见
典型接口适配寄存器轮询、自定义请求-响应AXI4-Stream、流水线消费
组合逻辑开销较低相对略高,需要额外首字保持逻辑

2. Standard读模式:rd_en发出后还要再等一拍的“请求-应答”机制

2.1 Standard模式的时序拆解

Standard模式的完整读取过程可以拆成三步:

  1. FIFO内写入数据后,写指针变化,empty在下一个时钟沿拉低;
  2. 读取方检测到非空,拉高rd_en;
  3. 下一个时钟沿,内部读指针指向目标地址,dout更新,valid(如果配置了该信号)随之拉高。

连续读取时,每个时钟周期可以读出一个数据,前提是rd_en保持有效且FIFO一直没有读空。如果你用Block RAM实现FIFO,这个“一拍延迟”其实来自BRAM的读出延迟;用分布式RAM时IP核也会用寄存器把时序打整齐,保证输出行为一致。

时序上最需要注意的是empty和dout的相位关系。empty拉低只是告诉你“FIFO里有货”,不代表“货已经在dout上”。很多新手直接把empty当作数据有效信号来用,这在Standard模式下是错的,empty之后通常还要经过一次rd_en触发才能采样数据。

2.2 适合Standard模式的场景

Standard模式最适合请求-响应式的读取场景。比如一个状态机要通过FIFO读固定长度的数据包:状态机先查empty,非空则拉rd_en,下一拍读取dout并开始处理。整个过程每个节拍都是一个明确的“请求-响应”配对,逻辑上非常容易推导。

还有一个场景也常选Standard模式:当FIFO连接的总线接口本身就需要等待状态时。比如挂在自定义寄存器总线上的模块,总线发起读请求后本来就要等一拍或两拍才能拿到数据,FIFO用Standard模式反而能和总线的等待状态自然对齐。

另外,当系统工作频率比较高、时序比较紧张时,Standard模式通常更友好。因为dout在rd_en之后才通过寄存器更新,数据路径上不要求“dout提前稳定”,对后端时序收敛的压力比FWFT小。

2.3 Standard模式常见的两个误区

误区一是把rd_en当成采样信号。有些人写代码时看到empty拉低就直接拉rd_en,然后在同一个状态里对dout进行数据处理,这样必然会采到上一拍的旧数据。正确做法是拉rd_en之后至少再等一拍,在数据有效的那一拍做采样。

误区二是不管empty,直接把rd_en当成自由时钟信号持续拉高。这会让FIFO在空状态下反复执行无效读操作。对于Xilinx IP核,空状态下rd_en会被内部逻辑忽略,但连续的空读会使你的监控逻辑看到错误的empty/valid变化,间接增加排查难度。

每一次读操作前都应该检查empty,或者使用valid信号作为“数据是否可采”的判据,而不是盲拉rd_en。

3. FWFT读模式:数据先行出现,rd_en变成“消费”信号

3.1 FWFT的直通机制

FWFT模式下,当你向FIFO写入第一个数据后,即使还没拉rd_en,dout也会直接变成这个数据。此时如果配置了valid信号,valid会拉高,表示“dout上有数据且可以直接取走”。当rd_en有效时,当前数据被消费,下一个时钟沿dout更新为FIFO中的下一个数据。

这里面有个关键概念转换:Standard模式下rd_en是“请把数据给我”,FWFT模式下rd_en是“这个数据我收下了”。同样是拉高rd_en,语义完全不同。FWFT读取方更像是流水线上的下游工位,数据流到工位时直接拿走,不需要先发起请求再等响应。

valid信号在FWFT模式下变得非常关键。它标记的是“dout当前是否有效”,而不是“刚刚完成一次读操作”。只要FIFO非空,valid就应该是高,dout也稳定在有效数据上。读取方可以无条件等待valid拉高,然后采样dout,同时拉rd_en,等下一拍继续。

3.2 适合FWFT模式的场景

FWFT模式在AXI4-Stream接口场景下几乎是首选。AXI4-Stream的握手信号是TVALID/TREADY,语义正是“数据已经准备好,等对方接收”。FWFT模式下:

  • TVALID可以直接映射到FIFO的valid;
  • TREADY可以映射到rd_en;
  • TDATA自然对应dout。

这组映射几乎不需要额外状态转换,非常顺畅。

另一个常见场景是“需要先看数据再决定是否接收”的模块。比如一个数据过滤单元,要先判断包头地址是否匹配,匹配才接收,不匹配就丢弃。如果用Standard模式,你得先把数据读出来,判断之后才能决定是否消费,但此时数据已经被读走了,操作上非常别扭。FWFT则可以先观察dout上的数据,再决定这一拍要不要拉rd_en,天然支持这种“先看再吃”的逻辑。

3.3 FWFT模式不能盲目使用

FWFT模式并不是所有场景的万能解。第一,dout上提前出现的组合数据会给数据路径增加额外的组合逻辑,在高速设计中可能成为时序瓶颈。第二,FIFO空与非空切换瞬间,valid和empty信号在时钟沿附近的变化可能产生毛刺,必须通过寄存器采样来规避,不能用在纯组合逻辑判断上。

还有一个隐藏问题:FWFT模式下,采样方很容易以为“dout上有数据”就等于“我可以随时处理”,但如果不处理好和rd_en的配对,很容易出现“数据被看到但没被消费”的假象,导致上游以为数据已经被取走,实际上FIFO里还存着这个数据。设计握手逻辑时,一定要保证valid和rd_en同时有效的那一拍,才算真正消费了一个数据。

4. Vivado里配置FIFO IP核的参数清单

4.1 创建同步FIFO IP核的完整配置过程

在Vivado中创建FIFO核的路径是:IP Catalog,搜索“FIFO Generator”,双击打开配置界面。核心配置项如下:

  1. 接口类型:选择“Native”接口,最简单直接;
  2. FIFO类型:由于这里讨论同步FIFO,选择“Synchronous FIFO”;
  3. Read Mode:在“Standard FIFO”和“First Word Fall Through”之间二选一;
  4. 数据宽度/深度:根据实际需求填写读写位宽与FIFO深度;
  5. 复位:选择同步复位或异步复位,同步FIFO两种都支持;
  6. 标志信号:按需勾选full、empty、valid、almost_full、almost_empty、data_count等输出端口;
  7. Memory Type:可选Block RAM、Distributed RAM或Auto;深度较小时用分布式RAM,深度较深时用Block RAM。

配置完成后,在IP Sources里能看到生成的wrapper文件,例化端口包括clk、srst/rst、din、wr_en、rd_en、dout、full、empty、valid等。

4.2 关键选项对两种模式的影响

配置选项Standard FIFOFWFT FIFO
Read Data Valid可作为数据采样指示应作为AXI-Stream的TVALID使用
Output Registers可增加输出寄存器改善时序,会增加延迟一般不建议额外寄存器,会影响首字直通特性
Reset Type两种模式均可两种模式均可
Memory TypeBlock/Distributed均可更依赖Block RAM/额外寄存器做首字缓存
Empty输出与读延迟无关,只表示是否为空与valid配合判断首个数据是否有效

Output Registers这个选项一定要留意。Standard模式下勾选后,dout的有效时刻会再往后挪一拍,如果读状态机没有跟着改,很可能又是错一拍的问题。FWFT模式下勾选额外的输出寄存器会打破“首字直通”的时序约定,导致第一个数据出现时间变得不直观。

4.3 不用IP核的方式:用xpm_fifo_sync轻量例化

如果你不想在工程里维护一个独立的FIFO IP核,可以用Vivado内置的XPM原语xpm_fifo_sync,效果和FIFO Generator生成的核等价,但例化方式更轻量,适合脚本化或代码复用。FWFT和Standard模式通过READ_MODE参数区分:

xpm_fifo_sync #( .FIFO_MEMORY_TYPE ("auto"), .READ_MODE ("fwft"), // 可选 "std" 或 "fwft" .FIFO_WRITE_DEPTH (16), .WRITE_DATA_WIDTH (8), .READ_DATA_WIDTH (8) ) u_fifo ( .clk (clk), .rst (rst), .din (din), .wr_en (wr_en), .rd_en (rd_en), .dout (dout), .full (full), .empty (empty), .valid (valid) );

需要注意,xpm_fifo_sync的READ_MODE指定为fwft时,内部会专门生成首字直通逻辑;指定为std时就是标准FIFO行为。具体端口和参数以所用Vivado版本自带的UG953为准,不同版本对XPM参数的支持细节略有差异。

5. 两种模式的仿真验证:代码与波形解读

5.1 测试平台整体设计思路

验证思路很简单:同一个时钟、同一组写入数据,分别例化一个Standard模式FIFO和一个FWFT模式FIFO,观察两者的dout、valid和empty行为差异。为了便于独立运行,下面用一个行为级FIFO模型模拟两种模式,综合项目里直接用FIFO Generator生成的IP核替代即可。

测试步骤分四段:先复位,再向两个FIFO写入同样的4个数据,接着同时拉高两个FIFO的rd_en连续读4拍,最后打印每一拍两个FIFO的dout和valid。这样能直观看到FWFT先有数据、Standard后出数据,以及两者valid的不同含义。

5.2 完整仿真代码

行为级模型:

`timescale 1ns/1ps module sync_fifo_model #( parameter DATA_WIDTH = 8, parameter DEPTH = 16, parameter READ_MODE = "STANDARD" )( input wire clk, input wire rst, input wire [DATA_WIDTH-1:0] din, input wire wr_en, input wire rd_en, output reg [DATA_WIDTH-1:0] dout, output wire full, output wire empty, output reg valid ); reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; integer wr_ptr = 0; integer rd_ptr = 0; integer count = 0; wire wr_ok = wr_en && !full; wire rd_ok = rd_en && !empty; assign full = (count >= DEPTH); assign empty = (count == 0); always @(posedge clk) begin if (rst) begin wr_ptr <= 0; rd_ptr <= 0; count <= 0; dout <= {DATA_WIDTH{1'b0}}; valid <= 1'b0; end else begin if (wr_ok) begin mem[wr_ptr] <= din; wr_ptr <= (wr_ptr == DEPTH-1) ? 0 : wr_ptr + 1; end if (rd_ok) begin rd_ptr <= (rd_ptr == DEPTH-1) ? 0 : rd_ptr + 1; end if (wr_ok && rd_ok) count <= count; else if (wr_ok) count <= count + 1; else if (rd_ok) count <= count - 1; if (READ_MODE == "STANDARD") begin if (rd_ok) begin dout <= mem[rd_ptr]; valid <= 1'b1; end else if (count == 0) begin dout <= {DATA_WIDTH{1'b0}}; valid <= 1'b0; end else begin valid <= 1'b0; end end else begin // FWFT 模式:dout 组合输出当前读指针数据 // 真实Xilinx IP会在此基础上增加寄存器打拍,不影响语义理解 dout <= mem[rd_ptr]; valid <= (count > 0); end end end endmodule

测试平台:

`timescale 1ns/1ps module tb_fifo_std_fwft; reg clk = 0; reg rst = 1; reg [7:0] din = 0; reg wr_en = 0; reg rd_en_std = 0; reg rd_en_fwft = 0; wire [7:0] dout_std, dout_fwft; wire full_std, empty_std, valid_std; wire full_fwft, empty_fwft, valid_fwft; always #5 clk = ~clk; sync_fifo_model #( .READ_MODE ("STANDARD") ) u_std ( .clk (clk), .rst (rst), .din (din), .wr_en (wr_en), .rd_en (rd_en_std), .dout (dout_std), .full (full_std), .empty (empty_std), .valid (valid_std) ); sync_fifo_model #( .READ_MODE ("FWFT") ) u_fwft ( .clk (clk), .rst (rst), .din (din), .wr_en (wr_en), .rd_en (rd_en_fwft), .dout (dout_fwft), .full (full_fwft), .empty (empty_fwft), .valid (valid_fwft) ); initial begin #25 rst = 0; // 连续写入 4 个数据 repeat (4) begin @(posedge clk); wr_en = 1; din = din + 8'h11; // 依次写入 0x11, 0x22, 0x33, 0x44 end @(posedge clk); wr_en = 0; // 写完后观察 FWFT 是否已把第一个数据放到 dout #1; $display("after write: fwft dout=%02x valid=%b empty=%b", dout_fwft, valid_fwft, empty_fwft); // 两个 FIFO 同时连续读 4 拍 rd_en_std = 1; rd_en_fwft = 1; repeat (4) begin @(posedge clk); #1; $display("@%0t std : dout=%02x valid=%b empty=%b | fwft: dout=%02x valid=%b empty=%b", $time, dout_std, valid_std, empty_std, dout_fwft, valid_fwft, empty_fwft); end rd_en_std = 0; rd_en_fwft = 0; #50; $finish; end initial begin $dumpfile("fifo_std_fwft.vcd"); $dumpvars(0, tb_fifo_std_fwft); end endmodule

5.3 从打印与波形能看到什么

如果直接用上面代码跑仿真,打印结果会很清楚:

  • 在写阶段结束后,FWFT的dout已经能读到0x11,valid为高,empty为低;Standard的dout则还是0,valid为低。
  • 开始同时读之后,同一拍的打印结果中,Standard的dout比FWFT落后一个数据:FWFT显示0x22时,Standard刚显示0x11,FWFT显示0x33时,Standard显示0x22。

这正好对应两种模式的核心差异:FWFT模式下数据始终“待命”在dout上,rd_en只是消费;Standard模式下必须等rd_en之后的一拍,数据才从请求变成实物。

仿真模型里FWFT的dout用了组合逻辑模拟直通,真实Xilinx IP会通过内部寄存器把输出打一拍,但接口上的语义是一致的。如果你在Vivado里用FIFO Generator生成IP后跑仿真,重点观察valid和rd_en的配合位置,以及empty到dout有效之间的周期数,会发现同样的规律。

6. 工程落地经验:复位、握手、反压与资源边界

6.1 复位释放后别急着写读

Xilinx的FIFO IP核在复位释放后,内部读写指针和状态信号需要一定时间进入稳定状态。常见做法是系统上电复位结束后,给FIFO留出至少2-4个时钟周期的“静默期”,再进行写操作或读操作。如果复位释放后立即写第一个数据,有些配置下可能丢失首字。

这个现象在FWFT模式里尤其明显,因为复位释放后valid、empty、dout需要协同建立“直通状态”,过早写入容易让内部首字缓存逻辑处于不确定状态。在实际工程中,我习惯用一个计数器在复位释放后数几个周期,再使能外部逻辑访问FIFO。

6.2 读侧握手:valid与rd_en如何配合

两种模式下的握手写法差别很大。

Standard模式下,推荐的状态机流程是:查empty,非空则拉rd_en;下一个周期检查valid,valid为高时采样dout。valid在这个模式里是“读操作完成”的指示。

FWFT模式下,更推荐的写法是:直接等valid拉高,valid为高时把dout上的数据消费掉,同时拉一个周期的rd_en。valid在这里是“数据可消费”的指示。

可以把FWFT直接映射成AXI4-Stream握手:

  • tvalid = valid;
  • tready = rd_en;
  • tdata = dout。

握手机制就是经典的TVALID/TREADY:两个信号同时为高表示数据传输完成一拍。这个模式下,不需要额外状态机去猜测数据什么时候出现,数据流驱动逻辑更清晰。

6.3 almost_full与almost_empty:反压与阈值判断

full和empty两个信号通常作为边界指示,但在实际流水线设计中,更常用almost_full和almost_empty做反压阈值。比如下游处理速度跟不上时,上游看到almost_full拉高就要提前停止写入,而不是等full拉高才停。因为full是组合输出或寄存器输出,存在一定的传播延迟,等full真正置位再停,可能已经多写了一个数据。

almost_empty同理,适合用来提前预判FIFO即将读空,让下游状态机减少无效等待。FWFT模式下,almost_empty配合valid使用,可以在最后一个数据被消费之前就给出预警,帮助前置逻辑准备状态切换。

6.4 资源与性能边界

FWFT模式并不是免费的。为了让第一个数据提前出现在dout上,FIFO内部需要额外的首字保持逻辑,通常会多消耗几十个FF/LUT,BRAM资源两者基本一致。如果FIFO位宽很宽、深度很深,FWFT模式增加的数据路径逻辑会更明显。

在高速设计里,如果读侧逻辑本来就紧张,优先考虑Standard模式,因为它对数据路径时序更友好。FWFT模式适合读侧有天然流水线结构、能接受一定组合路径开销的场景。

我在自己做的网络包处理设计里,曾经统一用FWFT,后来其中一路高速数据通道时序收敛困难,把FIFO改成Standard模式、读状态机多等一拍之后,时序马上就解了。可见模式选择没有绝对的对错,完全看接口和系统约束。选型前先画清楚读写两侧的握手时序图,再决定用哪种模式,能省很多不必要的调试时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 1:31:43

编译原理课设报告:词法分析状态图与递归下降语法分析详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:31:39

NPI作业管理规范:从试产阶段到门禁落地的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:31:39

Windows虚拟键值表(VK)原理与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:31:09

Python+Carla+Apollo联合仿真从环境搭建到闭环控制全实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:31:07

IEC 62351-100-3一致性测试手记:从NSM/RBAC到备测全攻略

想写IEC 62351-100-3一致性测试手记&#xff0c;起因是近期被同一个问题反复轰炸&#xff1a;“100-3到底考什么&#xff1f;”问的人里有做变电站监控的、有做远动装置的、有做安全网关的&#xff0c;还有几个是第三方检测机构的同行。大家的心态基本一致&#xff1a;标准文件…

作者头像 李华
网站建设 2026/10/2 1:30:59

汽车销售后台管理系统实战:Spring Boot+Vue前后端分离开发全流程解析

最近帮朋友收尾了一个汽车销售后台管理系统&#xff0c;从需求梳理、数据库设计到前后端联调、部署上线&#xff0c;前前后后折腾了一个多月。项目用的是 Spring Boot Vue 这套前后端分离的组合&#xff0c;整体跑下来很稳&#xff0c;也踩了不少文档里找不到的坑。这篇文章就…

作者头像 李华