如何用 AXI DMA 打通 Zynq 的“任督二脉”?—— 实现 PS 与 PL 高吞吐数据传输的实战心法
在做嵌入式开发时,你是否遇到过这样的场景:PL 端的数据像洪水一样涌来,ADC、摄像头或高速接口源源不断地输出流数据,而 CPU 却忙得焦头烂额,一边要处理协议栈,一边还要抽空去读 FIFO 缓冲区?结果不仅系统卡顿,还时不时丢几帧数据。这背后的根本问题,其实是数据搬运方式太原始了。
Xilinx Zynq 平台本是为了解决这类高性能需求而生——它把 ARM 处理器(PS)和可编程逻辑(PL)集成在同一颗芯片上,理论上可以实现软硬协同、并行处理。但如果你还在用 CPU 一 byte 一 byte 地搬数据,那这套架构的优势就被彻底浪费了。
真正的破局之道,就是让硬件自己动起来。而其中最关键的那块拼图,正是AXI DMA。
为什么传统数据搬运方式走不远?
我们先来看看典型的“低效模式”长什么样:
假设你在 PL 中接了一个 12-bit、100 MSPS 的 ADC,每秒产生 150MB 原始采样数据。如果采用 CPU 轮询方式从 FIFO 读取这些数据,并写入 DDR 内存,会发生什么?
- 每次中断可能只带来几十个字节;
- CPU 不得不频繁进出中断上下文;
- 数据还没处理完,新的采样又堆满了缓冲;
- 最终要么丢数据,要么整个系统响应迟缓。
更糟的是,这种模式下CPU 成了数据通道的瓶颈,哪怕你的算法再高效也没用——因为根本拿不到完整的原始数据。
这时候你就需要一个“搬运工”,一个不需要 CPU 指挥就能自动把数据从 PL 运到 DDR 的硬件模块。这个角色,就是AXI DMA。
AXI DMA 到底是什么?它凭什么能扛大梁?
简单来说,AXI DMA 是 Xilinx 提供的一个专用 IP 核,它的使命只有一个:在不打扰 CPU 的前提下,完成 PL 和 PS 内存之间的高速数据搬移。
它基于 AMBA AXI4 协议构建,支持两种核心通道:
- MM2S(Memory Map to Stream):从内存读数据发给 PL;
- S2MM(Stream to Memory Map):从 PL 接收流数据写入内存。
这两个通道可以同时运行,形成全双工通信管道。更重要的是,它不是简单的“直通 FIFO”,而是具备完整控制能力的智能控制器。
它是怎么工作的?一个比喻帮你理解
你可以把 AXI DMA 想象成一个快递调度中心:
- PL 是发货人,不断打包送出数据包(AXI4-Stream 流);
- DDR 内存是仓库,有多个空闲货架(缓冲区);
- AXI DMA 就是那个自动分拣员,拿着一张任务清单(描述符),知道每个包裹该送到哪个货架;
- 当一批货送完,它会打个电话通知管理员(触发中断),然后继续下一批。
整个过程不需要你亲自跑腿,也不怕高峰期爆仓。
关键特性一览:不只是“快”
| 特性 | 实际意义 |
|---|---|
| ✅ 支持最大 256-beat 突发传输 | 单次 AXI 事务可达 2KB,极大减少总线开销 |
| ✅ Scatter-Gather 模式 | 可使用非连续物理内存组成大容量接收队列 |
| ✅ 双向独立通道 | MM2S 和 S2MM 可并发工作,互不影响 |
| ✅ 中断机制丰富 | 支持帧完成、延迟计数、错误等多类中断 |
| ✅ Cache 一致性兼容 | 配合 ACP/HP 端口,在带 cache 系统中安全访问 |
| ✅ 可扩展性强 | 多实例部署支持多路并行采集 |
📌 注:理论带宽计算示例——以 100MHz 时钟、64bit 数据宽度、256-beat burst 为例,峰值速率约为 $100 \times 8 \times 256 = 2.048\,\text{GB/s}$(参考 PG021 文档)。虽然实际受 DDR 延迟和总线竞争影响难以达到,但远超 CPU 轮询所能企及。
怎么用?从初始化到实战全流程拆解
第一步:初始化 AXI DMA 控制器
#include "xaxidma.h" #include "xparameters.h" XAxiDma axi_dma; int init_axi_dma() { int status; XAxiDma_Config *config; // 查找设备配置 config = XAxiDma_LookupConfig(XPAR_AXIDMA_0_DEVICE_ID); if (!config) return XST_FAILURE; // 初始化实例 status = XAxiDma_CfgInitialize(&axi_dma, config); if (status != XST_SUCCESS) return XST_FAILURE; // 若启用 Scatter-Gather,则关闭默认中断(后续按需开启) if (XAxiDma_HasSg(&axi_dma)) { XAxiDma_IntrDisable(&axi_dma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE); XAxiDma_IntrDisable(&axi_dma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA); } return XST_SUCCESS; }📌关键点提醒:
-XAxiDma_LookupConfig会根据硬件设计中的设备 ID 自动匹配寄存器基址;
-CfgInitialize完成内部状态机重置和通道探测;
- 如果后续要用中断驱动,这里只是暂时禁用,后面再注册 ISR。
第二步:启动 S2MM 接收(轮询模式入门)
适用于调试或轻量级应用:
#define BUFFER_ADDR 0x10000000UL // 物理地址起始点 #define NUM_BYTES 0x1000 // 4KB 接收长度 int start_receive_polling() { int status; // 启动一次简单传输:将来自 PL 的数据写入指定内存 status = XAxiDma_SimpleTransfer(&axi_dma, BUFFER_ADDR, NUM_BYTES, XAXIDMA_DEVICE_TO_DMA); if (status != XST_SUCCESS) return XST_FAILURE; // 轮询等待完成 while (XAxiDma_Busy(&axi_dma, XAXIDMA_DEVICE_TO_DMA)); return XST_SUCCESS; }💡注意陷阱:
-BUFFER_ADDR必须是物理地址,且对齐到 AXI 要求(通常 4-byte 或 8-byte);
- 使用Xil_DCacheInvalidateRange(BUFFER_ADDR, NUM_BYTES)在读取前使缓存失效,否则可能看到旧数据!
进阶玩法:中断 + 多缓冲循环接收(生产级推荐)
真正稳定的系统不会靠轮询活着。下面是一个典型的中断驱动框架思路:
static void s2mm_done_callback(void *cb_data) { uint32_t *done_flag = (uint32_t *)cb_data; *done_flag = 1; // 标记本次接收完成 // 此处可切换缓冲区、启动下一轮接收、唤醒处理线程等 } // 注册中断回调(通常在初始化后调用) void setup_interrupts() { XAxiDma_IntrEnable(&axi_dma, XAXIDMA_IRQ_IOC_MASK, XAXIDMA_DEVICE_TO_DMA); XAxiDma_SetCallBack(&axi_dma, XAXIDMA_RECEIVE_HANDLER, s2mm_done_callback, &recv_done); }配合 RTOS 或 Linux 字符设备驱动,你可以实现:
- 缓冲区环形队列管理;
- 用户空间 mmap 映射零拷贝访问;
- 实时优先级调度保障采集不中断。
架构怎么搭?PS 与 PL 如何高效协同?
来看一张典型的数据通路结构图:
[ADC / Sensor Logic] │ ▼ (AXI4-Stream) [AXI DMA in PL] │ ├─── MM2S ───> HP0_FPD (→ DDR via High Performance Port) └─── S2MM ←─── HP0_FPD (← DDR) │ ▼ [ARM Cortex-A9/A53] │ ▼ [Application: FFT, Encode, Send]几个关键设计选择必须明确:
1. 选 HP 还是 ACP?
| 端口 | 适用场景 | 特点 |
|---|---|---|
| HP(High Performance) | 高带宽批量传输 | 不保证 cache 一致,需手动 flush/invalidate |
| ACP(Accelerator Coherency Port) | 低延迟协同加速 | 自动维护 cache 一致性,适合紧耦合处理 |
👉 建议:大数据采集用 HP;小包高频交互用 ACP。
2. 内存怎么管?别让“碎片”拖后腿
很多人以为“分配一大块内存”很简单,但在 Linux 用户空间往往做不到。这时就要靠Scatter-Gather 模式来救场。
SG 模式允许你提前准备一组分散的物理页,DMA 控制器会按顺序自动跳转写入。相当于把多个小货架串成一条流水线,照样能收整卡车的货。
✅ 使用条件:
- 必须启用 SG 模式(在 Vivado IP 配置中勾选);
- 描述符链表需驻留在内存中并由驱动管理;
- 需使用XAxiDma_BdRing相关 API 进行高级控制。
3. 时钟域咋办?跨时钟别忘了同步 FIFO
AXI DMA 通常工作在固定频率(如 100MHz),但你的 PL 数据源可能是 150MHz 的像素时钟,或者异步输入信号。
🚨 直接连上去?等着 FIFO 溢出吧。
✅ 正确做法:
- 在用户逻辑和 AXI DMA 之间插入AXI4-Stream FIFO;
- 启用“全/空”标志作为背压信号;
- FIFO 深度建议 ≥512 words,应对突发抖动。
实战避坑指南:那些手册里没写的“经验之谈”
❌ 坑点一:缓存没处理,程序读到“幻影数据”
现象:明明写了数据,CPU 读出来却是乱码或旧值。
原因:L1/L2 cache 缓存了旧内容,没有从 DDR 刷新。
✅ 解法:
// 接收前:确保 CPU 不会误读脏数据 Xil_DCacheInvalidateRange((UINTPTR)rx_buffer, size); // 发送前:把 cache 里的最新数据刷回 DDR Xil_DCacheFlushRange((UINTPTR)tx_buffer, size);❌ 坑点二:地址没对齐,传输莫名其妙失败
AXI 总线要求突发传输地址对齐。比如 64bit 宽度下,每次 burst 起始地址应为 8-byte 对齐。
✅ 解法:
- 分配缓冲区时使用aligned_alloc(8, size)或posix_memalign();
- 检查BUFFER_ADDR % 8 == 0;
- Vivado 中查看 MIG 是否启用了 ECC 或其他对齐限制。
❌ 坑点三:带宽评估不足,DDR 成了瓶颈
你以为 AXI DMA 能跑 2GB/s,结果实测只有 300MB/s?
常见原因:
- DDR 频率低(Zynq-7000 默认仅 533MHz);
- 多主竞争(GPU、DMA、CPU 同时访问);
- 行激活延迟高,随机访问性能差。
✅ 优化手段:
- 使用连续大块传输,最大化突发效率;
- 减少交叉访问,尽量集中操作同一区域;
- 在 UltraScale+ 上启用 LPDDR4 或 HBM 可大幅提升潜力。
结语:让数据自由流动,才是异构系统的灵魂
AXI DMA 看似只是一个“搬运工”,但它实际上是打通 Zynq “任督二脉”的关键枢纽。一旦你掌握了它的使用精髓,你会发现:
- CPU 终于可以从低级事务中解放出来;
- 系统吞吐能力跃升一个数量级;
- 实时性和稳定性得到本质提升。
无论是做高速数据采集卡、工业视觉检测、雷达信号处理,还是构建边缘 AI 推理前端,合理的 DMA 设计都是系统成败的分水岭。
未来随着 AIoT 对本地算力和数据流速的要求越来越高,类似 AXI DMA 的技术还会演化出更多形态——比如 AXI VDMA(视频专用)、AXI CDMA(cache 优化拷贝)、甚至结合 NoC 的片上网络方案。但其核心思想始终不变:让合适的人干合适的事,别让 CPU 去扛麻袋。
如果你正在开发基于 Zynq 的高性能系统,不妨现在就打开 Vivado,加一个 AXI DMA 试试看。也许只改这一处,整个项目的瓶颈就迎刃而解了。
💬 互动时间:你在项目中用过 AXI DMA 吗?遇到过哪些奇葩问题?欢迎在评论区分享你的“踩坑日记”!