AXI DMA实战全解:从零理解FPGA高速数据搬运核心
你有没有遇到过这样的场景?
摄像头刚接上,画面就开始掉帧;ADC采样频率一提上去,CPU直接飙到100%;明明硬件带宽足够,数据就是“卡”在中间传不过去。
问题出在哪?不是算法太慢,也不是逻辑写错了——而是数据搬运方式不对。
在高性能嵌入式系统中,CPU亲自搬数据的时代早已过去。真正高效的方案是:让硬件自动完成传输,CPU只管发号施令和处理结果。这就是AXI DMA的使命。
特别是在 Xilinx Zynq、Zynq UltraScale+ MPSoC 这类异构芯片里,AXI DMA 是连接 ARM 处理器(PS)与 FPGA 可编程逻辑(PL)之间的“高速公路收费站”。它不生产数据,但它决定数据能不能高效通行。
今天我们就来彻底讲清楚:
- AXI DMA 到底是什么?
- 它怎么工作?为什么能提升性能?
- 实际项目中该怎么用?有哪些坑要避开?
什么是 AXI DMA?别被名字吓住
先拆开看名字:A-X-I-D-M-A
- AXI:Advanced eXtensible Interface,ARM 提出的总线协议标准,在 Zynq 中用于 PS 和 PL 之间的通信。
- DMA:Direct Memory Access,直接内存访问,意思是外设可以直接读写内存,不用 CPU 插手。
合起来就是:一个基于 AXI 协议、实现直接内存访问的 IP 核。它的核心任务就两个:
- 把 PL 里的流数据存进 DDR 内存(S2MM)
- 把内存里的数据拿出来送给 PL 做处理(MM2S)
你可以把它想象成一个“快递中转站”:
- MM2S 是从仓库(DDR)往外发货(给 PL)
- S2MM 是把货从外面收进来入库(到 DDR)
而且这两个通道可以同时运行,互不干扰,支持全双工操作。
为什么非要用 AXI DMA?普通读写不行吗?
我们来做个对比。
假设你要传输一帧 1920x1080 的 RGB 图像,大约 6MB 数据。
方法一:CPU 轮询搬运
for (int i = 0; i < 6_000_000; i++) { data = read_from_pl(); ddr_buffer[i] = data; }这期间 CPU 被完全占用,啥也不能干。延迟高、效率低,实时性差得离谱。
方法二:中断驱动
每次收到几个字节就触发一次中断。看似聪明,实则更糟——每秒几百万次中断下来,CPU 光响应中断都忙不过来。
方法三:AXI DMA 出场
CPU 只需告诉 DMA:“去把这 6MB 数据从地址 A 搬到 B”,然后就可以去做别的事了。
DMA 自己启动、搬运、完成后发个中断通知:“老板,活干完了。”
整个过程 CPU 参与时间不到 1%,其余时间都可以跑 OpenCV 算法或者网络传输。
这才是现代嵌入式系统的正确打开方式。
AXI DMA 怎么工作的?三个阶段讲明白
AXI DMA 的运行流程非常清晰,分为三步:配置 → 启动 → 完成。
第一步:配置(CPU 上场)
通过 AXI Lite 接口设置关键参数:
- 源地址 / 目标地址
- 传输长度(最多支持约 64MB 单次传输)
- 是否启用中断
- 是否开启循环模式(适合视频流连续采集)
这些信息写入 DMA 的控制寄存器,相当于下达任务清单。
第二步:启动(DMA 接手)
向控制寄存器写启动命令,DMA 开始干活。
比如 S2MM 方向:
- PL 端开始发送 AXI4-Stream 数据流
- DMA 接收数据,并根据配置好的目标地址,打包写入 DDR
- 收到TLAST信号表示一包结束,DMA 更新状态
MM2S 方向则是反过来:从内存读出数据,转成 Stream 发给 PL。
第三步:完成(中断回调)
传输结束后,DMA 设置完成标志位,并可触发中断。
CPU 在中断服务程序中检查状态,确认无误后启动后续处理,比如图像显示或编码上传。
全程除了开头配置和结尾响应,CPU 零参与。
关键特性解析:不只是简单的搬运工
很多人以为 AXI DMA 就是个“搬砖”的,其实它很聪明。
✅ 双通道独立运行
| 通道 | 方向 | 应用场景 |
|---|---|---|
| MM2S | 内存 → 流 | 图像输出、波形生成 |
| S2MM | 流 ← 内存 | 视频采集、传感器输入 |
两个通道各自有独立的控制逻辑、地址寄存器和中断线,完全可以并行工作。
✅ 支持 Scatter-Gather(分散-聚集)模式
传统 DMA 只能搬连续的一块内存。但现实应用中,数据往往是分段存储的,比如网络包、多帧缓存。
Scatter-Gather 模式允许你定义一个描述符链表,每个条目包含:
- 物理地址
- 数据长度
- 控制标志(如是否中断)
DMA 会按顺序自动执行所有描述符,无需 CPU 干预。这对大数据量、非连续存储场景特别有用。
⚠️ 注意:开启 Scatter-Gather 会增加资源消耗(需要 BRAM 存放描述符),且驱动复杂度上升,建议仅在必要时使用。
✅ 实现零拷贝传输
在 Linux 系统中,用户空间申请的内存通常是虚拟地址,物理上不连续,不适合 DMA 直接访问。
解决方案是使用 CMA(Contiguous Memory Allocator)区域分配连续物理内存,再通过 UIO 或dmaengine驱动映射到用户空间。
这样 PL → DDR → 用户程序的数据路径全程无需复制,真正做到“零拷贝”。
✅ Cache 一致性不能忽视
Zynq 的 ARM 核有 L1/L2 缓存。当 DMA 把新数据写入 DDR 后,CPU 如果直接从 Cache 读取,拿到的是旧数据!
常见解决办法:
- 手动刷新 Cache:调用Xil_DCacheInvalidateRange(addr, len)
- 使用 write-combine 映射:禁用写缓存,确保总是读 DDR 最新值
- 在驱动层使用__dma_map_area()类接口统一管理
这一点在裸机开发中容易忽略,但在实际项目中至关重要。
AXI4 vs AXI4-Stream:搞懂协议差异才能用好 DMA
AXI DMA 的本质是一个协议转换器,它打通了两种不同的 AXI 协议。
AXI4(Memory-Mapped)——有地址的空间
- 用于访问 DDR、寄存器等具有固定地址的资源
- 支持突发传输(Burst),提高带宽利用率
- 读写通道分离,支持高并发
- 连接到 PS 端的 HP(High Performance)端口
在 AXI DMA 中,它负责与 DDR 打交道。
AXI4-Stream —— 无地址的流水线
- 不关心地址,只传递数据流
- 核心信号:
TVALID/TREADY:握手机制,保证双方同步TDATA:数据本身TLAST:标记一个数据包结束TKEEP:指示哪些字节有效(用于部分传输)
FPGA 内部很多模块都用这个接口:FFT、VDMA、以太网 MAC、摄像头接收器……
所以 AXI DMA 天然适合作为它们与系统内存之间的桥梁。
典型应用场景:图像采集系统实战
来看一个最常见的例子:FPGA 图像采集系统。
[摄像头] ↓ (MIPI/Parallel) [FPGA 解码 + 时序控制] ↓ (AXI4-Stream) [AXI DMA (S2MM)] —→ [DDR 内存] ↑ [中断] ↑ [ARM CPU] ↑ [OpenCV 处理 / 显示 / 网络推流]工作流程详解
预分配缓冲区
CPU 提前在 DDR 中分配一块连续物理内存作为帧缓存,获取其物理地址。配置 DMA
通过 AXI Lite 写入 S2MM 目标地址、帧大小、使能中断。启动采集
摄像头开始输出像素流,经 FPGA 解码后以 AXI4-Stream 形式送入 AXI DMA。DMA 自动写入
当收到TLAST(表示一行或一帧结束),DMA 将数据写入指定 DDR 地址。中断通知
写完一帧后,DMA 触发中断,CPU 进入 ISR。后续处理
CPU 可立即处理该帧(如人脸检测),或将地址交给显示控制器渲染。环形缓冲(可选)
若启用循环模式,DMA 会在多个缓冲区间轮转,避免丢帧。
设计避坑指南:那些文档不会明说的经验
🛑 地址必须对齐
AXI 总线要求地址按数据宽度对齐。例如:
- 64 位数据 → 8 字节对齐
- 128 位 → 16 字节对齐
否则可能导致事务失败或性能严重下降。
📦 突发长度要合理
Burst Size 设置影响带宽利用率。太小:握手开销大;太大:可能被仲裁打断。
推荐值:16~32 beats,具体根据系统负载调整。
🔔 中断别太频繁
如果是音频采样(每毫秒一个小包),每一包都中断,CPU 很快就被拖垮。
建议采用“中断合并”策略:每 N 包才报一次中断,平衡实时性与开销。
🧯 错误检测不可少
定期检查状态寄存器中的错误位:
-Decoding Error:地址无效
-Slave Error:从设备异常
-Internal Error:DMA 内部故障
发现异常应及时复位通道,防止累积错误。
💡 资源占用心里要有数
AXI DMA IP 大概消耗:
- 5000 LUTs 左右
- 几个 BRAM 块(尤其开启 Scatter-Gather 时)
设计初期就要评估可用资源,避免后期布线失败。
🧪 仿真验证很重要
用 Vivado Simulator 搭建测试平台:
- 注入 AXI4-Stream 数据流
- 观察 DMA 是否正确写入模拟内存模型
- 检查中断时序、地址跳转是否正常
提前发现问题,比上板调试省十倍时间。
代码实战:裸机与 Linux 下如何控制 AXI DMA
裸机环境:寄存器级操作
#include "xparameters.h" #include "xil_io.h" #define AXI_DMA_BASEADDR 0x40400000 #define MM2S_CTRL_REG 0x00 #define MM2S_START_ADDR_REG 0x18 #define MM2S_LENGTH_REG 0x28 void start_dma_transfer(u32 src_addr, u32 length) { // 停止当前传输 Xil_Out32(AXI_DMA_BASEADDR + MM2S_CTRL_REG, 0x0); // 设置起始地址 Xil_Out32(AXI_DMA_BASEADDR + MM2S_START_ADDR_REG, src_addr); // 启动 Run/Stop 位 Xil_Out32(AXI_DMA_BASEADDR + MM2S_CTRL_REG, 0x1); // 写长度即触发传输 Xil_Out32(AXI_DMA_BASEADDR + MM2S_LENGTH_REG, length); }📌 关键点:
- 先清控制寄存器
- 再写地址
- 最后写长度,才会真正启动
这是 Simple Mode 下的标准操作流程。
Linux 用户空间控制(UIO 示例)
想不写内核模块也能操控硬件?试试 UIO。
# 加载设备树节点后,绑定 UIO echo axi-dma > /sys/class/uio/uio0/nameint fd = open("/dev/uio0", O_RDWR); void *regs = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); // 配置 MM2S 通道 *((volatile u32*)(regs + MM2S_CTRL_REG)) = 0; *((volatile u32*)(regs + MM2S_START_ADDR_REG)) = phys_addr; *((volatile u32*)(regs + MM2S_CTRL_REG)) = 1; *((volatile u32*)(regs + MM2S_LENGTH_REG)) = frame_size; // 阻塞等待中断 int irq_count; read(fd, &irq_count, sizeof(int)); printf("✅ DMA transfer complete!\n");这种方式非常适合快速原型开发,调试效率极高。
最后一句话总结
AXI DMA 不只是一个 IP 核,它是通往高性能 FPGA 系统的大门钥匙。
掌握它,意味着你不再只是“会写逻辑”,而是真正懂得如何构建一个高效、稳定、可扩展的软硬协同系统。
无论是做图像处理、高速采集、通信协议栈还是边缘 AI 推理,只要涉及大量数据流动,AXI DMA 都是你绕不开的核心组件。
现在回头看看你的项目,是不是也有“卡顿”、“丢帧”、“CPU 占满”的问题?
也许换个思路,让 DMA 来扛活,一切就通了。
如果你正在学习 Zynq 或 FPGA 开发,不妨动手搭个最简单的 AXI DMA 回环实验:从内存读数据 → 经 PL 绕一圈 → 写回内存。
跑通那一刻,你会对“硬件加速”四个字有全新的理解。
欢迎在评论区分享你的实践心得,我们一起交流进步!