上周调一块ZYNQ开发板的采集链路,ADC在25MHz像素时钟下持续输出数据,PS端的DDR控制器跑在150MHz,两侧一握手就出问题——偶发错数、偶尔丢帧,抓ILA一看,问题集中在跨时钟域数据同步上。这类问题在ZYNQ开发里太常见了,特别是摄像机接口、高速ADC采集、多路串口转发这类场景。今天我把这次用AXI-stream FIFO解决跨时钟域数据同步的完整过程和代码整理出来。
这篇内容特别适合正在做ZYNQ数据采集、视频图像传输、或者PL与PS之间高速数据交换的开发者。如果你刚接触AXI-stream协议,文章也会从协议握手的基本概念开始讲。我把实际工程中用到的完整Verilog代码、FIFO深度计算方法、常见调试坑都放在后面,可以直接抄作业做参考。
1. 跨时钟域问题的本质与FIFO选型思路
1.1 为什么单比特同步方案在多比特数据面前失效
在处理跨时钟域问题时,很多初学者第一反应是用两级寄存器同步一下。这个方案对单比特信号,比如一个控制标志、一个使能信号,确实管用。两级寄存器能把亚稳态出现的概率压低到一个工程上可接受的水平。但当你需要在两个时钟域之间传递一组32位或者更宽的数据总线时,这个方案就完全行不通了。
原因很简单:一组多比特信号的每一个bit到达目标时钟域采样寄存器的时刻不可能完全一致。布线有长短差异、触发器建立保持时间有工艺偏差,哪怕你把所有bit同时从源时钟域打出去,目标时钟域采到的仍然可能是"这个bit已经变了、那个bit还没变"的中间值。这样读出来的数据就是个脏数据。实际现象就是偶发性错数,有时还是随机的、很难稳定复现的那种。
那退一步,每个bit都分别做两级同步行不行?也不行。同步后的各个bit相当于被"随机延迟"了0到2个目标时钟周期,数据总线的一致性彻底被打破,甚至可能采到完全无效的组合。所以在ZYNQ这种有多个时钟域的实际工程里,跨时钟域批量传数据的标准解法就是FIFO。
1.2 同步FIFO和异步FIFO到底该怎么选
先建立一个基本概念:按读写时钟的关系,FIFO分两种。同步FIFO的读时钟和写时钟是同一个时钟,信号在同一个时钟沿驱动和采样,逻辑简单,资源消耗少。异步FIFO的读写两侧各有自己的时钟,只要完成跨时钟域传递,内部必须做多比特指针同步处理。ZYNQ里专门讲AXI-stream FIFO,本质上就是AXI4-Stream接口的异步FIFO封装。
同步FIFO和异步FIFO的选择,只有一个判断标准:读写两侧是不是同一个时钟源。同一个时钟树下的分频、倍频,比如用MMCM/PLL出来的同源时钟,都可以当同步FIFO处理。真正的外部独立时钟,比如ADC的像素时钟、SDI视频的解串时钟、UART的波特率时钟,哪怕是名义上频率完全相同,只要源不同,就必须走异步FIFO。
这里有一个我踩过坑的细节:两个频率都是100MHz的独立晶振,互相之间也会有ppm级的频偏,长时间运行后累积的时钟周期差会把FIFO拖到溢出或者读空。所以只要时钟源不同,就老老实实走异步方案。
1.3 AXI-stream FIFO在ZYNQ体系里的定位
AXI-stream是ARM AMBA协议家族里专门为高速数据流设计的接口规范。它跟AXI4-Lite和AXI4-Full最大的区别是没有地址通道,数据是纯粹的流式传输。你可以把AXI-stream想象成一条传送带,数据从源头一直往前流,中间可以有FIFO暂存、可以拆分合并、可以做位宽转换。
在ZYNQ工程里,AXI-stream FIFO通常会出现在这些位置:PL侧采集的ADC数据进来后,先放进AXI-stream FIFO,再由DMA搬运到PS侧DDR;或者PS侧DMA发出来的数据先过FIFO做跨时钟域,再送到低速外设;又或者是两个PL子系统之间时钟域不同,用FIFO做连接桥梁。Xilinx官方提供的AXI4-Stream Data FIFO IP核支持可配置的数据位宽、FIFO深度、读写两侧独立时钟,非常适合这种场景。
和直接调用Xilinx FIFO Generator IP(也就是传统意义上的异步FIFO)相比,AXI-stream版本最大的好处是接口标准统一。后续接到AXI DMA、AXI Datamover这些IP时,不需要额外做协议转换逻辑,代码复用性高。如果未来需要把数据通路升级成多通道、加AXI寄存器切片,AXI-stream的生态也更完善。
2. AXI-stream FIFO IP核原理拆解
2.1 AXI-stream协议握手的四个关键信号
在用AXI-stream FIFO之前,必须先把AXI-stream的握手规则吃透。这个协议的核心是tdata、tvalid、tready、tlast这四个信号。
tdata是数据总线,宽度可配置为8、16、32、64等。tvalid是发送方拉高的有效标志,表示当前tdata上的数据有效。tready是接收方拉高的接收就绪标志,表示当前可以接收数据。tlast表示当前传输的是包的最后一个数据,用于标记帧边界或者包尾。传输只发生在tvalid和tready同时为高的时钟周期,握手成功,数据才算真正被接收。
初学者最容易犯的错误是只看tvalid,不看tready。发送方一股脑把数据推到总线上,接收方还没准备好,数据就丢了。AXI-stream的哲学是:谁都不能单方面决定数据是否传输成功,必须双方共同确认。你把这个规则想成两人交接货,一个人说"我这里有货"(tvalid),另一个人说"我准备好了"(tready),两边都点头了货物才算移交。
2.2 内部结构:读写指针、格雷码与亚稳态处理
AXI-stream FIFO内部的核心是一个双端口RAM,外加读指针、写指针和状态逻辑。写时钟域的写指针负责往RAM里写数据,读时钟域的读指针负责从RAM里读出数据。两侧完全独立,互不干扰。
关键难点在于空满判断。读侧想知道FIFO是不是空了,需要比较读指针和写指针;写侧想知道FIFO是不是满了,也需要比较这两个指针。但读指针在写时钟域里属于异步信号,不能直接比较。这就需要用两级同步寄存器把另一侧的指针同步过来,同步的过程又无法避免延迟,所以异步FIFO的满信号和空信号天然存在不确定性。
具体实现时,Xilinx IP核内部用的是格雷码指针。格雷码的特点是相邻两个数值之间只有1个bit发生变化,这样多比特指针穿过时钟域时,即使在边界处被采到亚稳态,造成的误差最多也只有1个数。再配合两级寄存器同步,可以把亚稳态导致的功能错误概率压到极低。如果你以后自己手写异步FIFO,也务必采用格雷码方案,二进制指针直接跨时钟域是绝对不行的。
超过FIFO容量还继续写就是溢出,超过容量读就是下溢,这两个都是工程中必须通过握手信号严格规避的状态。
2.3 FIFO深度怎么定:一个实际计算例子
FIFO深度是最常被问到的参数。不是拍脑袋定的,工程上要根据两端的传输速率、突发长度和容忍延迟来算。
假设采集系统中,写时钟是25MHz ADC像素时钟,读时钟是100MHz,数据位宽32bit。ADC一帧图像的有效数据是1920×1080 = 2,073,600个像素,但行与行之间有消隐期,不会连续不断输出。最坏情况是像素数据在有效行内连续输出,消隐期内ADC不输出数据。
有效行1920个像素,在25MHz下耗时76.8微秒。读侧在100MHz时钟下,如果DMA能够持续接收,那么这76.8微秒内理论上最多可以从FIFO读走7680个数据。但写侧只有1920个数据进来,所以最终读侧会先把FIFO里预存的数据读走,然后在某个时刻读空。如果我们要求DMA永远不读空,就需要在消隐期内把下一个小包的数据填进去。
换个更直白的思路来算。假设写侧突发写入B个数据,单个数据位宽为W字节,读侧时钟频率为f_read,写侧最大写入频率为f_write。最坏情况下,B个数据全部连写完成需要B / f_write秒。在这段时间里,读侧最多读走B × f_write / f_read个数据。那么FIFO容量至少需要B - B × f_write / f_read = B × (1 - f_write / f_read)个数据,再乘以一个安全系数。
举个例子,写侧突发128个数,频率25MHz,读侧100MHz,那么FIFO深度至少128 × (1 - 25/100) = 96个数。考虑时钟频偏、总线仲裁等待等因素,一般取1.5到2倍裕量,再取2的幂次,最终定成256比较稳妥。FIFO太深浪费Block RAM资源,太浅则容易溢出。定完深度之后,最好用最坏情况的仿真去验证一次。
提示:AXI-stream FIFO的IP核配置界面里有一个Read Data FIFO Depth字段,它决定了读侧数据缓存的大小。很多开发者只关注总FIFO深度,忽略了读侧缓存,结果在高频读、低频写场景下仍然出现丢数据,这个参数也要一起考虑。
3. Vivado配置与完整代码实现
3.1 IP核配置过程:一步步设置AXI-stream FIFO
在Vivado中创建AXI-stream FIFO很简单。打开IP Catalog,搜索"AXI4-Stream Data FIFO",双击打开配置界面。我这里用的是Vivado 2023.2版本,界面变化不大,大家按实际版本操作即可。
第一个要配置的是FIFO接口参数。Component Name建议起一个有意义的名字,比如axis_fifo_32x256,一看就知道位宽32、深度256。FIFO width设置为32,与ADC采样数据的位宽一致。Fifo Depth我这里设置为256,具体依据上一节的计算。Enable TLAST选择框勾上,因为我们传输的帧数据需要标记结尾。Enable TREADY也勾上,否则读侧只能盲读。Enable AXI-Slave Interface和Enable AXI-Master Interface都要勾选,一个用于写、一个用于读。
第二个需要关注的是读时钟和写时钟的独立配置。AXI-stream FIFO的s_axis_aclk是写侧时钟,m_axis_aclk是读侧时钟。在Vivado的IP配置里,这两个时钟端口都会暴露出来,你可以在顶层模块里分别接到对应的时钟域。需要注意,IP核内部跨时钟域同步用的就是这两个时钟,所以在例化时务必按实际工程来接。
第三个配置项是关于寄存切片(Register Slice)的。如果勾选Enable Register Slice选项,相当于在FIFO输出路径上多插了一拍寄存器,有助于缓解布线拥塞、提高时序收敛性,但代价是多一个周期的读延迟。在时钟频率较高或者跨时钟域路径较长时,可以尝试开启,一般不会有太大影响。
配置完成后,点击OK,Vivado会自动生成IP核的例化模板。在Sources窗口右键点击生成的IP,选择Instantiation Template可以查看Verilog模板,方便之后复制到顶层文件引用。
3.2 PL端发送模块代码:ADC像素时钟域的数据写入
发送模块的功能是把写时钟域的数据源(比如ADC采样数据)写入AXI-stream FIFO。下面这段代码是实际工程里简化的发送模块,可以从顶层调用:
module adc_axis_writer #( parameter DATA_WIDTH = 32 )( input wire adc_clk, // ADC像素时钟 input wire rst_n, // 低有效复位 // 数据源接口,来自ADC模块 input wire [DATA_WIDTH-1:0] adc_data, input wire adc_data_valid, // ADC数据有效标志 // AXI-stream 接口,接FIFO的写侧 output wire [DATA_WIDTH-1:0] m_axis_tdata, output wire m_axis_tvalid, input wire m_axis_tready, output wire m_axis_tlast, // FIFO状态反馈 input wire fifo_almost_full ); // 帧计数器,每FRAME_LEN个数据拉一次tlast localparam FRAME_LEN = 12'd1920; // 一行的像素点数,按实际调整 reg [11:0] frame_cnt; // 数据通路:有数据且FIFO未满时就拉高tvalid assign m_axis_tdata = adc_data; assign m_axis_tvalid = adc_data_valid & ~fifo_almost_full; // 当该拍握手成功且是最后一个数据时,置位tlast assign m_axis_tlast = (frame_cnt == FRAME_LEN - 1) & m_axis_tvalid & m_axis_tready; always @(posedge adc_clk or negedge rst_n) begin if (!rst_n) begin frame_cnt <= 12'b0; end else if (m_axis_tvalid && m_axis_tready) begin if (frame_cnt == FRAME_LEN - 1) frame_cnt <= 12'b0; else frame_cnt <= frame_cnt + 1'b1; end end endmodule这段代码的关键在于tvalid的控制。我用adc_data_valid表示ADC模块给出的数据有效标志,但没有直接用这个信号去驱动tvalid,而是额外做了~fifo_almost_full的保护。这样做是为了防止FIFO快满时继续写入导致溢出。很多初学者忽略这个细节,导致数据丢失,调试时又很难定位。
tlast的生成逻辑是:当当前计数等于FRAME_LEN - 1时,说明这一帧数据已经到最后一个,需要在握手成功的拍子上拉高tlast。注意这里tlast必须和最后一个数据在同一拍出现,不能提前也不能延后。用组合逻辑直接根据frame_cnt、tvalid、tready计算,不会破坏AXI握手时序。
3.3 PL端接收模块代码:系统时钟域的主动读取
接收模块挂在AXI-stream FIFO的读侧。这里我使用了一个主动读取的逻辑,系统时钟域的下游模块,比如DMA控制器,通过s_axis_tready反向压力控制FIFO的读出。
module axis_fifo_reader #( parameter DATA_WIDTH = 32 )( input wire sys_clk, // 系统时钟,比如150MHz input wire rst_n, // 下游模块请求接口 input wire downstream_ready, // 下游是否准备好接收数据 output wire [DATA_WIDTH-1:0] read_data, output wire read_valid, // AXI-stream 接口,接FIFO的读侧 input wire [DATA_WIDTH-1:0] s_axis_tdata, input wire s_axis_tvalid, output reg s_axis_tready, input wire s_axis_tlast ); // 用一个标志记录当前是否处于接收状态 reg receiving; // 只有当FIFO有有效数据,且下游准备好了,并且当前不在跨包间隙时才允许握手 always @(*) begin s_axis_tready = downstream_ready & ~receiving; end always @(posedge sys_clk or negedge rst_n) begin if (!rst_n) begin receiving <= 1'b0; end else if (s_axis_tvalid && s_axis_tready) begin if (s_axis_tlast) receiving <= 1'b1; // 收到包尾后进入接收完成状态 end else if (receiving) begin receiving <= 1'b0; // 等待下游重新发起请求 end end assign read_data = s_axis_tdata; assign read_valid = s_axis_tvalid & s_axis_tready; endmodule要注意,这段代码里receiving的逻辑并不是传统意义上的总线控制寄存器,它是我用来模拟"当前数据包是否正在被处理"的状态。实际工程里如果接DMA或者PL端的搬移引擎,通常直接把s_axis_tready跟DMA的接收使能相连,不需要这个状态机。上面代码的价值在于演示了tready反向背压的控制思路。
如果下游模块处理能力跟不上,只需要拉低downstream_ready,FIFO读侧自然暂停。AXI-stream的背压机制就体现在这里,读侧不拉tready,写侧写满了自然停。数据不会丢,只是吞吐量下降。
3.4 顶层例化:把FIFO接入工程
顶层模块要做的事情就是把ADC写入模块、AXI-stream FIFO IP核、系统时钟域读取模块串起来。Vivado生成的IP核例化模板里端口名可能带s_axis/m_axis前缀,这块需要根据实际IP核名称去对照。
假设IP核名字是axis_fifo_32x256,顶层逻辑大致如下:
module top_cross_domain_bridge ( input wire adc_clk, input wire [31:0] adc_data, input wire adc_data_valid, input wire sys_clk, input wire rst_n, output wire [31:0] data_out, output wire data_out_valid ); wire [31:0] fifo_tdata; wire fifo_tvalid; wire fifo_tready; wire fifo_tlast; wire fifo_prog_full; // ADC时钟域写入 adc_axis_writer #( .DATA_WIDTH(32) ) u_writer ( .adc_clk (adc_clk), .rst_n (rst_n), .adc_data (adc_data), .adc_data_valid (adc_data_valid), .m_axis_tdata (fifo_tdata), .m_axis_tvalid (fifo_tvalid), .m_axis_tready (fifo_tready), .m_axis_tlast (fifo_tlast), .fifo_almost_full (fifo_prog_full) ); // AXI-stream FIFO IP核例化 axis_fifo_32x256 u_fifo ( .s_axis_aclk (adc_clk), .s_axis_aresetn (rst_n), .s_axis_tdata (fifo_tdata), .s_axis_tvalid (fifo_tvalid), .s_axis_tready (fifo_tready), .s_axis_tlast (fifo_tlast), .m_axis_aclk (sys_clk), .m_axis_aresetn (rst_n), .m_axis_tdata (fifo_rd_tdata), .m_axis_tvalid (fifo_rd_tvalid), .m_axis_tready (fifo_rd_tready), .m_axis_tlast (fifo_rd_tlast), .almost_full (fifo_prog_full) ); // 系统时钟域读取 axis_fifo_reader #( .DATA_WIDTH(32) ) u_reader ( .sys_clk (sys_clk), .rst_n (rst_n), .downstream_ready (1'b1), // 这里简化为始终接收 .read_data (data_out), .read_valid (data_out_valid), .s_axis_tdata (fifo_rd_tdata), .s_axis_tvalid (fifo_rd_tvalid), .s_axis_tready (fifo_rd_tready), .s_axis_tlast (fifo_rd_tlast) ); endmodule这个例化模板里的almost_full信号,在AXI4-Stream Data FIFO IP核中可能叫prog_full或者almost_full,具体看配置界面勾选了哪些状态端口。建议把FIFO的prog_full、prog_empty引到系统的状态寄存器里,方便PS端通过AXI-Lite读取,就能时刻知道FIFO水位。
如果你用的是Xilinx传统的FIFO Generator IP,也可以选择Axis接口类型,它的配置方式稍有不同,但接口定义和这里展示的类似。有些旧工程的IP版本里还区分s_axis和m_axis的tkeep、tstrb信号,不需要的时候可以把它们忽略掉。
3.5 基于Petalinux的PS端读取思路
跨时钟域FIFO搭好之后,PL侧的难点就解决了大半。但数据最终是要给PS侧使用的,这里顺带提一下Petalinux部署的思路。
PL侧的bitstream可以通过Vivado导出hdf,然后用Petalinux 2025.1生成设备树和启动镜像。启动镜像通常需要boot.bin、boot.scr、image.ub文件。boot.bin由FSBL、bitstream和设备树合并生成,boot.scr是U-Boot脚本,image.ub是内核和根文件系统的打包镜像。制作SD卡启动时,把这三个文件放到FAT分区,设置好U-Boot的启动参数就能加载。
在Linux里访问这个AXI-stream FIFO的数据,可以选择两种常见方案。一种是通过AXI DMA把FIFO直接搬运到DDR,用户态程序通过mmap读取DDR区域,这种方案吞吐高、延迟低。另一种是简单粗暴地直接把FIFO的读侧映射成内存区的字符设备,每次open后read读一定长度。前者适合大流量,后者适合小数据包调试。
如果你只是简单验证数据通路,我建议先用第二种方案跑通全链路,确认跨时钟域没有丢数,再迁移到DMA方案。这样排查问题的时候不会多一个不确定因素。
4. 常见问题与排查技巧实录
4.1 踩坑记录一:s_axis_tready一直拉不高
我刚开始把IP核接进工程后,在ILA里观察到一个奇怪现象:写侧的tvalid很高,但tready一直不稳定,有时直接是低电平。FIFO的数据根本写不进去。查了半天才发现问题是复位时序。
AXI-stream接口的复位信号是低有效,也就是s_axis_aresetn和m_axis_aresetn必须保持足够长时间的低电平,确保内部指针和状态机彻底复位。如果复位释放太快,FIFO内部的格雷码指针还没有完全同步,就会导致tready异常。我的工程里PL复位模块的复位释放时间设为100微秒,后来改成200微秒后问题消失。而读侧如果m_axis_aresetn没有正确拉低,读指针处在未知状态,同样会导致tready不可用。
另一个容易忽略的是,IP核的s_axis_aclk和m_axis_aclk两侧时钟如果有相位差或者频率差,复位释放瞬间的跨时钟域同步也需要时间。如果你用PS的复位输出直接接IP核的aresetn,复位释放时刻刚好落在读时钟的上升沿附近,有概率出现指针同步竞争。稳妥的做法是用Xilinx复位生成的IP(Processor System Reset)产生peripheral_aresetn,并确保复位信号在两侧时钟域都做同步。
4.2 踩坑记录二:FIFO溢出背后的时钟频偏问题
有一块板子测试时表现一切正常,但连续跑了一个小时后,偶尔会出现某一帧数据明显错误。一开始怀疑是ADC的配置有问题,后来用ILA把almost_full信号触发下来一看,FIFO确实出现了接近满的状态。
原因在于两个时钟名义上都是25MHz,但板载晶振和ADC的像素时钟来自两个完全独立的振荡器,实际频率之间存在几百ppm的偏差。短时间看不出影响,时间一长,累计误差会让FIFO的读写速率差变得不可忽略。FIFO深度不够的情况下,最终会溢出。
解决思路有几种。第一是增大FIFO深度,给频偏一个更大的容纳空间。第二种是控制写侧速率,比如在adc_data_valid到来时做节流,人为插入空闲周期,让平均写入速率严格小于理论速率。第三种是从系统层面做时钟同步,比如将ADC的主时钟配置成由FPGA内部MMCM产生,而不是板载独立晶振——这样读写两侧时钟同源,频偏问题自然消失。
实际工程中,AD采样时钟用FPGA内部PLL生成是比较常见做法,因为采样时钟和系统时钟本来就同源,天然避免了频偏问题。但如果ADC是外部独立时钟,那FIFO深度就得留足裕量。
4.3 踩坑记录三:tlast没有正确携带引发DMA断帧
还有一次问题是DMA搬运数据时一直报"insufficient data"错误,查了很长时间,最后发现是FIFO读侧没有把tlast信号传递到DMA的s_axis接口。
AXI4-Stream Data FIFO内部默认是支持TLAST信号的,但这个信号必须在实际传输的最后一个数据上拉高。如果写侧代码里tlast的时序不对,比如提前一拍或者延后一拍,FIFO虽然能正常传数据,但DMA无法正确识别帧边界。
调试这类问题,我习惯在FIFO读侧加一个ILA,同时抓tdata、tvalid、tready、tlast。触发条件设成任一遍缘变化,然后检查tlast和最后一个数据是否在同一拍。如果发现错位,优先检查写侧的帧计数器逻辑。tlast必须与对应数据的握手周期同步,不能有偏差。
4.4 完整调试流程参考
第一次搭建AXI-stream FIFO数据通路,建议按下面的顺序来验证:
第一步,先做纯PL仿真。用Verilog testbench模拟ADC在25MHz时钟下输出连续数据,检查FIFO两侧的时序和最终输出数据是否一一对应。仿真时不加DMA、不加PS交互,排除其他因素干扰。
第二步,用ILA实际抓FIFO读侧的信号。如果方便,生成一个仿真时用到的连续递增数据,送到FIFO写侧。读侧读到的应该是连续递增的序列,任何跳变都说明FIFO配置或者握手逻辑有问题。
第三步,接入真实数据源,对比输入输出数据内容。如果数据错位或者丢失,回头检查FIFO深度和时钟频率比。
第四步,再接入DMA或者PS侧驱动。如果这一步报错,优先确认AXI-stream总线的tlast和tready信号是否正确连接。
这个顺序能帮你把问题分层定位,不会一头扎进某个环节反复排查。
5. 实测心得与扩展建议
5.1 一套顺手好用的AXI-stream调试小技巧
ILA调试AXI-stream总线有个非常好用的触发方式:把tvalid和tready做AND之后作为触发条件。这样只要发生有效握手,ILA就会抓一拍。配合tdata总线,就能迅速判断数据内容是否符合预期。
另外一个技巧是利用递增数据模式。不管接ADC还是接DMA,调试期间先用PL侧的一个计数器生成递增数据源,比如从0开始每个时钟周期加1。这样任何丢数都能立刻从序列跳变中看出来。数据0,1,2,3,4,5如果中间蹦出一个8,说明中间丢了3个数,问题范围立刻缩小。
最后一个心得就是仿真一定不能省。以前偷懒跳过仿真直接上板调试,结果在板子上反复抓波形,时间花了十倍不止。AXI-stream的时序在Vivado XSim里很容易仿真,testbench写一次还能复用到不同的配置组合,性价比远高于上板调ILA。
5.2 后续还能往哪些方向扩展
这次的跨时钟域数据通路搭好后,后续扩展空间还很大。一个方向是把写侧从ADC扩展到更通用的数据源,比如视频解码器、多路UART聚合、以太网MAC的接收端。只要外部接口可以产生连续流数据,都能用AXI-stream FIFO作为时钟域桥梁。
另一个方向是引入AXI DMA,把FIFO读到的数据直接搬进DDR。这样PS侧Linux只需要处理内存数据,CPU占用大幅降低。DMA与FIFO之间通常还需要一个AXI Datamover IP做转换,但整体逻辑套路和我上面展示的类似。
如果你对PL侧代码不感兴趣,想完全用Linux驱动来完成数据搬运和跨时钟域处理,也可以关注Petalinux里u-dma-buf或者FPGA Manager的使用,这些工具能简化PL和PS之间的数据通路管理。但无论哪种方案,AXI-stream FIFO把数据和时钟域问题解决得越干净,上层软件就越轻松。