FPGA跨平台设计:通用FWFT FIFO接口适配器设计与验证
1. 为什么需要FWFT FIFO接口适配器第一次接触FPGA设计时我就被各种FIFO接口搞得晕头转向。特别是当项目需要在不同厂商的FPGA平台间移植时FIFO接口的差异简直让人抓狂。记得有次把Xilinx平台的设计移植到国产安路FPGA上原本正常工作的数据采集模块突然就罢工了调试了整整两天才发现问题出在FIFO的读端口行为不一致上。标准FIFO和FWFT FIFO最大的区别在于数据有效时机。标准FIFO在empty信号拉低时输出的数据还不是有效数据需要再给一个rd_en信号才能获取有效数据。而FWFTFirst Word Fall ThroughFIFO则贴心得多empty信号拉低的同时数据就已经有效了rd_en信号只是告诉FIFO准备下一个数据。这种零延迟的特性让FWFT FIFO成为很多设计中的首选。但在实际项目中我们经常会遇到平台限制。比如Xilinx Vivado里的FIFO Generator可以灵活选择FIFO类型而Intel Quartus中的FIFO IP核他们称之为Normal FIFO和Show-ahead FIFO配置选项就有所不同。更麻烦的是国产安路TD工具目前只提供标准FIFO这一种选项。这种平台差异导致我们精心设计的FWFT接口模块在其他平台上无法直接使用。2. FWFT FIFO适配器设计原理2.1 核心设计思路要让标准FIFO模拟FWFT行为关键在于解决数据有效时机的问题。经过多次尝试我发现可以通过一个巧妙的状态机来实现这个转换。这个适配器的核心任务就是在标准FIFO的数据真正有效前提前将empty信号置为有效状态。具体来说当检测到标准FIFO的empty信号变低时说明FIFO中有数据了。这时适配器会立即发出一个rd_en信号给标准FIFO同时将转换后的fwft_empty信号拉低。这样在外界看来empty变低的同时数据就已经有效了完美模拟了FWFT FIFO的行为。这里有个关键点需要注意标准FIFO的读延迟参数READ_LATENCY。这个参数表示rd_en有效后需要多少个时钟周期数据才会出现在输出端口。Xilinx的标准FIFO通常是延迟1个周期而有些FIFO IP核可能配置为2个或更多周期延迟。我们的适配器需要根据这个参数调整状态机的行为。2.2 状态机设计我设计的状态机主要包含以下几个状态IDLE初始状态等待FIFO非空PRE_READ发出预读信号准备获取数据DATA_VALID数据有效状态WAIT_CYCLE处理读延迟大于1的情况状态转换的逻辑是这样的当标准FIFO的empty信号变低时从IDLE进入PRE_READ状态立即给standard_fifo_rd_en一个脉冲。根据READ_LATENCY参数可能需要等待1个或多个时钟周期后才进入DATA_VALID状态这时fwft_empty才会拉低表示数据有效。当外部给出fwft_fifo_rd_en信号时状态机需要根据当前是否还有数据决定下一步动作。如果FIFO中还有数据就立即再发一个rd_en获取下一个数据如果是最后一个数据则回到IDLE状态等待新的数据到来。3. Verilog实现详解3.1 模块接口定义让我们先看看这个适配器的完整接口定义module standardFIFO2FWFTFIFO #( parameter STANDARD_FIFO_READ_LATENCY 1, parameter STANDARD_FIFO_DOUT_WIDTH 8 )( // FWFT接口 output wire [STANDARD_FIFO_DOUT_WIDTH-1:0] fwft_fifo_dout, output reg fwft_fifo_empty, input wire fwft_fifo_rd_en, // 标准FIFO接口 input wire [STANDARD_FIFO_DOUT_WIDTH-1:0] standard_fifo_dout, input wire standard_fifo_empty, output wire standard_fifo_rd_en, // 时钟和复位 input wire clk, input wire srst );接口设计有几个需要注意的地方参数化设计READ_LATENCY和DATA_WIDTH都是可配置参数提高模块的复用性明确的信号方向FWFT接口和标准FIFO接口分开定义避免混淆复位信号采用高电平有效这是为了与Xilinx FIFO IP核的默认行为保持一致3.2 关键逻辑实现核心的状态机实现如下// 状态定义 localparam IDLE 2b00; localparam PRE_READ 2b01; localparam DATA_VALID 2b10; localparam WAIT_CYCLE 2b11; reg [1:0] state, next_state; reg [STANDARD_FIFO_DOUT_WIDTH-1:0] data_reg; reg pre_read_done; // 状态转移逻辑 always (posedge clk) begin if (srst) begin state IDLE; fwft_fifo_empty 1b1; pre_read_done 1b0; end else begin state next_state; case (next_state) IDLE: begin fwft_fifo_empty 1b1; pre_read_done 1b0; end PRE_READ: begin if (STANDARD_FIFO_READ_LATENCY 0) begin fwft_fifo_empty 1b0; end end DATA_VALID: begin fwft_fifo_empty 1b0; data_reg standard_fifo_dout; end WAIT_CYCLE: begin // 等待读延迟周期结束 end endcase end end // 下一状态逻辑 always (*) begin case (state) IDLE: next_state standard_fifo_empty ? IDLE : PRE_READ; PRE_READ: begin if (STANDARD_FIFO_READ_LATENCY 1) begin next_state DATA_VALID; end else begin next_state WAIT_CYCLE; end end DATA_VALID: begin if (fwft_fifo_rd_en) begin next_state standard_fifo_empty ? IDLE : PRE_READ; end else begin next_state DATA_VALID; end end WAIT_CYCLE: begin if (pre_read_done) begin next_state DATA_VALID; end else begin next_state WAIT_CYCLE; end end endcase end // 标准FIFO读使能生成 assign standard_fifo_rd_en (state PRE_READ) || (state DATA_VALID fwft_fifo_rd_en !standard_fifo_empty); // 数据输出选择 assign fwft_fifo_dout (state DATA_VALID) ? data_reg : standard_fifo_dout;这段代码有几个关键点使用经典的三段式状态机设计状态定义、状态转移、输出逻辑根据READ_LATENCY参数调整状态转移路径在DATA_VALID状态锁存输出数据确保数据稳定性读使能信号生成考虑了连续读取的情况4. 功能验证与测试4.1 测试平台搭建为了全面验证适配器的功能我设计了一个包含以下测试场景的testbench单次写入单个数据立即读取单次写入单个数据延迟读取连续写入多个数据连续读取混合读写操作测试平台同时例化了一个真正的FWFT FIFO作为参考用来对比我们的适配器输出是否与真实FWFT FIFO行为一致。module standardFIFO2FWFTFIFO_tb(); timeunit 1ns; timeprecision 10ps; localparam STANDARD_FIFO_READ_LATENCY 1; localparam STANDARD_FIFO_DOUT_WIDTH 8; // 测试信号声明 logic [7:0] din; logic wr_en; logic full; logic fwft_fifo_empty; logic fwft_fifo_rd_en; logic true_fwft_fifo_empty; logic true_fwft_fifo_rd_en; logic clk; logic rstn; // 实例化被测设计 standardFIFO2FWFTFIFO #( .STANDARD_FIFO_READ_LATENCY(STANDARD_FIFO_READ_LATENCY), .STANDARD_FIFO_DOUT_WIDTH(STANDARD_FIFO_DOUT_WIDTH) ) uut (.*); // 时钟生成 localparam CLKT 2; initial begin clk 0; forever #(CLKT / 2) clk ~clk; end // 测试序列 initial begin // 初始化 rstn 0; wr_en 1b0; #(CLKT * 10) rstn 1; // 测试场景1单次写入单个数据立即读取 wr_en 1b1; #CLKT wr_en 1b0; #(CLKT*5); // 测试场景2单次写入单个数据延迟读取 wr_en 1b1; #CLKT wr_en 1b0; #(CLKT*5); // 测试场景3连续写入多个数据 wr_en 1b1; #(CLKT*3) wr_en 1b0; #(CLKT*1); // 测试场景4混合读写 wr_en 1b1; #CLKT wr_en 1b0; #(CLKT*30) $stop; end endmodule4.2 测试结果分析当READ_LATENCY1时适配器的行为与真实FWFT FIFO完全一致。empty信号拉低的同时数据就已经有效rd_en信号只是触发下一个数据的准备。这种模式下适配器可以完美模拟FWFT FIFO的所有特性。但当READ_LATENCY≥2时情况就有些不同了。由于标准FIFO需要多个周期才能输出有效数据适配器无法保持数据连续有效。这时empty信号会在数据间隔期间拉高虽然仍然符合FWFT的基本行为但无法实现真正的零间隔连续读取。通过波形对比可以清楚地看到这些行为差异。在实际应用中建议尽量使用READ_LATENCY1的标准FIFO配合这个适配器这样可以获得最佳的FWFT模拟效果。5. 跨平台应用实践5.1 Xilinx平台应用在Vivado环境中使用这个适配器非常简单。首先按照常规方法例化一个标准FIFO IP核确保将读延迟配置为1。然后将标准FIFO的读端口连接到我们的适配器适配器的输出就可以当作FWFT FIFO使用了。需要注意的是Vivado自己的FIFO Generator其实提供了FWFT选项所以这个适配器主要用在需要与第三方IP或现有设计兼容的场景。5.2 Intel Quartus平台应用Quartus中的FIFO IP核称为SCFIFO或DCFIFO配置略有不同。在参数设置中需要选择Show-ahead模式相当于FWFT FIFO。但如果某些情况下只能使用标准模式我们的适配器就能派上用场了。我在Cyclone 10 LP开发板上测试过这个方案工作非常稳定。由于Altera的FIFO通常也是1个周期的读延迟所以适配效果很好。5.3 国产安路TD平台应用目前安路的TD开发工具只提供标准FIFO这正是我最初开发这个适配器的原因。在EG4系列FPGA上的实测表明适配器能够很好地工作解决了FWFT接口缺失的问题。使用时有几个注意事项确保FIFO配置的读延迟为1时钟域要一致适配器不能用于异步FIFO复位信号要同步释放避免亚稳态6. 性能优化与扩展6.1 时序优化技巧在大数据量、高速应用场景下这个适配器可能会成为时序瓶颈。通过以下几种方法可以优化性能流水线设计将状态机的判断逻辑拆分成多个周期提高时钟频率输出寄存器在fwft_fifo_dout后添加一级寄存器改善输出时序提前预判根据写指针位置提前预判empty信号变化// 添加输出寄存器的改进版本 always (posedge clk) begin if (srst) begin fwft_fifo_dout_reg 0; end else begin if (state DATA_VALID) begin fwft_fifo_dout_reg data_reg; end end end assign fwft_fifo_dout (state DATA_VALID) ? data_reg : fwft_fifo_dout_reg;6.2 扩展功能基础版本已经能满足大多数需求但还可以进一步扩展添加almost_empty/threshold信号支持支持背压机制ready/valid握手增加性能监控计数器支持AXI-Stream接口转换这些扩展可以根据具体项目需求选择性实现。比如添加almost_empty支持只需要增加一个比较器和阈值参数parameter ALMOST_EMPTY_THRESH 2; output wire fwft_fifo_almost_empty; assign fwft_fifo_almost_empty (fifo_count ALMOST_EMPTY_THRESH);在实际项目中这个FWFT FIFO适配器已经帮我节省了大量移植和调试时间。特别是在多平台支持的项目中只需要针对不同平台调整FIFO IP核的配置而所有业务逻辑代码都能保持完全一致。这种设计模式也适用于其他需要统一接口的场景比如不同厂商的DDR控制器、PCIe IP核等。