FIFO的IP核学习

在 Vivado 中配置 IP 核就像是"菜单式点菜",你只需要告诉编译器你需要一个多大、多宽、什么工作模式的 FIFO,它就会自动去 FPGA 底层帮你把几万个逻辑门和存储单元连好。

1,召唤FIFO生成器

1.1,在 Vivado 左侧导航栏(Flow Navigator)中,点击 IP Catalog

1.2,在右侧弹出的搜索框里输入 FIFO

1.3,找到 FIFO Generator,双击打开它的配置界面。

2,Basic 选项卡(定基调)

这是配置的第一页,决定了这个 FIFO 的核心物理架构。"给我用芯片上的物理硬核内存(BRAM),搭建一个不用跨时钟域(Common Clock)的、可以直接受我底层状态机裸线控制(Native)的巨大蓄水池。"

  • Component Name(组件名称)

    • 填入fifob_tx_data_merge_sign

    • 为什么 :必须和你在 Verilog 代码里实例化的名字一字不差,否则 Vivado 依然找不到它。

  • Interface Type(接口类型)

    • 选择Native

    • 为什么 :如果选 AXI,它会自动带上 TVALID/TREADY 那些复杂的握手信号。但你看你的代码,师兄是自己写状态机去控制 wr_en(写使能)和 rd_en(读使能)的,所以必须选最原始、最纯粹的 Native 接口。它是 FIFO 最原始、最裸露的物理接口。只有最干瘪的几根线:din(进货)、dout(出货)、wr_en(写开关)、rd_en(读开关)。你不给 en 信号,它绝对不动。

  • Fifo Implementation(FIFO 底层实现)

    • 选择Common Clock Block RAM

    • 为什么

Common Clock(共用时钟)

因为你代码里只输入了一个 clk,读和写都是这一个时钟,FIFO 的进货门(写)和出货门(读),听的是同一个时钟节拍(同一个振荡器), 大家步调完全一致。称为同步FIFO。如果是跨时钟域(比如之前的 CDC 模块),这里就要选 Independent Clocks。

这就是我们最早看 CDC(跨时钟域)代码时聊到的情况。比如 ARM 给数据用的是 40MHz 时钟,而你物理层处理用的是 100MHz 时钟。

Block RAM(块 RAM)

FPGA 内部有专门用来做大容量存储的硬核(BRAM)。

  • 通俗比喻"芯片出厂时就建好的钢筋混凝土标准大仓库"

  • 物理意义:Xilinx 在设计这块 FPGA 芯片时,在晶圆上专门划出了一块块纯粹的 SRAM 物理内存条。

  • 优点:读写速度极快,不消耗宝贵的 LUT 逻辑门资源。

  • 为什么这里必须选它?:面对 4096 深度的海量数据缓冲,调用 BRAM 是唯一的选择。这就像大规模物流集散中心,必须用专业的独立大仓库。

信令一存就是几千个,如果用 Distributed RAM(分布式 RAM,用逻辑门拼出来的)会严重浪费极其宝贵的逻辑资源。

  • 通俗比喻"用乐高积木临时拼一个仓库"

  • 物理意义强行征用那些本来用来做加减乘除逻辑运算的 LUT,把它们当成内存单元来存数据。

  • 什么时候用? :当你的 FIFO 极其小 的时候(比如深度只有 16 或者 32),用分布式 RAM 反应最快,布局最灵活。

  • 如果在这里用了会怎样?:灾难!你的深度是 4096,位宽是 32。这需要海量的存储空间。如果用分布式 RAM,会把这片区域的逻辑门全吃光,导致你的 OFDM 算法(如 FFT)没地方放,或者因为连线太拥挤导致编译超时、时序爆炸。

3,Native Ports 选项卡(定尺寸)

这一页决定了水管的粗细和蓄水池的大小。

  • Read Mode(读模式)

    • Standard FIFO(标准模式)

    • 行为逻辑"先申请,后发货" 。当下游模块把读使能 rd_en 拉高后,FIFO 会在下一个时钟上升沿 到来时,才把数据吐到 dout 端口上。这中间有 1 个时钟周期的物理延迟(Latency = 1)

    • 为什么 Standard 模式会让这段代码崩溃? 在你的代码中,有这样两句纯组合逻辑:

      // 只要不空,就立刻对外宣告有货 (Valid = 1)

      assign data_merge_valid = (tx_state == tx_state_sign & (!fifob_sign_empty)) | ...;

      // 把 FIFO 的输出引脚,直接连到对外的总线上

      assign data_merge_i = tx_state == tx_state_sign ? sign_dout[15:0] : ...;

    • 如果用 Standard 模式 : 当信令 FIFO 刚写入第一个数据时,empty 变成了 0(不空了)。 代码立刻反应:对外拉高了 data_merge_valid = 1但是! Standard 模式下,这时候你要的数据还在 FIFO 的肚子里,dout 引脚上现在全是垃圾数据或者上一帧残留的旧数据。你必须给出 rd_en = 1,再等 1 个时钟周期,正确的数据才会出现在 dout 上。 这就导致了致命的时序错位:你对外喊着"我有货(Valid=1)",但交出去的第一个数据却是垃圾。

    【第 0 拍】:风平浪静
    • 信令 FIFO:空(empty = 1)。

    • 输出引脚:sign_dout 上没有任何有效数据,全是上一帧残留的电平,也就是**"垃圾数据(Garbage)"**。

    • 对外状态:组合逻辑算出来 data_merge_valid = 0。下游乖乖等着。

    🎬 【第 1 拍的瞬间】:写数据,大祸临头!

    就在这一拍,上游把第一个真实数据 [Data_0] 写进了 FIFO。

    • 瞬间反应 A :FIFO 内部的计数器从 0 变成 1,empty 标志瞬间1 变成了 0(不空了!)。 中间有个冷启动

    • 瞬间反应 B :你写的组合逻辑(逻辑1)就像被踩了尾巴的猫,一看到 empty == 0瞬间(0延迟) 把对外的引脚 data_merge_valid 拉高变成了 1

    • 致命的现实 :此时此刻,因为你才刚刚发现有数据,你的状态机才刚刚把 rd_en 拉高,真实的数据 [Data_0] 还死死地锁在 FIFO 的肚子里! 此时 sign_dout 导线上挂着的,依然是那个**"垃圾数据"**!

    🎬 【第 2 拍】:迟到的真相,全盘错位

    时钟沿再次打响。

    • 因为上一拍 rd_en 是 1,Standard FIFO 终于把真实的 [Data_0] 推到了 sign_dout 引脚上。

    • 但是,下游在上一拍已经吃进了一个垃圾数据,所以下游理所当然地认为,现在线上的这个 [Data_0]第 1 个信令数据(实际上它是第 0 个)。

    • 接下来发的所有数据,全部向后错位了 1 拍。一帧 720 个信令,下游收到的第 0 个是垃圾,第 1 到 719 个才是真实数据,而且最后少了一个数据。

    • 通信系统里,错位 1 个数据,整帧解调必定彻底失败(CRC 校验绝对报错)。

      👉 此时下游接收端看到的画面是什么? 下游一看:哇!VALID = 1(你有货),我刚好 READY = 1(我能收)。 握手成功!下游毫不犹豫地张开大嘴,把线上的**"垃圾数据"**当成第 0 个信令吃了进去!

      场景一:"冷启动"(从无到有)

      假设系统刚复位,或者上一帧刚发完,系统正在休息。

    • 初始状态tx_state = tx_state_idle(空闲状态)。

    • 信令 FIFO :空的(empty = 1)。

    • 现在,上游发来了本帧的第一个信令数据,写进了 FIFO。empty 瞬间降为 0。 这个时候,显微镜下的微观世界是这样的:

    • 组合逻辑(瞬间反应)

      • 交警的大脑(第一段状态转移逻辑)瞬间发现 ~fifob_sign_empty 成立了,立刻算出 tx_nextstate = tx_state_sign

      • 但是! 此时负责产生 valid 的那根导线去检查 tx_state 时,发现 tx_state 依然是 IDLE(因为时钟边沿还没来,状态还没真正切过去)。

      • 所以,此时 data_merge_valid 依然是 0。下游此时不会误吃数据。

    • 时钟沿到来(打响指,过了一拍)

      • 状态寄存器更新:tx_state 正式变成了 SIGN

      • 就在 tx_state 变成 SIGN 的这个绝对瞬间,那根产生 valid 的组合逻辑导线终于等齐了两个条件(tx_state == SIGNempty == 0)。读使能sign_rd_en才拉高

      • 于是,data_merge_valid 瞬间跳成 1

  • FWFT (First Word Fall Through,首字掉落模式)

    • 行为逻辑"提前备货,随用随取" 。FIFO 只要不为空,它内部的逻辑会自动 把最老的那一个数据推到 dout 端口上挂着。当下游拉高 rd_en 的那一瞬间,下游**立刻(0 延迟)**就能取走这个数据,然后在下一个时钟沿,FIFO 会自动补上第二个数据。

    • 代价:虽然方便,但为了实现"自动提前拿出来",芯片底层需要额外消耗一批寄存器(Register)来做缓存,浪费了资源。既然你的状态机能完美处理 Standard 的那一拍延迟,就完全没必要浪费资源去选 FWFT。

  • Write Width / Read Width(数据位宽)

    • 填入32

    • 为什么 :回忆一下你的代码 .din({sign_symbmod_q, sign_symbmod_i})。I 路 16 位,Q 路 16 位,拼在一起塞进去刚好是 32 位。所以进出水管都必须是 32 宽。

    • 在物理层算法中,I路(同相分量)和 Q路(正交分量)永远是成对出现的复数信号。I 路是 16 根导线(16-bit); Q路是 16 根导线(16-bit)。如果不拼接,你必须在这个车间里建两个平行的 FIFO,一个专门装I,一个专门装Q。这会让控制逻辑变得极其复杂(万一两个 FIFO 状态不同步就全乱了)。

    • 师兄的做法 din({sign_symbmod_q, sign_symbmod_i}) 相当于用电工胶布,把 Q路的 16 根线和 I 路的 16 根线并排绑在了一起,捆成了一根 32 根线粗的超级排线 。 这样,只需要一个 32 位宽的 FIFO,每次存取都是"I 和 Q 同进同出",完美保证了复数信号在物理层上的绝对同步。

  • Write Depth(数据深度)

    • 填入4096

    • 为什么 :(极其硬核的推导来了! ) 你看代码里定义了 wire [12:0] fifob_sign_count;这是一个 13 位的变量,13根线[12:0]表示从第0位到第12位,共3位! 在二进制中,13 位能表示的最大数字是 8191。但是,为什么不填 8192 呢? 因为如果深度是 4096(2的16次方),当 FIFO 存满时,里面有 4096 个数据,数字 4096 的二进制是 1 0000 0000 0000刚好需要 13 位来表示 。而且它里面存的数据个数 data_count 有多少种可能? 是从 0 个(全空)一直到 4096 个(全满)。注意!从 0 数到 4096,一共包含了 4097 个状态!如果你只给 12 根线,它能表示的最大数字是 4095了。

    • 这就完美对上了师兄留下的那根 [12:0] 的线!同时,一帧信令才 720 个,深度 4096 完全装得下,还能起到很好的缓冲作用。

4,Status Flags 选项卡(定报警器)

这页配置默认的满/空报警。

  • Standard Flags 里的 Almost Full FlagAlmost Empty Flag 可以不用勾选

  • 为什么 :因为我们的代码没有用到 .almost_full() 这个引脚。代码里判断空用的是 empty,这个引脚是必定存在的,不需要额外配置。

5,Data Counts 选项卡(最关键的一步!)

这一步如果你漏了,综合时一定会报错! 因为它是反压机制的"眼睛"。

  • Use Data Count(使用数据计数器)

    • 必须勾选!

    • 为什么 :你代码里有一句核心反压逻辑:assign sign_symbmod_ready = fifob_sign_count < 'd3000;。这意味着模块需要实时知道 FIFO 里到底装了多少个数据。

    • 勾选这个选项后,你会看到下方弹出一个 Data Count Width,它会自动变成 13。这就是我们在第三步推导出来的完美匹配结果!

    • 勾选后,IP 核的引脚上就会多出一个叫 data_count 的输出端口,这样你的 .data_count(fifob_sign_count) 这行代码就能顺利连上了。

6,Summary 选项卡 与 生成 (Generate)

  • 点击左侧的 Summary ,你能看到一个预览图。此时你核对一下引脚: clk, srst, din[31:0], wr_en, rd_en, dout[31:0], full, empty, data_count[12:0]如果这 9 个引脚都在,说明你捏出来的零件完美契合了代码图纸!

  • 点击右下角的 OK

  • 在弹出的 Generate Output Products 窗口中,直接点击 Generate

7,IP核(模块)用法

在 C 语言或 Python 里,我们配好了一个库,接下来就是去查它的 API 文档,看看这个函数要传什么参数。 在 FPGA 的硬件世界里,这个动作有一个专门的术语,叫做**"例化"(Instantiation)**。这就好比你在车间里拿到了刚刚造好的"黑盒 FIFO 蓄水池",现在要把管子(引脚)接进你的主干道里。

Vivado 极其贴心,它每次生成完 IP 核,都会自动给你生成一份**"接线说明书(例化模板)"**。跟着下面这三步,你就能找到它:

1,

点击 IP Sources 标签页;

IP Sources 列表里,你会看到你刚刚生成的所有 IP 核,找到并展开 fifob_tx_data_merge_sign

找到一个名叫 Instantiation Template(例化模板)的文件夹,展开它。

看到一个后缀为 .veo 的文件(代表 Verilog Template),双击打开它

fifob_tx_data_merge_sign your_instance_name (

.clk(clk), // input wire clk

.srst(srst), // input wire srst

.din(din), // input wire [31 : 0] din

.wr_en(wr_en), // input wire wr_en

.rd_en(rd_en), // input wire rd_en

.dout(dout), // output wire [31 : 0] dout

.full(full), // output wire full

.empty(empty), // output wire empty

.data_count(data_count) // output wire [12 : 0] data_count

);

"函数传参"语法:.引脚名(你的线)

在 Verilog 里,例化的语法非常死板且严谨。

  • fifob_tx_data_merge_sign :这是你配出来的 IP 核的名字(相当于软件里的类名)。

  • your_instance_name :这是你在车间里给这个具体的池子起的编号,随便起。你师兄起的叫 fifob_tx_data_merge_sign_inst(相当于软件里的对象名)。

  • 括号里的映射 .x(y)

  • 点号 . 后面的名字 :绝对不能改!这是 IP 核自带的物理引脚名字(就像插座上的火线、零线标识)。

  • 括号 ( ) 里面的名字 :这是你自由发挥的地方!把你自己图纸上的导线名字填进去(就像你拿什么颜色的电线去插那个插座)。

相关推荐
我爱C编程2 小时前
【3.3】FFT变换的FPGA实现整体概述以及模块划分
fpga开发·fft·多级fft·二维分治fft
星华云2 小时前
[FPGA] Spartan6 单总线协议 (One-Wire) 读取DS18B20温度传感器
fpga开发·温度传感器·ds18b20·单总线协议·one-wire bus
折锦烟2 小时前
AI Agent 开发 0-1 学习路线(学习目标)
学习
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(六):<线程同步与互斥>线程同步(上)
java·linux·运维·服务器·c++·学习·线程
brave_zhao3 小时前
什么是增值税
学习
herinspace3 小时前
管家婆实用帖-如何使用ping命令检测网络环境
网络·数据库·人工智能·学习·excel·语音识别
阳光宅男@李光熠3 小时前
【电子通识】为什么PCB能短接还要用0Ω电阻?0欧电阻怎么做降额?
笔记·学习
小饕3 小时前
RAG学习之-Rerank 技术详解:从入门到面试
人工智能·学习
爱凤的小光3 小时前
ROS1/ROS2中TF坐标变换---个人学习篇
学习