1. FIFO含义FIFO (First In First Out)是一种常见的数据缓存结构即先进先出队列含义是先写入的数据会先被读出数据的输出顺序与输入顺序保持一致。在Verilog数字电路设计中FIFO常用于在两个模块之间进行数据缓冲和速率匹配。当数据生产模块和数据消费模块的工作速度不同或者两者不能在同一时刻完成数据交互时可以通过FIFO暂存数据避免数据丢失。例如在图像视频数据缓存、ADC采样数据暂存、处理器与加速器之间的数据交换、跨时钟域数据传输以及流水线模块之间的速率匹配等场景FIFO都是非常常用的结构。简单来说FIFO在Verilog设计中的核心作用是在前后级模块之间暂存数据使数据的写入和读取可以在时间上解耦并按照原始顺序传递给后级模块从而缓冲短期速率波动、适配突发传输并提高系统的数据传输稳定性和模块连接灵活性。FIFO的分类方式很多例如按时钟关系可以分为同步FIFO和异步FIFO按读写位宽关系可以分为同宽FIFO和位宽转换FIFO。本文聚焦于FIFO的接口形式讨论普通FIFO和FWFT (First-Word Fall-Through) FIFO的差异并给出普通FIFO转换成FWFT FIFO的方式。2. 普通FIFO一般而言在Vivado中例化的FIFO IP核都是普通的接口时序无法提供前后级握手能力。下面我们例化一个深度16位宽为8的FIFO仿真并观察它的时序特点。FIFO IP的信息汇总如下注意虽然Handshaking这一项是Selected但其实这不是严格意义上的握手机制只是提供了输出有效信号valid而已。测试激励文件fifo_tb.vmodule fifo_tb; parameter clk_period 10; reg clk; reg rst_n; wire [7:0]fifo_din; wire fifo_wren; wire fifo_rden; wire [7:0]fifo_dout; wire fifo_full; wire fifo_empty; wire fifo_outvalid; fifo fifo_inst ( .clk(clk), // input wire clk .rst(~rst_n), // input wire rst .din(fifo_din), // input wire [7 : 0] din .wr_en(fifo_wren), // input wire wr_en .rd_en(fifo_rden), // input wire rd_en .dout(fifo_dout), // output wire [7 : 0] dout .full(fifo_full), // output wire full .empty(fifo_empty), // output wire empty .valid(fifo_outvalid) // output wire valid ); initial clk 1b1; always #(clk_period/2) clk ~clk; reg [7:0]din; reg wr_en; reg rd_en; assign fifo_din din; assign fifo_wren wr_en (~fifo_full); assign fifo_rden rd_en (~fifo_empty); integer i; initial begin rst_n 1b0; din 8d0; wr_en 1b0; rd_en 1b0; #100; rst_n 1b1; #100.2; for(i0;i18;ii1)begin wr_en 1b1; din i1; #10; end wr_en 1b0; #20; for(i0;i18;ii1)begin rd_en 1b1; #10; end rd_en 1b0; #20; $stop; end endmodule在实际FIFO使用中为了避免在FIFO已满时继续写入或在FIFO为空时继续读取通常需要对外部读写请求进行保护。对应的代码为assign fifo_wren wr_en (~fifo_full); assign fifo_rden rd_en (~fifo_empty);wr_en和rd_en相当于外部的读写请求而真正连接到FIFO IP的读写使能是fifo_wren和fifo_rden。下面我们来看仿真波形在仿真中写使能fifo_wren保证FIFO非满才写读使能fifo_rden保证FIFO非空才读。因此在FIFO中空满信号empty和full是必须也很关键的信号下面给出两个信号变化的关键条件。empty0 - 1剩1个数据fifo_wren0且fifo_rden1empty1 - 0无数据fifo_wren1full0 - 1剩1个空位fifo_wren1且fifo_rden0full1 - 0满数据fifo_rden1对于FIFO写入端而言可以将外部写请求信号wr_en理解为输入有效信号in_valid(输入)将~full理解为输入准备信号in_ready(输出)。只有当wr_en和~full同时为高时才表示本次数据被FIFO成功写入对应握手接口中的in_valid in_ready。因此FIFO写入端可直接作为握手接口使用。但是对于FIFO读出端而言情况就完全不同了。信号~empty可作为读请求准备信号rd_req_ready(输出)但是信号rd_en却不能作为读请求有效信号rd_req_valid(输入)。原因是当~empty和rd_en同时为高时只能表示一次读请求被接受而不能在同一时刻去用fifo_dout的数据延迟一个周期才出现。3. FWFT FIFO基于第2节的分析普通标准FIFO的输出数据通常会相对于读请求延迟一个时钟周期因此其读端只能表示“读请求被接受”不能直接表示“输出数据已被下游消费”。在很多场景下设计者需要额外添加状态机、计数器或数据有效控制逻辑来管理读延迟从而增加了外部控制复杂度。FWFT FIFO的全称是First-Word Fall-Through FIFO可以理解为FIFO中只要有数据第一个数据就会自动“透传”到输出端不需要先给读使能再等一个周期。因此当empty为低时输出数据dout已经是有效数据读使能不再表示“请求下一拍读出数据”而是表示“当前输出数据被消费”。FWFT的时序图如下所示数据刚被写入就已经透传到输出端rden为高时就可以直接消费当前数据。下面我们来介绍如何将普通FIFO改造成FWFT FIFO。3.1 将普通FIFO改造为FWFT FIFO下文描述的信号除非特殊说明是普通FIFO否则大家一定要站在FWFT FIFO的角度来思考时序尤其是rden和dout是同周期一起消费的。下文的分析比较长大家做好心理准备(๑•̀ㅂ•́)و✧。普通FIFO无法握手的根本原因是读出端数据和读请求无法同拍所以改造思路就是将FIFO中的数据提前放到数据输出端口上。注意下文改造的普通FIFO有两个特点默认读延迟为 1 拍多拍读延迟 FIFO 也可类似改造但控制逻辑需相应调整。当没有新的读使能时输出端口dout通常会保持上一次读出的数据。在数字电路中寄存器可以在时钟沿寄存数据并在随后将该数据稳定保持在输出端。因此需要为普通FIFO添加一个位宽相同的寄存器来辅助存储数据。为此我们要考虑什么情况数据进入寄存器什么情况数据进入普通FIFO。当FWFT FIFO为空时第一个数据只能写入寄存器中使其在当前拍寄存后即可作为有效输出如果先写入普通FIFO中则需要下一拍才能发起读请求再下一拍数据才会出现在输出端。为了表示寄存器中是否有数据要有一个寄存器数据有效信号fifo_buf_valid。当FWFT FIFO有1个数据且在寄存器中下一拍再往里写数据并不一定写入普通FIFO中。如果FWFT FIFO的wren为高而rden为低那么此拍数据写入普通FIFO中如果wren为高且rden也为高则此拍数据写入寄存器中。可以想象当wren拉高连续写且rden延迟一个周期拉高连续读所有的数据都是写入寄存器的这里没有普通FIFO啥事。当向FWFT FIFO连续写入n个数据且暂时不读肯定第一个数据写入寄存器后面数据写入普通FIFO中。现在rden拉高开始读数据rden拉高的第1个周期由于第1个数据在寄存器中我们在寄存器输出端使用即可rden拉高的第2个周期需要FIFO里面的数据了发现将此时的rden用来读普通FIFO行不通普通FIFO下一拍数据才能读出来而FWFT FIFO当前拍就要消费数据。所以我们需要利用rden在第1个周期的高电平来作为普通FIFO的读使能那rden在第2个周期时数据就出现在普通FIFO的输出端口了。同理rden在第m个高电平时消费第m个数据同时从普通FIFO中读出第m1个数据。补充讨论一。普通FIFO的读使能需要前一次的FWFT FIFO的rden来提前驱动的。问题在于是否所有的rden都应该来驱动普通FIFO的读使能从严谨的角度来看并不是比如上面讨论过的所有数据都写入寄存器普通FIFO根本不参与这种情况理论上普通FIFO的读使能保持低电平即可。理论上什么时候需要让FWFT FIFO的rden来提前读出普通FIFO中的数据取决于你下一个数据从寄存器还是普通FIFO中来。实际上当前拍rden为高电平且下一个数据应该从寄存器中取此刻普通FIFO肯定为空rden来提前读这个空的普通FIFO也无所谓。补充讨论二。假如我说普通FIFO里还有2个数据有效可用是否表示普通FIFO里面还有两个数据答案是否定的。在FWFT FIFO内部普通FIFO总是提前备好数据的即提前将数据放到自己的输出端口上。FWFT FIFO想要使用时拉高rden同时直接取走数据。普通FIFO看到rden为高说明数据没了立马吭哧吭哧准备下一个数据。所以我说普通FIFO有2个数据有效可用本质上是1个数据在普通FIFO内部还有一个数据在输出端口上。当FWFT FIFO剩1个数据且在普通FIFO中下一拍应该往哪儿写只有一个数据且在普通FIFO中那只能是在普通FIFO的输出端备着说明普通FIFO内部已经空了。分析可知若FWFT FIFO的wren为高而rden为低那么此拍数据写入普通FIFO中若wren为高且rden也为高则此拍数据必须写入寄存器中。解释一下如果就剩1个数据在普通FIFO的输出端此时wren和rden都为高。rden为高将剩的一个数据用掉了此刻又要准备下一个数据那数据是不是只能写入寄存器中因为写入寄存器中的数据能立刻出现在寄存器输出端。文字分析到此为止下面直接给出改造的控制代码随后再给出代码的分析。普通FIFO转换成FWFT FIFO的代码如下timescale 1ns / 1ps module fwft_fifo( input clk, input rst_n, input wren, input [7:0]din, output full, input rden, output [7:0]dout, output empty ); //raw_fifo port wire [7:0]raw_fifo_din; wire raw_fifo_wren; wire raw_fifo_rden; wire [7:0]raw_fifo_dout; wire raw_fifo_full; wire raw_fifo_empty; //control logic reg [4:0]fifo_cnt; reg [7:0]fifo_buf; reg fifo_buf_valid; wire fifo_buf_wren; wire wr_fire; wire rd_fire; //assign assign full (fifo_cnt5b10000); assign empty (fifo_cnt5b00000); assign dout fifo_buf_valid ? fifo_buf : raw_fifo_dout; assign wr_fire wren ~full; assign rd_fire rden ~empty; assign fifo_buf_wren wr_fire (empty | (rd_fire fifo_cnt5d1)); assign raw_fifo_wren wr_fire ~(empty | (rd_fire fifo_cnt5d1)); assign raw_fifo_din raw_fifo_wren ? din : 8d0; assign raw_fifo_rden rd_fire (~fifo_buf_wren | ~fifo_buf_valid); //fifo_cnt always(posedge clk or negedge rst_n) if(!rst_n) fifo_cnt 5d0; else case({wr_fire,rd_fire}) 2b10: fifo_cnt fifo_cnt 5d1; 2b01: fifo_cnt fifo_cnt - 5d1; default: fifo_cnt fifo_cnt; endcase //fifo_buf always(posedge clk or negedge rst_n) if(!rst_n) fifo_buf 8d0; else if(fifo_buf_wren) fifo_buf din; //fifo_buf_valid always(posedge clk or negedge rst_n) if(!rst_n) fifo_buf_valid 1b0; else if(fifo_buf_wren) fifo_buf_valid 1b1; else if(fifo_buf_valid rd_fire) fifo_buf_valid 1b0; //Module Instance// fifo raw_fifo( .clk(clk), // input wire clk .rst(~rst_n), // input wire rst .din(raw_fifo_din), // input wire [7 : 0] din .wr_en(raw_fifo_wren (~raw_fifo_full)), // input wire wr_en .rd_en(raw_fifo_rden (~raw_fifo_empty)), // input wire rd_en .dout(raw_fifo_dout), // output wire [7 : 0] dout .full(raw_fifo_full), // output wire full .empty(raw_fifo_empty), // output wire empty .valid() // output wire valid ); endmodule我们改造的对象是位宽为8深度为16的普通FIFOraw_fifo是由Vivado生成的IP核。raw_fifo例化时读写端代码如下为了避免向已满的FIFO继续写入或向已空的FIFO继续读出。.wr_en(raw_fifo_wren (~raw_fifo_full)), // input wire wr_en .rd_en(raw_fifo_rden (~raw_fifo_empty)), // input wire rd_en代码中额外添加了一些信号以便进行辅助控制。fifo_cnt用于统计FWFT FIFO中已有的数据个数位宽为5bit。fifo_buf作为辅助存储的寄存器位宽和普通FIFO相同。fifo_buf_valid是数据有效信号表示寄存器fifo_buf中的数据有效可以被外部读取使用。fifo_buf_wren是写寄存器fifo_buf的使能信号为高时表示将外部数据写入寄存器中。wr_fire和rd_fire分别表示当前周期实际执行写入和读出的控制信号。下面对一些关键信号的生成逻辑进行阐述。always(posedge clk or negedge rst_n) if(!rst_n) fifo_cnt 5d0; else case({wr_fire,rd_fire}) 2b10: fifo_cnt fifo_cnt 5d1; 2b01: fifo_cnt fifo_cnt - 5d1; default: fifo_cnt fifo_cnt; endcasefifo_cnt的生成是比较重要的因为其驱动了full、empty、fifo_buf_wren和raw_fifo_wren等信号。生成逻辑比较简单根据实际执行写入和读出的控制信号wr_fire和rd_fire来进行相应的加减操作。assign fifo_buf_wren wr_fire (empty | (rd_fire fifo_cnt5d1)); assign raw_fifo_wren wr_fire ~(empty | (rd_fire fifo_cnt5d1)); assign raw_fifo_rden rd_fire;fifo_buf_wren信号的产生很关键表示向寄存器fifo_buf中写入的使能信号。前文我们分析了FWFT FIFO的三种情况空有1个数据且在寄存器中剩1个数据且在普通FIFO。第一种情况数据肯定要写入寄存器中后面两种情况rd_fire为高时也必须写入寄存器中。raw_fifo_wren的生成逻辑是数据如果不是写入寄存器中那肯定写入普通FIFO中。raw_fifo_rden的生成上文也讨论过即便下一个数据来自寄存器中读一个空的普通FIFO也无所谓况且.rd_en(raw_fifo_rden (~raw_fifo_empty))还避免这一点发生。always(posedge clk or negedge rst_n) if(!rst_n) fifo_buf_valid 1b0; else if(fifo_buf_wren) fifo_buf_valid 1b1; else if(fifo_buf_valid rd_fire) fifo_buf_valid 1b0;fifo_buf_valid表示fifo_buf中数据有效。当向寄存器中写入数据时拉高该信号当不向寄存器写数据寄存器中数据又有效且数据需要读出时拉低该信号。有人可能想问为什么这里用rd_fire就够了因为如果寄存器中数据有效的话下一次的读出的数据肯定来自寄存器而不是普通FIFO。这也解释了assign dout fifo_buf_valid ? fifo_buf : raw_fifo_dout;的原因。3.2 改造代价将普通FIFO改造成FWFT FIFO接口本质上是在原始FIFO外围增加一层很轻量的控制逻辑。该逻辑主要包括一个输出缓存寄存器fifo_buf、一个缓存有效标志fifo_buf_valid、一个数据计数器fifo_cnt以及少量读写控制组合逻辑。从资源消耗来看改造代价很小。以当前8bit数据宽度、16深度FIFO为例额外增加的寄存器资源主要是fifo_buf : 8个FF,与普通FIFO位宽相同 fifo_buf_valid : 1个FF fifo_cnt : 5个FF,与普通FIFO深度相关组合逻辑方面仅增加了少量LUT用于实现FIFO状态判断、读写控制、缓存与FIFO存储体之间的写入选择以及输出数据选择等控制逻辑。下图也在Vivado上跑了一个实现后的结果作为参考。原始普通FIFO用了36个LUT和33个FF改造后的FWFT FIFO用了52个LUT和46个FF共多了16个LUT和13个FF。因此普通FIFO改造成FWFT FIFO的面积代价很小但可以换来更方便的可握手接口。总体来看这是一个低资源开销、高接口便利性的改造。3.3 AI辅助虽然FWFT FIFO的外围控制逻辑最终看起来只有寥寥几行但是要一次性把所有边界情况都考虑完整并不是一件很容易的事情。尤其是FIFO这类时序接口很多问题并不会出现在普通的“写一串、读一串”测试中而是隐藏在一些非常细小的状态切换里例如空到非空、满到非满、只剩一个数据、同时读写等场景。在本设计过程中我借助AI对FWFT FIFO的控制逻辑进行了辅助分析和仿真验证。AI不只是简单地检查语法而是围绕FIFO的真实使用场景构建了较全面的测试用例包括先写满 FIFO再连续读空只写入一个数据再读出写入一串数据后再连续读出连续写入后间隔一个周期开始读取空 FIFO 状态下同时读写满 FIFO 状态下同时读写FIFO 只剩一个数据时同时读写不同 FIFO 深度下的读、写、同时读写组合长时间随机读写压力测试这些测试之间让FIFO在连续运行状态下经历不同场景切换这样更接近真实硬件使用情况也更容易暴露状态残留或边界切换问题。在验证过程中当FWFT FIFO剩1个数据且在普通FIFO中这种情况得以补充漏洞得以修复。因此AI在本设计中主要起到了两个作用一是辅助分析控制逻辑中容易遗漏的边界条件二是帮助构建更全面、更有针对性的仿真用例。虽然最终代码并不复杂但通过AI辅助验证可以显著降低遗漏边界问题的风险提高设计的可靠性。所以大家可以直接参考上面的代码来改造其他位宽和深度的普通FIFO。4. 总结本文重点介绍了如何将普通FIFO改造成FWFT FIFOFWFT FIFO的端口尤其适合用于模块之间的握手。在很多场景下FWFT FIFO的控制更简单数据传输更优美自然。希望本文能够给大家带来一些帮助有问题欢迎评论区讨论。