VIVADO AXI-Stream 数据流控制实战:从模块设计到握手协议解析

📅 发布时间:2026/7/12 20:59:53 👁️ 浏览次数:
VIVADO AXI-Stream 数据流控制实战:从模块设计到握手协议解析
1. 从零开始AXI-Stream 到底是什么如果你刚开始接触 FPGA 设计尤其是用上了 Xilinx 的 VIVADO 平台那么“AXI”这个词你肯定绕不过去。它就像是 FPGA 内部各个模块之间沟通的“官方语言”。今天我们不聊复杂的 AXI4 内存映射就聚焦在AXI-Stream上你可以把它想象成一条单向的、高速的数据流水线。这条流水线专门用来传输连续的数据流比如视频的像素流、音频的采样流或者你从传感器里源源不断读出来的数据。它的核心目标就一个高效、无阻塞地把数据从一个地方“搬”到另一个地方。我刚开始学的时候总觉得协议很复杂一堆信号看着头疼。但实际用下来发现只要你理解了最核心的那对“握手信号”整个数据流控制就通了剩下的都是锦上添花。那么这条流水线是怎么工作的呢它不像你写软件一个函数调用另一个函数那么简单。在硬件里发送数据的模块Master和接收数据的模块Slave是独立工作的它们的时钟节奏可能微妙地不同处理速度也可能有快有慢。AXI-Stream 协议的精妙之处就在于它用一套简单的握手规则完美解决了“什么时候能发”和“什么时候能收”的问题确保数据不会在传输中丢失也不会被重复发送。我们这次实战就是要亲手搭建一条这样的流水线从产生数据、打包、经过 FIFO 缓冲、通过总线互联最后解包还原把整个流程走通。你会发现一旦掌握了这个模式很多涉及数据搬运的 FPGA 设计难题思路都会清晰起来。2. 实战蓝图我们的数据流管道如何搭建在动手写代码之前我们先得把整个实验的蓝图或者说数据流的“管道图”在脑子里画清楚。参考我之前的经验一个典型的数据流处理链路可以这样设计它模拟了一个很常见的场景数据产生、加工、缓冲、路由、最终消费。第一个模块是data_gen数据生成器。它的活儿很简单就是像个泉眼一样按一定的规律比如递增产生原始数据字节。我给它设计了一个done信号当它产生完预设数量比如128个的数据后就拉高这个信号告诉后面的模块“我这儿的数据告一段落了”。这个模块是管道的起点它只负责生产不关心下游能不能处理得过来所以它的接口非常简单。第二个模块是data_packed数据打包器。生成器吐出来的是一个个字节8位但我们的流水线可能更擅长传输16位或32位的“数据包”。打包器的作用就是把两个8位数据拼成一个16位数据当然为了实验简单我们这里采用补零的方式打包。最关键的是从这个模块开始我们就要引入 AXI-Stream 的握手信号了。它作为发送方Master需要产生m_valid信号来指示它输出的数据是否有效同时它也要侦听来自下游的m_ready信号只有下游说“我准备好了”它才能把数据真正送出去。第三个环节是AXI-Stream FIFO和AXI-Stream Interconnect。这是 VIVADO 的 IP 核我们直接调用。FIFO 的作用至关重要它是一个数据缓冲区。想象一下如果数据生产的速度时快时慢消费的速度也不稳定没有缓冲的话管道很容易就“堵住”或者“断流”。FIFO 在这里起到了流量平滑和时钟域隔离如果两端时钟不同的作用。而 Interconnect互联则像一个智能路由器负责把数据从 FIFO 的出口可靠地传输到下一个目的模块。我们这次实验用最简单的点对点连接即可。最后一个模块是data_unpacked数据解包器。它是管道的终点也是数据的消费者。它的任务就是把打包好的16位数据拆开还原出原始的8位数据流。同样它作为接收方Slave需要根据 AXI-Stream 协议在能够接收数据时拉高s_ready信号并检查上游的s_valid信号。当两者同时有效时它才把数据读进来。整个管道的 Block Design 连接起来数据就从左流到右完成了一次完整的旅程。下面我们就深入每个模块看看代码怎么写信号怎么接。3. 模块设计详解手把手编写核心代码3.1 数据源泉data_gen 模块设计这个模块的设计追求极致的简单和可靠。它的功能很纯粹在复位释放后每个时钟周期产生一个新的数据直到达到预定数量。module datagen ( input wire sys_clk, // 系统时钟 input wire sys_rst_n, // 低电平有效的异步复位 output wire [7:0] data_string, // 8位数据输出 output reg done // 数据生成完成标志 ); reg [7:0] data; // 内部数据寄存器 reg [7:0] counter; // 计数器用于控制生成数量 always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin // 复位时一切归零 counter 8h0; data 8h0; done 1b0; end else if (done 1b0) begin // 只有当未完成时才继续生成 // 计数器到1270x7F后归零否则加1 counter (counter 8h7f) ? 8h0 : counter 1; // 数据在计数器归零时归零否则递增这里只是示例可以是任何序列 data (counter 8h7f) ? 8h0 : data 1; // 当计数器计满127时拉高完成信号 done (counter 8h7f) ? 1b1 : 1b0; end // 如果 done 已经是1则保持状态等待可能的外部复位 end // 将内部寄存器的值连续输出 assign data_string data; endmodule我在这里踩过一个小坑最初我把done信号的判断逻辑写得比较复杂导致它提前拉高。后来想明白了对于这种状态明确的模块done信号就应该严格由计数器counter的溢出条件来驱动这样最清晰也最不容易出错。这个模块不涉及 AXI-Stream 协议它只是一个“自私”的数据源所以接口里没有ready信号。这意味着如果下游堵塞了它产生的数据会被直接丢弃。在实际项目中我们通常会给数据源也加上流控但作为实验起点这样设计更容易理解。3.2 握手起点data_packed 模块设计从这里开始事情变得有趣了。打包模块是第一个需要遵循 AXI-Stream 协议的模块。它需要接收上游的原始数据进行打包我们这里简单地将低8位放数据高8位补零然后尝试发送给下游。valid和ready的握手舞蹈就在这里上演。module DataPacker ( input wire sys_clk, input wire sys_rst_n, input wire [7:0] DP_data_string, // 来自 data_gen 的数据 input wire DP_done, // 来自 data_gen 的完成信号 input wire m_ready, // 来自下游FIFO的 ready 信号 output reg [15:0] out_packed_data, // 打包后的16位输出 output reg m_valid // 输出给下游的 valid 信号 ); reg [15:0] packed_data; // 内部打包数据寄存器 // 第一个 always 块负责生成有效的打包数据和 valid 信号 always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin packed_data 16h0; m_valid 1b0; end else if (!DP_done) begin // 上游数据生成未完成持续打包 // 将输入数据放在低8位高8位补零 packed_data {DP_data_string, 8h00}; // 只要上游有数据在产生我们就认为有有效数据可发送 m_valid 1b1; end else begin // 上游数据生成已结束停止打包valid 拉低 packed_data 16h0; m_valid 1b0; end end // 第二个 always 块负责在握手成功时将数据输出 always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin out_packed_data 16h0; end else if (m_ready) begin // 关键只有当下游的 ready 信号有效时才更新输出端口的数据 // 这意味着数据在握手成功的那个时钟沿被下游采样 out_packed_data packed_data; end else begin // 如果下游没准备好输出可以保持也可以输出零。这里选择输出零以更清晰。 out_packed_data 16h0; end end endmodule这个模块有两个关键点。第一m_valid信号的生成逻辑。它依赖于上游的DP_done信号。只要上游还在工作m_valid就一直为高表示“我这里有数据”。这是一种比较简单的设计假设打包操作没有延迟。第二也是最核心的一点数据输出 (out_packed_data) 的更新时机。请注意它不是在第一个 always 块里直接赋值而是在第二个 always 块里判断m_ready信号有效时才更新。这严格遵循了 AXI-Stream 协议数据内容在valid和ready同时为高的时钟上升沿被成功传输。在这个设计中packed_data寄存器在第一个 always 块准备好而out_packed_data端口则在握手条件满足时将准备好的数据推送出去。这种分离的设计使得握手逻辑非常清晰。4. 协议核心深入理解 Valid/Ready 握手前面我们反复提到valid和ready现在就来彻底搞懂它。你把数据发送方想象成快递员接收方是收件人。valid就是快递员举着包裹喊“你的包裹到了”数据已准备好。ready就是收件人打开门伸出手说“给我吧”我已准备好接收。规则非常简单只有当快递员举着包裹valid1并且收件人伸着手ready1的那一刻包裹才完成交接数据传输成功。这个交接动作发生在时钟的上升沿。在 Verilog 代码里我们通常这样描述握手成功的条件if (m_valid m_ready) begin ... end。那么这两个信号谁先谁后呢协议对此非常灵活这也就产生了三种典型情况Valid 先来Ready 后到这是最常见的情况。发送方先准备好数据拉高valid然后等待接收方。当接收方后续拉高ready时在下一个时钟上升沿数据被传输。valid信号可以在数据传完后继续保持也可以拉低。Ready 先来Valid 后到接收方很积极提前就拉高了ready表示“我随时可以收”。当发送方数据准备好拉高valid的瞬间因为ready已经在了所以数据在同一个时钟沿就被立刻传输。这种模式延迟最低。同时到来发送方拉高valid和接收方拉高ready发生在同一个时钟周期数据在该周期立刻传输。在 VIVADO 的 AXI-Stream IP 核比如 FIFO中Slave 端的ready信号行为是可以配置的。我实测下来很多时候它默认或配置为“常高”也就是上面第二种情况。这有什么好处呢好处是一旦发送方有数据可以毫无延迟地立刻送入 FIFO最大限度地避免发送方因为等待而可能造成的数据丢失或时序问题。当然这要求接收方这里是 FIFO必须有足够的缓冲空间。在我们的打包模块代码中第二个 always 块就是为这种情况准备的只要m_ready常高那么packed_data寄存器里的值会在下一个时钟周期立刻出现在out_packed_data端口上并等待被 FIFO 采样。理解了这个握手机制你就掌握了 AXI-Stream 的命脉。其他信号如tlast数据包结尾标志、tkeep字节有效指示都是在握手成功的基础上附带传递的辅助信息。比如tlast拉高表示当前传输的数据是某个包的最后一个数据。这些信号让 AXI-Stream 不仅能传流数据还能传有结构的数据包。5. 系统集成与调试在 VIVADO 中连接一切设计好各个模块后我们进入 VIVADO 的图形化界面进行系统集成。这一步就像搭积木但比积木更需要细心。首先将我们编写的data_gen和DataPacker模块封装成 IP 核或者直接以 RTL 模块的形式添加到 Block Design 中。然后从 IP Catalog 中搜索并添加AXI4-Stream Data FIFO和AXI4-Stream Interconnect。FIFO 的深度需要根据数据速率和时钟关系来设置实验环境下设个 1024 或 2048 都行。Interconnect 我们选择最简单的“1个 Master 到 1个 Slave”模式。连接的时候要特别注意信号流向data_gen的data_string和done连接到DataPacker的对应输入。DataPacker的out_packed_data和m_valid连接到FIFO 的 S_AXIS 接口。DataPacker的m_ready则连接FIFO 的 S_AXIS_tready。FIFO 的 M_AXIS 接口连接到 Interconnect 的 S00_AXIS 接口。Interconnect 的 M00_AXIS 接口连接到我们后续要写的data_unpacked模块。最后把所有的时钟sys_clk和复位sys_rst_n信号分别连接到一个时钟发生器 IP 和处理器系统复位 IP 上确保整个系统有统一的时钟和复位源。连接完成后点击“Validate Design”进行自动检查。这里往往就是踩坑的开始。一个非常常见的错误就是接口属性不匹配比如我上次就遇到了下面这个报错[BD 41-237] Bus Interface property FREQ_HZ does not match between /axis_interconnect_0/s00_couplers/s00_data_fifo/S_AXIS(100000000) and /axis_interconnect_0/s00_couplers/s00_regslice/M_AXIS(10000000)这个错误说的是在 Interconnect 内部FIFO 的从接口和内部一个寄存器切片regslice的主接口它们的时钟频率属性FREQ_HZ不一致一个是1亿一个是1千万。这通常是因为我们在连接时钟时有些 IP 的时钟端口没有正确关联上时钟网络的约束。我查了很多资料最后在 Xilinx 论坛找到一个靠谱的解决方法在 Block Design 中找到出问题的时钟信号线把它删掉然后从时钟源重新拉一根线连接过去。VIVADO 在重新连接时会自动进行正确的属性推断和关联。这个方法虽然看起来有点“土”但实测非常有效。另一个我还没完全解决的问题是 Interconnect 的 Master 端输出位宽有时会被莫名其妙地锁定为1而不是自动匹配 Slave 端的位宽。如果你也遇到这个问题可以尝试检查 IP 的配置页面看看是否有强制位宽的选项或者一个更直接的办法是放弃使用 Interconnect对于简单的点对点连接直接用一个 AXI-Stream Register Slice IP 来替代。这个 IP 非常轻量几乎就是一个打拍子的流水线寄存器既能满足协议要求又能改善时序位宽设置也灵活在很多场景下比 Interconnect 更简单好用。6. 解包与验证完成数据闭环并上板测试数据经过千山万水终于流到了终点站——解包模块data_unpacked。这个模块是握手协议中的接收方Slave它的行为模式与发送方是对称的。module DataUnpacker ( input wire sys_clk, input wire sys_rst_n, // AXI-Stream Slave 接口 input wire [15:0] s_axis_tdata, input wire s_axis_tvalid, output reg s_axis_tready, // 用户侧接口 output reg [7:0] unpacked_data, output reg data_valid ); // 作为 Slave我们通常希望尽可能快地接收数据所以让 tready 常高 always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin s_axis_tready 1b0; end else begin s_axis_tready 1b1; // 常高表示随时准备接收 end end // 当握手成功时进行解包操作 always (posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin unpacked_data 8h0; data_valid 1b0; end else if (s_axis_tvalid s_axis_tready) begin // 握手成功取低8位作为解包后的数据 unpacked_data s_axis_tdata[7:0]; data_valid 1b1; // 通知用户侧数据有效 end else begin data_valid 1b0; // 无数据传输时有效信号拉低 end end endmodule这个模块的设计体现了接收方的典型思路s_axis_tready常高积极接收。当s_axis_tvalid有效时握手在同一个周期内完成数据被存入unpacked_data寄存器同时拉高一个data_valid脉冲供模块外部的逻辑使用。这样就完成了从16位打包数据到8位原始数据的还原。整个系统集成完毕后我们需要进行仿真和上板验证。在仿真中你可以清晰地看到valid和ready信号的跳变以及数据在它们同时为高时的传输时刻。我建议你使用 VIVADO 自带的仿真器编写一个简单的 testbench给系统施加复位然后观察data_gen的done信号、各个接口的握手信号以及最终unpacked_data是否正确。如果一切顺利数据应该能完整地走完整个流程。上板测试时最直观的方法就是使用ILA集成逻辑分析仪IP 核。把关键信号比如data_gen的data_string、DataPacker的m_valid/m_ready、out_packed_data、以及DataUnpacker的unpacked_data和data_valid都添加到 ILA 的观察窗口中。然后编译生成比特流下载到 FPGA 开发板上。触发运行后你就能在 VIVADO 的硬件管理器中看到真实的波形图。亲眼看到数据按照你设计的协议在信号线的指挥下有序流动那种感觉是非常棒的。这也是调试复杂数据流问题最直接有效的手段任何协议上的理解偏差在波形图前都会无所遁形。通过这个从设计到验证的完整闭环你对 AXI-Stream 数据流控制的掌握就从理论真正落到了实地。