第二十六章 FIFO IP核实验
对应《正点原子 ZYNQ FPGA 开发指南》第二十六章(约 P601~P630),本章无外部硬件参与,实验结果通过两个 ILA(分属读写两个时钟域)观察。
本章在讲什么?(先看这个)
FIFO(First In First Out,先入先出)是 FPGA 设计里出场率最高的缓存结构:数据从一头写、从另一头按同样的顺序读出,没有地址线、不用关心"存在哪",读写指针由内部自动管理。它主要解决两类问题:多比特数据跨时钟域传输(上一章埋的伏笔——伪双端口 RAM 读写可各用各的时钟,FIFO 内部正是这个结构)和两侧处理带宽不一致时的缓冲。
本章做一个异步 FIFO(写时钟 50MHz、读时钟 100MHz,由第 23 章学的 PLL 提供),实验逻辑故意设计成极限场景:FIFO 读空了就写、写满了就读,从而完整观察到 empty/full/almost_empty/almost_full 四个状态信号的切换。本章最重要的知识点有两个:跨时钟域信号必须"打两拍同步",以及 FIFO 复位后必须等 wr_rst_busy/rd_rst_busy 释放才能工作。
核心概念拆解
26.1 FIFO IP 核简介
FIFO = RAM + 读写控制逻辑。与普通 RAM 的核心区别:没有外部地址线,只能顺序写、顺序读,也因此天然不存在读写冲突问题。按时钟域分为同步 FIFO(读写同一时钟)和异步 FIFO(读写时钟独立,用于跨时钟域)。
常用参数速览:宽度(一次读写的数据位宽)、深度(能存多少个数据)、empty(空,防无效读)/full(满,防写溢出)、almost_empty/almost_full(将空/将满,比空/满提前一个操作生效,用来"提前刹车")、prog_empty/prog_full(用户自定义阈值的预警信号)。
位宽不对等时有个冷知识:FIFO 先写入的数据排在高位。比如写位宽 8、读位宽 2,写入 8'b11000111,读出依次是 11、00、01、11。

实现 FIFO 有三条路:自己写控制逻辑、用开源 IP、用官方 FIFO Generator(推荐,针对不同器件还能自动做结构优化)。FIFO Generator 的信号框图如下,黑箭头是必要信号(wr_clk/wr_en/din/full、rd_clk/rd_en/dout/empty、rst),蓝箭头是可选信号(almost_empty/almost_full 等),灰箭头是可选边带信号(prog_full、data_count、wr_rst_busy 等)。

26.2 实验任务
生成一个异步 FIFO:空时写入数据直到写满停止写;满时读出数据直到读空停止读。仿真验证后下载到开发板用 ILA 观察。
26.3 程序设计
整体模块设计
系统共五个模块:顶层 ip_fifo + PLL(clk_wiz_0,输出 50M 写时钟和 100M 读时钟)+ FIFO IP(fifo_generator_0)+ 写模块 fifo_wr + 读模块 fifo_rd。

FIFO IP 核配置
IP Catalog 搜 "fifo",双击 FIFO Generator。配置页面比 BMG 复杂,重点在四个选项卡:

① Basic 选项卡:
- Interface Type 选 Native(AXI 接口用于 PS 交互或高速流处理);
- Fifo Implementation 选 Independent Clocks Block RAM——用 BRAM 实现读写时钟独立的异步 FIFO。资源选择的粗略规则:容量超过 1024 字节用 BRAM,否则用分布式 RAM(另两种:移位寄存器只能做同步 FIFO,Builtin FIFO 基本用不到);
- Synchronization Stages 保持 2(跨时钟域同步器的级数,对应我们后面代码里的"打两拍")。
② Native Ports 选项卡:

- Read Mode 选 Standard FIFO(标准模式):数据输出比读使能延迟一拍;另一种 FWFT(首字直通/预读)模式数据和读使能同时出现,两者写下游逻辑的思路完全不同;
- 写数据位宽 8、写深度 256——注意实际深度只有 255(Xilinx FIFO 都会少一个);
- 输出寄存器(Output Registers)不勾选,保证输出只延迟一拍;
- 复位相关保持默认:启用复位脚(高有效)、启用复位同步、启用安全电路(输出 wr_rst_busy/rd_rst_busy)、满信号复位值 1。
③ Status Flags 选项卡:勾选输出 Almost Full Flag 和 Almost Empty Flag(本次实验的写/读停机信号就是它们)。

④ Data Counts 选项卡:勾选 Write Data Count 和 Read Data Count(异步 FIFO 时两个计数分别同步于写/读时钟),方便在 ILA 里直观看到 FIFO 里的数据量。
Summary 核对后 OK → Generate。
时序图讲解:空满与复位
读写时序:empty 拉高时发起读会读到无效数据;full 拉高后继续写会写不进去、数据丢失(如果配置了握手信号,会表现为 wr_ack 拉低、overflow 拉高;读侧对应 valid 拉低、underflow 拉高)。almost_full 在"只能再写一次"时拉高、almost_empty 在"只剩最后一个数据"时拉高——用 almost 信号做停机条件,正好在最后一条数据处理完后干净地关掉使能,不浪费也不多余。

复位时序(异步 FIFO 启用安全电路时):
- 复位信号至少保持 8 个时钟周期有效(以慢时钟为准);
- 复位后约 7 个写时钟周期,wr_rst_busy 拉高、full 拉高;读侧同理 rd_rst_busy 拉高、empty 拉高;
- 复位结束后至少再等 60 个时钟周期(以慢时钟为准)才能写数据——实际工程中直接用 wr_rst_busy/rd_rst_busy 是否释放来判断即可。

FIFO 写模块设计
写模块的输入 empty 是读时钟域的信号,对写时钟域而言是异步信号,直接使用会有亚稳态风险——所以先打两拍同步到写时钟域(empty_d0/empty_d1)。策略:wr_rst_busy 释放后,检测到 empty_d1 为高就开写,almost_full 拉高就停写。

module fifo_wr(
//mudule clock
input wr_clk , // 时钟信号
input rst_n , // 复位信号
//FIFO interface
input wr_rst_busy , // 写复位忙信号
input empty , // FIFO 空信号
input almost_full , // FIFO 将满信号
output reg fifo_wr_en , // FIFO 写使能
output reg [7:0] fifo_wr_data // 写入FIFO 的数据
);
//reg define
reg empty_d0;
reg empty_d1;
//------------------------------------------------
//因为empty 信号是属于FIFO 读时钟域的
//所以对empty 打两拍同步到写时钟域下
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
empty_d0 <= 1'b0;
empty_d1 <= 1'b0;
end
else begin
empty_d0 <= empty;
empty_d1 <= empty_d0;
end
end
//对fifo_wr_en 赋值,当FIFO 为空时开始写入,写满后停止写
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n)
fifo_wr_en <= 1'b0;
else if(!wr_rst_busy) begin
if(empty_d1)
fifo_wr_en <= 1'b1;
else if(almost_full)
fifo_wr_en <= 1'b0;
end
else
fifo_wr_en <= 1'b0;
end
//对fifo_wr_data 赋值,0~254
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
fifo_wr_data <= 8'b0;
else if(fifo_wr_en && fifo_wr_data < 8'd254)
fifo_wr_data <= fifo_wr_data + 8'b1;
else
fifo_wr_data <= 8'b0;
end
endmodule
写数据在 0~254 循环——正好 255 个数,与 FIFO 的实际深度(256-1=255)对应,读出时就能逐个对账。
FIFO 读模块设计
镜像结构:full 是写时钟域信号,打两拍同步到读时钟域;rd_rst_busy 释放后,full_d1 为高开读,almost_empty 拉高停读。

module fifo_rd(
//system clock
input rd_clk , //时钟信号
input rst_n , //复位信号
//FIFO interface
input rd_rst_busy , //读复位忙信号
input [7:0] fifo_rd_data, //从FIFO 读出的数据
input full , //FIFO 满信号
input almost_empty, //FIFO 将空信号
output reg fifo_rd_en //FIFO 读使能
);
//reg define
reg full_d0;
reg full_d1;
//------------------------------------------------
//因为full 信号是属于FIFO 写时钟域的
//所以对full 打两拍同步到读时钟域下
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n) begin
full_d0 <= 1'b0;
full_d1 <= 1'b0;
end
else begin
full_d0 <= full;
full_d1 <= full_d0;
end
end
//对fifo_rd_en 进行赋值,FIFO 写满之后开始读,读空之后停止读
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n)
fifo_rd_en <= 1'b0;
else if(!rd_rst_busy) begin
if(full_d1)
fifo_rd_en <= 1'b1;
else if(almost_empty)
fifo_rd_en <= 1'b0;
end
else
fifo_rd_en <= 1'b0;
end
endmodule
顶层模块设计
顶层例化 PLL、FIFO IP、读写模块和两个 ILA。一个关键细节:系统复位要和 PLL 的 locked 信号相与(rst_n = sys_rst_n & locked),确保 PLL 时钟稳定后系统才解除复位——这正是第 23 章"locked 之前的时钟不可用"的落地应用。
module ip_fifo(
input sys_clk , // 系统时钟信号
input sys_rst_n // 系统复位信号
);
//wire define
wire clk_50m ; // 50M 时钟
wire clk_100m ; // 100M 时钟
wire locked ; // 时钟锁定信号
wire rst_n ; // 复位,低有效
wire wr_rst_busy ; // 写复位忙信号
wire rd_rst_busy ; // 读复位忙信号
wire fifo_wr_en ; // FIFO 写使能信号
wire fifo_rd_en ; // FIFO 读使能信号
wire [7:0] fifo_wr_data ; // 写入到FIFO 的数据
wire [7:0] fifo_rd_data ; // 从FIFO 读出的数据
wire almost_full ; // FIFO 将满信号
wire almost_empty ; // FIFO 将空信号
wire full ; // FIFO 满信号
wire empty ; // FIFO 空信号
wire [7:0] wr_data_count ; // FIFO 写时钟域的数据计数
wire [7:0] rd_data_count ; // FIFO 读时钟域的数据计数
//------------------------------------------------
//通过系统复位信号和时钟锁定信号来产生一个新的复位信号
assign rst_n = sys_rst_n & locked;
//例化PLL IP 核
clk_wiz_0 clk_wiz_0 (
.clk_out1(clk_50m ),
.clk_out2(clk_100m),
.locked (locked ),
.clk_in1 (sys_clk )
);
//例化FIFO IP 核
fifo_generator_0 fifo_generator_0 (
.rst (~rst_n ), // input wire rst(高有效,注意取反)
.wr_clk (clk_50m ),
.rd_clk (clk_100m ),
.wr_en (fifo_wr_en ),
.rd_en (fifo_rd_en ),
.din (fifo_wr_data ),
.dout (fifo_rd_data ),
.almost_full (almost_full ),
.almost_empty (almost_empty ),
.full (full ),
.empty (empty ),
.wr_data_count (wr_data_count),
.rd_data_count (rd_data_count),
.wr_rst_busy (wr_rst_busy ),
.rd_rst_busy (rd_rst_busy )
);
//例化写FIFO 模块
fifo_wr u_fifo_wr (
.wr_clk (clk_50m ),
.rst_n (rst_n ),
.wr_rst_busy (wr_rst_busy ),
.fifo_wr_en (fifo_wr_en ),
.fifo_wr_data (fifo_wr_data),
.empty (empty ),
.almost_full (almost_full )
);
//例化读FIFO 模块
fifo_rd u_fifo_rd (
.rd_clk (clk_100m ),
.rst_n (rst_n ),
.rd_rst_busy (rd_rst_busy ),
.fifo_rd_en (fifo_rd_en ),
.fifo_rd_data (fifo_rd_data),
.almost_empty (almost_empty),
.full (full )
);
//写时钟域下ila
ila_0 u_ila_wr (
.clk (clk_50m ),
.probe0 (fifo_wr_en ),
.probe1 (fifo_wr_data ),
.probe2 (almost_full ),
.probe3 (full ),
.probe4 (wr_data_count)
);
//读时钟域下ila
ila_1 u_ila_rd (
.clk (clk_100m ),
.probe0 (fifo_rd_en ),
.probe1 (fifo_rd_data ),
.probe2 (almost_empty ),
.probe3 (empty ),
.probe4 (rd_data_count)
);
endmodule
26.4 仿真验证与下载验证
TB 里特意模拟了一次二次复位:先复位 200ns,正常跑 10us,再拉低复位保持 160ns(= 8 × 20ns,满足"至少 8 个慢时钟周期"的要求)后释放——用来观察 FIFO 复位期间各标志的表现。
`timescale 1ns / 1ps
module tb_ip_fifo();
parameter CLK_PERIOD = 20; //时钟周期 20ns
reg sys_clk;
reg sys_rst_n;
//信号初始化
initial begin
sys_clk = 1'b0;
sys_rst_n = 1'b0;
#200
sys_rst_n = 1'b1;
//模拟按下复位
#10000 ;
sys_rst_n = 0;
#160 ;
sys_rst_n = 1;
end
//产生时钟
always #(CLK_PERIOD/2) sys_clk = ~sys_clk;
ip_fifo u_ip_fifo (
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n)
);
endmodule
仿真波形要点:
- 复位来临后,经若干读/写时钟周期,wr_rst_busy/rd_rst_busy 拉高,空/满标志恢复到初始设定值;
- 写状态:写使能拉高后写数据 0~254 连续写入,写完倒数第二个数时 almost_full 拉高、写完最后一个数时 full 拉高、写使能关闭。wr_data_count 的变化不是 1 步进递减、也不会递减到 0——因为读写速率不同 + 跨时钟域同步延迟,写使能在计数更新之前就重新开启了,这是正常现象;
- 读状态:读使能拉高后读出数据,读完倒数第二个数 almost_empty 拉高、读完最后一个数 empty 拉高、读使能关闭。读数据比读使能延迟一拍(标准模式的读延迟)。


下载验证:引脚仍只需 sys_clk(U18)、sys_rst_n(N16),LVCMOS33。ILA 波形与仿真一致:读空了就写、写满了就读,空满标志切换正常,FIFO 读写实验验证成功。
初学者容易踩的坑
- 跨时钟域的空满标志必须打两拍再用。empty 属于读时钟域、full 属于写时钟域,对端直接拿来做判断有亚稳态风险。打两拍会引入 2 拍延迟,所以状态信号的反应比"理论时刻"晚几个周期——wr_data_count 看起来"不归零"就是这么来的,不是 bug。
- 深度设 256 实际只有 255。Xilinx FIFO 的实际可用深度永远是配置值减 1,写满判断、数据对账都要按 255 算。
- 复位时序有三重讲究:复位至少保持 8 个慢时钟周期;复位后要等 60 个慢时钟周期(或直接等 wr_rst_busy/rd_rst_busy 释放)才能读写;复位期间读写时钟必须有效(不能给 FIFO 断时钟)。最省心的做法就是像本章一样全程盯着 rst_busy 信号。
- FIFO IP 的 rst 是高电平有效,而系统复位 sys_rst_n 是低电平有效——例化时写
.rst(~rst_n)别忘了取反,这和第 23 章 PLL 的坑如出一辙。 - 标准模式和 FWFT 模式下游逻辑写法完全不同。标准模式"给读使能→下一拍出数据",FWFT 模式"数据一直在读数据口上、给读使能表示取走这条"。换模式不改代码,时序就全错。
- 要两个 ILA 才能观察全貌。写时钟域和读时钟域信号各有各的采样时钟,ILA 的采样时钟必须与被观察信号同域,所以本章例化了 ila_0(50M,观察写侧)和 ila_1(100M,观察读侧)。
- 位宽和深度别贪大。FIFO 吃的是 BRAM 资源,满足需求的前提下尽量往小配。
小结 & 下一步
本章是存储器三部曲的收官:用 PLL 提供双时钟搭建了异步 FIFO,掌握了 FIFO Generator 的核心配置(Independent Clocks + BRAM、Standard/FWFT 读模式、almost 标志、data_count、安全电路),理解了空满标志跨时钟域打两拍同步、rst_busy 等待、8/60 周期复位规则这三个异步 FIFO 的标配操作。至此,"跨时钟域安全传输多比特数据"这个 FPGA 设计的经典课题,你已经有了一套完整的工程化方案。
下一步进入第二十七章:UART 串口通信实验——FPGA 第一次和外部芯片"对话",用 115200 波特率收发数据,环回验证。