高云FPGA异步FIFO IP核配置与Verilog驱动实战
最近在做一个FPGA项目,需要把摄像头采集的数据传给另一个时钟域的图像处理模块,中间的数据缓冲和跨时钟域传输让我头疼了一阵。后来我用了高云FPGA自带的异步FIFO IP核,问题迎刃而解。今天我就把这个从IP核配置到Verilog驱动、再到仿真验证的完整流程分享给大家,手把手教你搞定异步FIFO。
1. 异步FIFO是什么?为什么需要它?
1.1 FIFO基础:数据流的“排队缓冲区”
咱们先打个比方。FIFO就像银行排队叫号系统,先来的人先办业务(先进先出)。在FPGA里,FIFO就是一个数据缓冲区,用来临时存放数据流,解决数据生产者和消费者速度不匹配的问题。
可能有人会问,这和我学单片机时用的堆栈(Stack)有啥区别?我刚开始也混淆过,这里简单说下:
- FIFO:就像排队,队尾进,队头出。适合处理连续的数据流,比如视频流、网络数据包。在FPGA里,它常用于数据缓冲、流量控制。
- 堆栈:就像叠盘子,你只能从最上面放和取(后进先出)。单片机里主要用来管理函数调用、保存临时变量。
简单说,FIFO管“流水线”,堆栈管“函数调用”。咱们今天的主角是前者。
一个FIFO有几个关键参数你得知道:
- 深度:能存多少个数据。好比排队大厅有多少个座位。
- 宽度:每个数据有多少位(比如8位、16位)。好比每个人手里拿的号牌大小。
- 空/满标志:告诉你缓冲区是空的(没数据可读)还是满的(没空间可写),防止读错或写爆。
- 读写指针:内部用来记录读到哪、写到哪的“记号笔”。
1.2 同步FIFO vs 异步FIFO
FIFO按时钟分两种,这是核心区别:
同步FIFO:读写都用同一个时钟。好比整个银行只有一个挂钟,所有人按同一个时间节奏办事。控制简单,但只能用在同一个时钟域。
异步FIFO:读和写用两个独立的时钟。好比取号机和柜台有各自的钟,速度可能不一样。它就是为了解决跨时钟域数据传递而生的,内部有专门的电路(比如双端口RAM、格雷码计数器)来安全地同步读写指针,避免数据出错。
什么时候用异步FIFO?当你FPGA设计里有两个模块,一个在50MHz时钟下产生数据,另一个在75MHz时钟下处理数据,中间就得用异步FIFO来做缓冲和时钟域隔离。图像处理、音频编解码、高速接口(如DDR、PCIe)的数据缓存都离不开它。
2. 高云云源软件里的FIFO IP核
高云FPGA的开发软件(Gowin云源)提供了现成的FIFO IP核,咱们不用从零写,直接配置调用就行,省时省力还可靠。
打开软件,在IP核生成器里你能找到几种FIFO:
- FIFO:就是咱们要用的异步FIFO(双时钟)。
- FIFO SC:同步FIFO(单时钟)。
- FIFO HS / FIFO SC HS:支持更高时钟频率的版本。
双击“FIFO”,就进入了配置界面。别被一堆选项吓到,我带你一个个看关键的。
2.1 核心参数配置
首先给IP核起个名,别用默认的fifo_top,因为顶层模块通常叫top。我这里改成fifo_ip。
1. Options(基本选项)
- Write Depth/Width:写深度和写数据位宽。比如我设深度128,宽度8位(就是能存128个8位数据)。
- Read Depth/Width:读深度和读数据位宽。注意:异步FIFO支持读写位宽不同!比如你写是8位,读可以设成16位,IP核会自动计算。公式是:
Read Data Width = (Write Depth * Write Data Width) / Read Depth。位宽和深度别设太大,会浪费FPGA里宝贵的Block RAM资源。 - Output Register Select:勾上后,输出数据会延迟一个时钟周期,能改善时序,一般建议勾选。
- Controlled by RdEn:输出数据是否受读使能控制。勾选后,只有
RdEn有效时数据才出现在输出端口。
2. FIFO Implementation(实现方式)就是用FPGA里的哪种存储资源:
- Block SRAM (BSRAM):块RAM,容量大,适合深度大于1KB的情况。
- Shadow SRAM (SSRAM):分布式RAM,灵活,适合小容量FIFO(小于1KB)。
- REG:用寄存器和查找表搭,极浅的FIFO才用。 通常按建议来,小的用SSRAM,大的用BSRAM。
3. Read Mode(读模式)
- Standard FIFO:标准模式。需要读使能有效,才会把下一个数据输出。
- First-Word Fall-Through (FWFT):首字直通模式。第一个写入的数据会立刻出现在输出端,不需要等读使能。后续数据才需要读使能控制。这种模式延迟更低。
4. Data Number & Reset(数据计数与复位)
- Read/Write Data Num:勾选后,IP核会输出
Wnum和Rnum信号,告诉你当前FIFO里存了多少个数据(写计数)和还能读多少个数据(读计数),调试时非常有用。 - En_Reset:使能复位。勾选后,读写时钟域有独立的复位信号
WrReset和RdReset。 - Reset_Synchronization:如果选了
En_Reset,这个选项让你选择是否使用一个统一的复位信号Reset。为了简单,咱们通常选这个。
5. Flag Control(标志位控制)除了基本的空(Empty)/满(Full)标志,还可以使能“几乎满/几乎空”标志,让你提前知道缓冲区快满了或快空了,方便做流控。
- Almost Full Flag:使能半满标志
Almost_Full。阈值可以设成固定值(Constant Parameter)或者通过输入端口动态设置(Input Parameter)。比如我设固定值120,当FIFO里的数据达到120个时,Almost_Full就拉高。 - Almost Empty Flag:同理,使能半空标志
Almost_Empty。我设固定值10,当FIFO里数据少于等于10个时,Almost_Empty拉高。
其他如ECC校验,一般数据位宽1-64位时可选,用于检错纠错,根据需求来。
配置完点击Generate,软件就会生成一个可用的FIFO模块(.v文件)和对应的例化模板。
3. 手把手编写Verilog驱动模块
IP核生成好了,但它只是个“黑盒子”。我们需要写控制逻辑来告诉它什么时候写、什么时候读。咱们把读写控制分开成两个模块,结构更清晰。
3.1 写控制模块 (fifo_wr.v)
这个模块负责产生写使能和要写入的数据。逻辑是:当FIFO为空时,开始写入;当写计数达到我们设定的最大值(比如128)时,停止写入。
module fifo_wr( input wr_clk , // 写时钟信号 input wr_rst_n , // 复位信号,低电平有效 input [7:0] fifo_Wnum_count , // 从IP核来的写计数(当前FIFO中数据量) input fifo_empty , // 从IP核来的空标志 output reg fifo_wr_en , // 输出给IP核的写使能 output reg [6:0] fifo_wr_data // 输出给IP核的写入数据 ); parameter WR_DATA_MAX = 7'd127; // 我们要写入的最大数据值(0-127) // 写使能控制逻辑 always @(posedge wr_clk or negedge wr_rst_n) begin if(!wr_rst_n) fifo_wr_en <= 1'b0; // 复位时关闭写使能 else if(fifo_empty) // 如果FIFO空了,就启动写入 fifo_wr_en <= 1'b1; else if(fifo_Wnum_count == 8'd128) // 如果已经写满了128个,就停止 fifo_wr_en <= 1'b0; else fifo_wr_en <= fifo_wr_en; // 其他情况保持原状态 end // 写入数据生成逻辑:简单地从0递增到127 always @(posedge wr_clk or negedge wr_rst_n) begin if(!wr_rst_n) fifo_wr_data <= 7'd0; else if(fifo_wr_en && (fifo_wr_data < WR_DATA_MAX)) fifo_wr_data <= fifo_wr_data + 7'd1; // 使能有效且未到最大值,数据加1 else fifo_wr_data <= 7'd0; // 否则(如写停止或复位),数据归零 end endmodule注意:这里
fifo_Wnum_count是8位,最大值是255。我们设定的FIFO深度是128,所以当计数到128时表示真的满了。fifo_wr_data是7位,范围0-127,这是我们实际写入的数据内容。
3.2 读控制模块 (fifo_rd.v)
这个模块负责产生读使能。逻辑是:当FIFO为满时,开始读取;当可读数据量快被读空(比如只剩1个)时,停止读取,防止读空。
module fifo_rd( input rd_clk , // 读时钟信号 input rd_rst_n , // 复位信号 input [7:0] fifo_Rnum_count , // 从IP核来的读计数(当前可读数据量) input fifo_full , // 从IP核来的满标志 output reg fifo_rd_en // 输出给IP核的读使能 ); parameter RD_EMPTY_TH = 8'd1; // 读空阈值,当可读数据小于等于1时停读 always @(posedge rd_clk or negedge rd_rst_n) begin if(!rd_rst_n) fifo_rd_en <= 1'b0; else if(fifo_full) // FIFO满了,赶紧开始读 fifo_rd_en <= 1'b1; else if(fifo_Rnum_count <= RD_EMPTY_TH) // 快读空了,停 fifo_rd_en <= 1'b0; else fifo_rd_en <= fifo_rd_en; // 保持 end endmodule3.3 顶层模块整合 (fifo_top.v)
现在我们把IP核和两个控制模块“连线”起来。这个顶层模块就是整个设计的“总指挥部”。
module fifo_top( input sys_clk, // 系统时钟(这里为了简化,读写用同一时钟,实际可接不同时钟) input sys_rst_n, // 系统复位,低电平有效 output [6:0] fifo_rd_data // FIFO读出的数据,引到顶层输出便于观察 ); // 内部连线声明 wire fifo_wr_en; wire fifo_rd_en; wire [6:0] fifo_wr_data; wire almost_full; wire almost_empty; wire fifo_full; wire fifo_empty; wire [7:0] fifo_Wnum_count; wire [7:0] fifo_Rnum_count; // 例化FIFO IP核 fifo_ip u_fifo_ip ( .Data (fifo_wr_data), // input [6:0] .Reset (~sys_rst_n), // input, 注意IP核复位是高有效,我们系统复位是低有效,所以取反 .WrClk (sys_clk), // input .RdClk (sys_clk), // input, 实际异步应用时这里接不同的时钟 .WrEn (fifo_wr_en), // input .RdEn (fifo_rd_en), // input .Wnum (fifo_Wnum_count), // output [7:0] .Rnum (fifo_Rnum_count), // output [7:0] .Almost_Empty (almost_empty), // output .Almost_Full (almost_full), // output .Q (fifo_rd_data), // output [6:0] .Empty (fifo_empty), // output .Full (fifo_full) // output ); // 例化写控制模块 fifo_wr u_fifo_wr ( .wr_clk (sys_clk), .wr_rst_n (sys_rst_n), .fifo_Wnum_count (fifo_Wnum_count), .fifo_empty (fifo_empty), .fifo_wr_en (fifo_wr_en), .fifo_wr_data (fifo_wr_data) ); // 例化读控制模块 fifo_rd u_fifo_rd ( .rd_clk (sys_clk), .rd_rst_n (sys_rst_n), .fifo_Rnum_count (fifo_Rnum_count), .fifo_full (fifo_full), .fifo_rd_en (fifo_rd_en) ); endmodule提示:在这个例子里,为了仿真方便,读写时钟都接了
sys_clk。在实际的异步FIFO应用中,WrClk和RdClk必须接两个不同源或不同频率的时钟信号,这才是异步FIFO的价值所在。
4. Modelsim仿真验证
代码写完了,对不对?上仿真!用Modelsim跑一下最放心。这里有个高云FPGA仿真的坑要特别注意:如果设计里用了IP核或者一些底层原语,必须在仿真测试文件里例化一个GSR单元,否则仿真会报错。
`timescale 1ns / 1ps // 定义仿真时间单位/精度 module fifo_tb(); // ---- 关键!必须添加的GSR实例化 ---- GSR GSR_inst (.GSRI(1'b1)); // --------------------------------- // 产生测试时钟和复位 reg sys_clk; reg sys_rst_n; always #10 sys_clk = ~sys_clk; // 产生50MHz时钟(周期20ns) initial begin sys_clk = 1'b0; sys_rst_n = 1'b0; // 开始复位 #200; // 复位保持200ns sys_rst_n = 1'b1; // 释放复位 #5000; // 仿真运行5000ns $stop; // 结束仿真 end // 连接被测设计 wire [6:0] rd_data; fifo_top u_fifo_top ( .sys_clk (sys_clk), .sys_rst_n (sys_rst_n), .fifo_rd_data (rd_data) ); endmodule把上面这个测试文件、以及我们写的三个模块(fifo_top.v,fifo_wr.v,fifo_rd.v)和IP核生成的fifo_ip.v一起加到Modelsim工程里编译仿真。
看波形关键点:
- 复位后:
fifo_empty应为高,fifo_full为低,写计数和读计数为0。 - 启动写入:
fifo_empty变低后,fifo_wr_en拉高,fifo_wr_data从0开始递增。同时fifo_Wnum_count(写计数)增加。 - 触发半满:当
fifo_Wnum_count增加到我们设定的阈值120时,almost_full信号应该拉高。这说明FIFO快满了。 - 写满停写:当
fifo_Wnum_count达到128(深度)时,fifo_full拉高,同时我们的写控制模块应关闭fifo_wr_en。 - 启动读取:
fifo_full拉高后,fifo_rd_en应拉高,开始读出数据。fifo_Rnum_count(读计数)减少,fifo_rd_data输出数据。 - 触发半空:当
fifo_Rnum_count减少到我们设定的阈值10时,almost_empty拉高。 - 读空停读:当
fifo_Rnum_count为0时,fifo_empty拉高,读使能fifo_rd_en关闭。
波形如果完全按这个顺序变化,恭喜你,仿真通过了!
5. 上板实测与在线调试
仿真通过后,就可以综合、布局布线,生成比特流下载到高云FPGA开发板了。
引脚约束:根据你的板子,把顶层模块的时钟(sys_clk)和复位(sys_rst_n)信号分配到实际的晶振引脚和按键引脚上。输出数据fifo_rd_data可以接到LED或者用在线逻辑分析仪看。
高云在线逻辑分析仪(GAO):这是调试利器。把fifo_wr_en,fifo_rd_en,fifo_wr_data,fifo_rd_data,almost_full,almost_empty,full,empty这些关键信号加进去观察。
上板抓到的波形应该和仿真一致。可能会看到数据在almost_full和almost_empty阈值附近有些毛刺或抖动,这是正常的,因为在线采样和时钟域不同步。只要主要逻辑正确就没问题。
6. 实际项目中的几点心得
- 深度计算要留余量:别可丁可卯地设深度。比如你计算出来需要缓存100个数据,最好设成128或256,防止偶尔的数据突发导致溢出。
- 异步时钟要约束:如果读写真是异步时钟,一定要在设计约束文件里定义这两个时钟,并设置
set_clock_groups -asynchronous来告诉工具它们是异步的,避免工具做无谓的时序优化。 - 标志位延迟:
empty和full标志的产生有延迟(通常1-2个读时钟或写时钟周期)。所以你的控制逻辑不能看到full才停写,看到empty才停读,要提前(用almost_full/empty)或留出安全余量。 - 复位处理:异步FIFO的复位需要特别小心,确保复位信号在各自时钟域下被正确同步和释放。用IP核的话,它内部一般会处理好。
好了,从IP核配置、Verilog驱动到仿真上板的完整流程就是这样。异步FIFO是FPGA跨时钟域通信的基石,把这个练熟了,后面做图像、通信项目就顺手多了。遇到问题多看看波形,理解数据流和标志位的变化,调试起来就不难了。