ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Zynq UltraScale+高速数据采集:FPGA-Linux-ARM64 DMA框架解析

Zynq UltraScale+高速数据采集:FPGA-Linux-ARM64 DMA框架解析 干过高速数据采集的兄弟都清楚项目做到最后最折磨人的往往不是FPGA里那套时序有多难收敛也不是Linux驱动有多复杂而是“前端高速数据怎么稳定、不掉点、不丢帧地搬进ARM64的内存里”。这个环节做不好前面ADC采样率再高、FPGA逻辑再漂亮数据到不了CPU手里全白搭。我去年基于Zynq UltraScale平台整理了一套面向高速数据采集的FPGA-Linux-ARM64一体化DMA搬运框架工程代号叫hs_dma_framework这篇文章把这套东西的思路、架构、代码层面的关键细节、以及踩过的坑完整梳理一遍给做软件无线电、图像传感器采集、雷达回波处理、多通道高速ADC数据记录的朋友做个参考。这套框架本质上解决的是“FPGA采样数据到ARM64内存之间最后一公里高速搬移”的问题核心由三块组成FPGA侧的AXI DMA传输链路、Linux侧的DMA驱动与零拷贝用户态接口、以及ARM64平台上的缓存一致性和中断处理策略。适合的读者包括两类一类是FPGA工程师想把设备接入Linux系统但又不想从零啃虚拟地址、MMU、DMA一致性这些操作系统概念另一类是嵌入式Linux工程师需要在ARM64处理器上接收FPGA产生的持续高速数据流且数据吞吐量在数百MB/s到数GB/s的量级。1. 为什么要把FPGA、Linux、ARM64绑在一起做采集1.1 “伪高速”采集方案的四个典型翻车点先说清楚我们面对的真实问题。高速数据采集这一个需求单独用任何单一技术路线做都会在某个维度翻车。纯FPGA裸机方案逻辑上接ADC、接DDR、做触发、做格式打包都没问题但一旦需要把数据写到机械硬盘/SSD、需要通过网络远程读取、需要支持复杂的命令配置和固件升级在纯FPGA上干活简直是一场灾难。哪怕用MicroBlaze软核跑个lwIP性能和开发效率也远不如一个成熟的Linux系统。纯Linux嵌入式方案用ARM核心直接接SPI或并口去读ADC单次转换可能还可以持续高速流式数据根本扛不住。中断触发一次进一次内核每个数据包几KB到几十KB每秒几千次中断CPU直接被打满更不用说Linux调度抖动带来的采样时间不确定性问题。FPGALinux但不用DMA的方案FPGA把数据写到某个地址CPU用readl轮询读取搬运。这类方案在数据量小时凑合到了高速率下CPU内存拷贝带宽就是瓶颈而且轮询期间CPU什么都干不了。只用Linux自带的通用驱动外加大量拷贝用户态读一次数据要经过内核缓冲区、拷贝到用户缓冲区再加上驱动的内存映射吞吐量至少打五折。hs_dma_framework的出发点就是把这三种硬件资源各自的优势组合起来FPGA负责时序确定性和高速并行处理Linux负责协议栈、存储、人机交互和生态复用ARM64提供64位地址空间、大内存容量和多核调度能力。三者通过DMA通道完成数据交接谁都不需要迁就谁。1.2 这个框架解决的三个核心矛盾第一个矛盾是“确定性”和“复杂性”的矛盾。高速采集中采样时刻、数据到达时刻必须精准模拟前端时钟抖动、LVDS信号对齐、帧同步信号触发这些都需要FPGA级别的硬件控制。但采样之后的处理比如FFT、滤波、数据存储、可视化、网络上传用软件做灵活得多。框架把两者硬性隔开硬件逻辑只管把数据打上时间戳送进DMA管道软件逻辑只管从管道里取数据职责各归各。第二个矛盾是“中断过载”和“实时响应”的矛盾。如果每个DMA包完成都触发一次中断在高速场景下CPU会因为频繁进出异常模式而大量浪费。框架采用中断合并interrupt coalescing与环形缓冲组合的方式数据量小的时候中断立即上报保证低时延数据量大的时候中断被抑制CPU通过状态查询或批量处理换取高吞吐。这个设计在Cortex-A72这种乱序执行核上效果格外明显。第三个矛盾是“设备虚拟地址”和“硬件物理地址”的矛盾。ARM64的MMU让每个用户进程活在独立虚拟地址空间里但DMA控制器操作的是物理地址。框架必须提供一个稳定的物理内存池在驱动里完成“物理地址到用户态虚拟地址”的映射而不是反复复制数据。这一层解决好了用户态程序拿到的内存就能直接和FPGA交换数据零拷贝才算真正落地。1.3 这套东西能用在哪些项目上我的实际使用中可以列一个适用场景清单都是验证过的软件无线电前端ADC采样率在几十MSPS到数GSPS之间数据流宽度从12bit到16bit不等通过JESD204B或并行LVDS进入FPGA打包后送DMA。图像传感器数据采集比如工业相机sensor输出RAW格式一行一行的像素数据经过FPGA做MIPI/LVDS接收后按帧写入内存配合VDMA能实现连续帧采集。多通道同步振动/声发射信号采集每一通道采样率不高但几十上百个通道总和起来数据量很大DMA可以按通道交织或按块交织做数据组织。激光雷达回波波形采集每个激光脉冲产生一次高速采样窗采样窗之间时间不确定恰好需要FPGA做精准触发DMA负责把每次采样窗的波形搬进Linux。如果你做的项目符合上面任何一种形态那这套框架的架构基本可以直接套用不需要从零设计。2. 整体架构拆解数据从引脚进内存到底走的是哪条路2.1 信号前端的接入方式LVDS、SPI、JESD204B到AXI-Stream硬件链路的第一段是从传感器或ADC出来的原始信号进入FPGA。这一步和DMA框架本身关系不大但决定了进入DMA管道的数据格式所以必须提一下。常见的前端接口有三类并行LVDS常见于多通道高精度ADC比如14bit/16bit的ADC每个通道一个LVDS差分对加上DCO时钟线和帧同步信号。FPGA里的IBUFDS原语把差分信号转成单端再按bit位拼接把串行数据恢复成并行word。JESD204B现在的射频采样ADC大量采用这种串行协议一条lane跑几Gbps到十几GbpsFX在逻辑里要放JESD204B物理层IP核、链路层核和传输层核。数据恢复输出是AXI-Stream格式直接能接DMA链路非常友好。SPI接口主要用在低速高精度ADC或寄存器配置场景速度一般几十MHz SPI时钟数据先落到一个小容量FIFO达到阈值后再打包发起DMA传输。不管前端是什么接口sensor产生的数据都必须被整理成一个连续的“包”的概念。这个包可以是一次触发采集的波形也可以是一帧图像也可以是固定长度的数据块。某种意义上看DMA框架不关心数据具体是什么只关心数据在哪里、有多长、什么时候准备好。2.2 FPGA内部跨时钟域与AXI-Stream数据通道设计FPGA内部需要先解决跨时钟域问题。ADC的采样时钟和DDR/PS侧参考时钟通常是不同域的而且存在频率偏差。标准的做法是ADC数据先进入一个异步FIFO用Xilinx的XPM_FIFO异步IP核即可写时钟用ADC输出时钟读时钟用AXI总线的时钟。FIFO深度要根据最大突发长度设计避免读端长时间拉低ready信号导致写端溢出。数据离开FIFO后拼接成128bit位宽对齐AXI-Stream总线这一步很关键如果ADC是16bit位宽而AXI-Stream是128bit那就需要做位宽匹配和字节对齐。更高端的做法是直接让FPGA逻辑输出一个“包头”比如每个数据块开头加一个32bit的帧头字段包含通道号、采样点数、时间戳信息。这样软件在解析数据时不用担心数据流边界问题DMA只负责把一整块内存原样搬走。2.3 总线拓扑ARM64、DDR、DMA之间的带宽怎么算很多刚接触Zynq平台的人以为DMA就是把数据从FPGA搬到DDR带宽随便就够。实际上总线带宽是硬约束设计前必须算清楚。以Zynq UltraScale为例PS侧DDR4总线通常是64bit位宽运行在2400MT/s理论带宽大约19.2GB/s。但这块带宽要被CPU访问、GPU、USB、以太网全都共享。FPGA侧通过AXI_HP接口或者S_AXI_HP从端口接进系统每个HP口的位宽是128bit综合下来典型有效带宽在2~5GB/s之间取决于读写比例和突发长度。计算需求很简单采样率乘以采样位宽得到原始数据率保留至少30%的余量同时考虑同时读写的情况。举个例子一个500MSPS的12bit ADC数据是600MB/s加上包头和填充对齐实际约700MB/s这种流量一个128bit300MHz的AXI DMA完全扛得住。但如果是四个通道同时跑总流量到2.8GB/s那就要考虑多通道DMA并行或者用更高效的packing方式减少带宽浪费。2.4 地址映射与MMU为什么要关注“物理地址连续”DMA框架里最容易让新手犯迷糊的是地址映射问题。ARM64平台CPU访问的内存是虚拟地址而DMA控制器无论是FPGA里AXI DMA还是PS侧DMA控制器只认识物理地址。Linux内核为DMA分配内存有两类方式一致性DMA内存coherent DMA用dma_alloc_coherent申请内存物理连续且不会经过CPU cacheDMA和CPU访问数据天然一致代价是分配较大的连续内存块时可能失败。流式DMA映射streaming DMA mapping先用kmalloc或页面分配器拿普通内存然后通过dma_map_single或dma_map_sg建立映射。这种方式要求软件自己处理缓存同步用dma_sync_single_for_cpu和dma_sync_single_for_device在访问前后做缓冲同步。hs_dma_framework的驱动里主通道用的是一致性DMA内存。原因很直接高速持续采集下如果每次传输都做手动缓存同步不仅代码容易出错而且同步开销会吃掉不少带宽。一致性内存的缺点——分配大块连续内存难——用预留CMA内存区解决。内核设备树里给DMA驱动预留一块固定大小的CMA区域启动时分配好驱动运行时直接从这块池子里再细分。3. 软件层驱动里DMA框架的完整落地过程3.1 用dmaengine框架还是自己写寄存器驱动Xilinx官方推荐用Linux内核的dmaengine框架来操作AXI DMA IP。dmaengine框架相当于一层统一的DMA抽象层类似Linux V4L2对摄像头设备的抽象。对开发者来说好处是可以复用大量内核基础设施completion完成量、tasklet、SG描述符机制。如果一个驱动直接裸写AXI DMA寄存器比如直接操作MM2DMA的源地址、目标地址寄存器、控制寄存器当然也可以跑但问题是代码可维护性差而且Xilinx的驱动改动升级后你要跟着手动适配。在hs_dma_framework里我最终还是用了dmaengine和Xilinx官方驱动配合稳定性明显更好。dmaengine跑一个典型读请求的流程大致是这样的先调用dma_request_channel申请一个DMA通道。为本次传输准备struct dma_async_tx_descriptor描述符用dmaengine_prep_dma_memcpy或dmaengine_prep_slave_sg构建。用dmaengine_submit把描述符提交到通道队列。调用dma_async_issue_pending触发传输。等待回调或者wait_for_completion_interruptible_timeout收到传输完成信号。关键理解是dmaengine描述的方法是一次传输的“切片”而高速采集需要无限持续的流。因此要循环准备描述符并提交形成一个描述符流水线。这就是SG模式和cyclic模式存在的意义。3.2 scatter-gather模式解决连续物理内存不足问题如果DMA传输的数据块很大比如每包512KB而系统里虽然总内存充足但连续物理内存只有256KB那一次映射就会失败。SG模式把一个大块数据分散到多个物理上不连续的页面上通过描述符表告诉DMA控制器“第一段在哪、第二段在哪、长度多少”。AXI DMA的SG模式支持最多可以一次搬运几十个不连续的片段FPGA侧只要给出第一次传输的缓冲描述符基地址IP核会自动顺着描述符链往下走。这个机制配合环形缓冲非常优雅预先建立一个循环DMA描述符列表驱动不断追加已完成的缓冲块用户态不断取走填好的缓冲块形成环形的生产者-消费者模型。hs_dma_framework的采集主通道用的就是这种模式实现下来后在大流量下几乎没有因为内存分配失败导致丢数据的情况。3.3 缓存一致性ARM64平台上的第一大坑我调试这套框架时遇到最恶心的问题是DMA写完了数据CPU读出来的却全是旧数据。核心就是ARM64处理器的cache coherence问题CPU读内存时优先命中Cache如果Cache里还留着旧的数据即使DMA已经改了DDR里的内容CPU看到的依然是旧值。用一致性DMA内存可以规避这个问题因为这类内存直接被标记为non-cacheableCPU访问它时绕过Cache直接读DDR虽然慢一点但保证正确。在Zynq UltraScale上AXI DMA的地址和普通内存地址空间无差别的访问路径加上核多了之后缓存被冲突替换的概率变大容易出现“时好时坏”的诡异bug所以排查思路上不要怀疑编译器先检查一致性属性。如果项目里必须要用流式DMA映射节省内存那必须在合适的时机调用缓存维护操作// 设备写完数据后CPU开始读之前使失效对应cache dma_sync_single_range_for_cpu(chan-dev, dma_addr, offset, size, DMA_FROM_DEVICE);注意ARM64上失效只有“失效并分配”invalidate with allocate这类操作效率比x86低所以频繁调用同步操作非常贵。这也反向论证了在高速采集场景里用一致性DMA内存是性价比更高的选择。3.4 用户态的零拷贝mmap拿到物理内存驱动内部的数据搬运次数直接决定了系统吞吐量。最优状态是FPGA写入内存一次、CPU从内存取走一次整个过程用户态程序看不到内核接口的多次拷贝。实现方法就是我们熟知的mmap零拷贝。在file_operations结构体里实现.mmap回调把之前dma_alloc_coherent分配的内存映射到用户进程地址空间。实现到一半时很多人的困惑是dma_alloc_coherent返回的是内核虚拟地址和DMA物理地址怎么拿它去做用户态映射正确姿势是用remap_pfn_range把物理页帧号pfn映射到用户vma区域static int dma_framework_mmap(struct file *fp, struct vm_area_struct *vma) { unsigned long size vma-vm_end - vma-vm_start; struct dma_fw_dev *dev fp-private_data; // 确保映射范围不大于驱动地址池大小 if (size dev-buf_size) return -EINVAL; // remap_pfn_range把物理页映射到用户空间 if (remap_pfn_range(vma, vma-vm_start, virt_to_phys(dev-buf_virt) PAGE_SHIFT, size, vma-vm_page_prot)) return -EAGAIN; return 0; }用户态拿到映射地址后配合一个简单的ioctl或者共享内存中的“写索引/读索引”就能实现类似环形缓冲区的数据读取。一个数据块填满后驱动在对应的struct dma_buf里记上完成标志用户态轮询这个标志就能取走数据。整个过程零拷贝、零系统调用开销。3.5 中断频率与吞吐的取舍中断合并和轮询模式高速数据流的吞吐优化绕不开中断处理。默认情况下每个DMA传输完成都会产生一次中断假设一次传输了4KB数据、每秒要传输10万次那么系统每秒要响应10万次中断这直接把Cortex-A72核心打到满负荷其余任务全被卡死。hs_dma_framework采用了两级策略。第一级是中断合并Xilinx AXI DMA IP内部有一个IRQ_THRESHOLD和IRQ_DELAY寄存器可以设置在多少包之后产生一次中断或者在多少时间内最多产生一次中断。第二级是程序侧的“批量轮询”当检测到中断频率超过某个阈值时驱动切换工作模式在timer或tasklet里批量检查DMA完成标志批量上报。这有点类似网卡驱动的NAPI机制。调整后实测在800MB/s的数据率下CPU占用率可以压到15%以下。4. 高速数据采集的调优与实测参数4.1 一个具体的带宽预算实例用一个我实际调过的例子说明4通道、250MSPS、16bit ADC同时采集。原始数据率是4 × 250e6 × 2 2GB/s。FPGA里每个通道各自打包加32字节包头、做128bit对齐后实际约2.1GB/s。如果单条AXI DMA跑死第二通道的DMA提交只能排队这样会形成瓶颈。设计上自然要有两条DMA通道每个通道处理2路信号理论上每路负载1.05GB/s单通道128bit300MHz的理论带宽是4.8GB/s有效率通常打七折也有3.36GB/s所以实际有余量。但注意DDR同时要接收DMA写数据、LCD显示控制器的读取请求实测最终DMA有效带宽只有大约2.5GB/s仍能满足2.1GB/s的需求。计算时的关键变量可以用这个公式所需DMA带宽 采样率 × 位宽 / 8 × 通道数 × 对齐损耗系数对齐损耗系数通常在1.05到1.2之间取决于包头和字节填充策略。规划时要求的DMA带宽至少要为这个值的1.3倍给突发访问和调度抖动留余量。4.2 双缓冲与环形缓冲连续采集不丢数据的关键DMA传输有一个固有缺陷在搬运当前缓冲区的过程中如果驱动程序还在处理之前的缓冲区新的数据没有可用的缓冲区就丢了。唯一的解法是准备至少两个缓冲区轮换——双缓冲。实际工程里我更推荐“多缓冲环形队列”缓冲区数量在4到8个之间即使用户态程序短暂调度延迟数据也有地方放。实现环形队列不复杂驱动初始化时分配N个相同大小的缓冲区全部挂到DMA完成队列上。提交数据时依次取一个空闲缓冲区交给DMADMA完成后该缓冲区加入“已满队列”用户态从“已满队列”取走数据并释放回“空闲队列”。环形队列用内核链表或者简单的数组加索引都可以。应用层的poll或read可以被阻塞在“已满队列”上这比用户态强制死等更高效。4.3 典型的寄存器配置与状态检查流程如果用Xilinx AXI DMA IPFPGA侧需要事先把寄存器配置好。在设备树里AXI DMA相关的节点通常长这样axi_dma_0: dmaa0020000 { compatible xlnx,axi-dma-1.00.a; reg 0x0 0xa0020000 0x0 0x10000; dma-channels 0x1; #dma-cells 0x1; interrupt-parent intc; interrupts 0 29 4; xlnx,addrwidth 0x40; xlnx,include-sg 0x1; };这里include-sg开启SG模式addrwidth设为0x40表示64位地址宽度为的是访问所有DDR空间。驱动中启动DMA传输时的寄存器访问顺序是有讲究的先设置源/目的地址再设置长度最后把控制寄存器里的run bit拉高。反过来操作会引发地址错乱。调试时通过查看中断状态寄存器IRQ_STATUS判断错误类型Xilinx文档里IRQ_DELAY和IRQ_THRESHOLD两个字段对应超时中断和阈值中断如果频繁出现延时中断说明数据速率低于预期或者数据源没送够数据。这些细节在官方手册里被几十页表格淹没实际操作时可以用Xilinx的devmem工具在命令行即时读寄存器确认。5. 常见问题与排查技巧实录5.1 数据错位、首包丢失——最典型的两个现场第一次把整套系统跑起来时日志里数据看起来很好但过一会儿在MATLAB里分析波形发现每个数据块开头几字节对不上时不时的还会丢包。按经验排查错位问题十有八九是AXI-Stream总线的对齐没做好。FPGA送出的stream数据必须保证tready信号在突发传输中不能拉低否则AXI DMA会认为传输断裂导致长度计数错乱。丢包问题的根源基本在缓冲区不足驱动里的环形队列数量不够DMA提交来不及时数据被FPGA FIFO溢出丢弃。解决方法是加大环形队列深度、或者把PetaLinux里的实时核跑到一个空闲CPU核心专门处理DMA中断。排查丢包还可以用状态自检在FPGA侧给每个数据包头增加一个递增序号字段上位机软件解析序号一旦发现跳号就知道在哪里丢的这比分析波形“感觉不对”高效得多。5.2 CMA分配失败与内存碎片高速采集驱动里动辄要求为整个环形队列分配64MB甚至128MB连续物理内存这在系统运行一段时间后经常失败。典型报错是dma_alloc_coherent: allocation failed。主要原因是系统里其他驱动和设备占用了足够多的低端物理内存碎片化严重。解决方案分两步。第一是设备树里提前预留CMA区域并且确保内核命令行有cma128M这类参数或者设备树的linux,cma-default属性。第二是分配时机提前在驱动probe阶段就完成内存池分配而不是等到用户打开设备时才分配。还有一点容易被忽略如果开启了CONFIG_CMA_DEBUG并开了大量调试跟踪信息性能下降明显高速传输时还可能引入时序抖动生产版本一定要关掉。5.3 中断风暴数据没丢但系统卡死有一段时间系统表现诡异数据不丢但整机响应极慢ping都卡顿。查看/proc/interrupts时发现DMA中断计数每秒钟暴涨到几十万次。这就是中断风暴。原因是我把中断合并阈值设得太小比如每个4KB块都产生中断。解决办法从驱动侧把IRQ_THRESHOLD调大比如64个块才中断一次同时中断处理函数里不要再做任何耗时的内存操作把真正的数据通知放到tasklet或workqueue里异步处理。实测中断频率从十万级降到几千级系统响应立刻恢复。5.4 没有真机也能先期验证QEMU模拟ARM64环境这个可能超出不少FPGA工程师的认知但现代Linux内核的驱动开发完全可以先跑在QEMU上。我调试驱动框架时不希望每次都启动庞大的Zynq硬件平台用QEMU模拟一个arm64 virt机器把交叉编译的内核和根文件系统跑起来然后用内存模拟的DMA字符设备先验证零拷贝路径和环形队列逻辑。具体命令大致是qemu-system-aarch64 -machine virt -cpu cortex-a72 \ -kernel Image -initrd initrd.img -append consolettyAMA0 \ -m 2G -nographic如果驱动逻辑里用错了dma_map_single之类的API在QEMU上编译运行也能暴露大部分问题只有缓存一致性这类硬件相关的bug必须真机验证。这个工作流帮我节省了大量等硬件的时间。5.5 FPGA时序问题导致DMA传输挂死还有一类问题出在FPGA侧代码本身。DMA传输挂死表现为驱动提交描述符后直接超时不产生中断。排查时通过读取AXI DMA寄存器发现控制寄存器的run bit变成了0但同时SGChannel的错误状态寄存器里出现了“Slave Error”。这类问题几乎都是AXI总线的突发长度配置不匹配导致的如果AXI DMA IP配置的最大突发长度是16而FPGA侧数据源一次送出的字节数超过了这个限制总线就会报错。对照AXI协议规范检查突发长度后把发送侧逻辑改成按固定burst size切片发送问题就消失了。6. 我的几个经验体会这类软硬结合的高速采集平台最值得的投入是把DMA这一层做成稳定的公共模块。DMA数据通路一旦验证通过上面要做协议转换、多通道分配、数据记录只是业务问题但如果DMA通路三天两头出数据错乱所有上层功能都会跟着变得不可信。hs_dma_framework这套东西我还会继续扩展下一步打算加多核并行分散处理、把DMA通道和进程绑定实现CPU亲和性以及在用户态接上类似libpcap的通用读取接口让上层应用切换成本更低。最后分享一个经验值一套稳定的DMA数据通路从FPGA逻辑到Linux驱动再到用户态验证正常情况下需要两到三周的集中调试。如果你手头有类似的高速采集项目可以参考这套架构从DMA通道的设计切入别一开始就陷在业务逻辑里先把数据通路跑得千锤百炼后面的路会好走很多。
返回列表