第二十六章 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。

图26.1.1 写位宽大于读位宽时的数据排序

实现 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.1.3 常规接口的FIFO信号框图

26.2 实验任务

生成一个异步 FIFO:空时写入数据直到写满停止写;满时读出数据直到读空停止读。仿真验证后下载到开发板用 ILA 观察。

26.3 程序设计

整体模块设计

系统共五个模块:顶层 ip_fifo + PLL(clk_wiz_0,输出 50M 写时钟和 100M 读时钟)+ FIFO IP(fifo_generator_0)+ 写模块 fifo_wr + 读模块 fifo_rd

图26.3.1 FIFO读写实验系统框图

FIFO IP 核配置

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

图26.3.3 Basic选项卡配置界面

① 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 选项卡

图26.3.4 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(本次实验的写/读停机信号就是它们)。

图26.3.5 Status Flags选项卡配置界面

④ 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 信号做停机条件,正好在最后一条数据处理完后干净地关掉使能,不浪费也不多余

图26.3.8 同步FIFO读写操作时序图

复位时序(异步 FIFO 启用安全电路时):

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

图26.3.9 安全电路下的异步FIFO复位时序图

FIFO 写模块设计

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

图26.3.13 FIFO写模块波形图

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 拉高停读。

图26.3.15 FIFO读模块波形图

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 拉高、读使能关闭。读数据比读使能延迟一拍(标准模式的读延迟)。

图26.3.19 FIFO IP写状态仿真波形1

图26.3.21 FIFO IP读状态仿真波形1

下载验证:引脚仍只需 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 波特率收发数据,环回验证。