ZYNQ AXI DMA 高速数据传输实战:从 Vivado 硬件搭建到 Linux 回环测试(含 4 个踩坑)
适用场景:ZYNQ(xc7z020)+ Petalinux 2018.3 开发,想在 PS 和 PL 之间实现高速数据传输。本文从 Vivado 硬件搭建开始,完整记录 AXI DMA + AXI Stream Data FIFO 回环实验的全过程,包括设备树配置、内核驱动、Linux DMA Engine API 测试驱动,以及 4 个真实踩坑的排查与解决。
前言
在 ZYNQ 开发中,PS 和 PL 之间的数据传输是核心课题。GPIO、UART 这些低速接口显然不够用,AXI DMA 才是高速数据搬运的标配。
最近我在 AC880 开发板上做了 AXI DMA 回环实验,从 Vivado 硬件搭建到 Linux 驱动测试完整走通,256 字节到 1MB 数据全部传输正确。过程中踩了不少坑,特别是 dma_request_channel 拿不到通道、16384 字节差 1 字节、大传输超时这几个问题,折腾了挺久。
本文把完整流程和踩坑记录分享出来,希望能帮到正在做 AXI DMA 的朋友。
环境信息:
|
项
|
值
|
| --- | --- |
|
开发板
|
AC880 ZYNQ(xc7z020clg484-2)
|
|
Vivado 版本
|
2018.3
|
|
Petalinux 版本
|
2018.3
|
|
内核版本
|
4.14-xilinx-v2018.3
|
|
AXI DMA 模式
|
Simple(Scatter Gather 关闭)
|
一、AXI DMA 硬件架构速览
AXI DMA 有两个数据通道:
MM2S(Memory Map to Stream):从 DDR 读数据 → 输出 AXI Stream
S2MM(Stream to Memory Map):接收 AXI Stream → 写入 DDR
回环实验的数据通路:
DDR(tx_buf) ──MM2S──> AXI Stream ──FIFO──> AXI Stream ──S2MM──> DDR(rx_buf)
中间用一个 AXI Stream Data FIFO 把 MM2S 的输出连到 S2MM 的输入,形成回环。
二、Vivado 硬件搭建
2.1 添加 IP
在 Block Design 中添加:
AXI DMA:高速数据搬运
AXI Stream Data FIFO:回环缓冲
2.2 AXI DMA 配置
|
参数
|
值
|
说明
|
| --- | --- | --- |
|
Scatter Gather Engine
|
关闭
|
Simple 模式
|
|
Width of Buffer Length Register
|
14
|
决定单次最大传输 = 2^14 - 1 = 16383 字节
|
|
Memory Map Data Width
|
32
| |
|
Stream Data Width
|
32
| |
|
启用通道
|
MM2S + S2MM
| |
2.3 AXI Stream Data FIFO 配置
|
参数
|
值
|
说明
|
| --- | --- | --- |
|
TDATA Width (bytes)
|
4
|
32 位,必须和 DMA Stream 宽度一致
|
|
FIFO Depth
|
默认
|
经测试 4096 字节以内传输稳定
|
2.4 时钟域配置(关键!)
AXI DMA 的控制口和数据口必须用不同时钟域:
|
信号
|
连接到
|
时钟
|
| --- | --- | --- |
|s_axi_lite_aclk(控制)
|
FCLK_CLK0
|
100 MHz
|
|m_axi_mm2s_aclk(数据读)
|
FCLK_CLK1
|
200 MHz
|
|m_axi_s2mm_aclk(数据写)
|
FCLK_CLK1
|
200 MHz
|
|s_axis_aclk(FIFO)
|
FCLK_CLK1
|
200 MHz
|
控制口走 GP0(100MHz),数据口走 HP0(200MHz)。如果时钟域搞错,Vivado 会报一堆时钟不匹配错误。
2.5 HP 端口共享
如果 HP0 已经被 VDMA 占用,不需要启用 HP1,复用现有 axi_smc(AXI SmartConnect)即可:
S00: axi_vdma_0
S01: axi_dma_0 MM2S
S02: axi_dma_0 S2MM
配置为 3 Slave → 1 Master → S_AXI_HP0。
2.6 地址分配
|
接口
|
地址范围
|
大小
|
| --- | --- | --- |
|
S_AXI_LITE(控制)
|
0x4040_0000 - 0x4040_FFFF
|
64K
|
|
Data_MM2S(HP0)
|
0x0000_0000 - 0x3FFF_FFFF
|
1G DDR
|
|
Data_S2MM(HP0)
|
0x0000_0000 - 0x3FFF_FFFF
|
1G DDR
|
地址无冲突,无需手动分配。
2.7 时序分析
生成 bitstream 后时序报告显示 WNS = -0.174 ns(174ps 违例),仅 1/33712 端点失败。
这种程度的违例对功能无影响:
174ps 是皮秒级,远小于 1ns
是 AXI DMA IP 内部路径,无法修改 RTL
FCLK_CLK1 = 200MHz 不能降频
实际芯片性能通常比 STA 悲观估计好
直接 Generate Bitstream 即可。
2.8 导出 HDF
File → Export → Export Hardware,勾选 Include bitstream,导出到 ~/ZYNQ/zynq_linux/hdf/。
三、Petalinux 配置
3.1 导入 HDF
cd ~/ZYNQ/zynq_linux
petalinux-config --get-hw-description=./hdf/
# menuconfig 直接 Save → Exit
3.2 内核配置
petalinux-config -c kernel
Device Drivers --->
[*] DMA Engine support --->
<*> Xilinx DMA Configuration Engine ← CONFIG_XILAXIDMA
3.3 设备树(重要踩坑!)
HDF 导入后,Petalinux 会自动在 pl.dtsi 中生成 axi_dma_0 节点:
axi_dma_0: dma@40400000 {
#dma-cells = <1>;
clock-names = "s_axi_lite_aclk", "m_axi_mm2s_aclk", "m_axi_s2mm_aclk";
clocks = <&clkc 15>, <&clkc 16>, <&clkc 16>;
compatible = "xlnx,axi-dma-7.1", "xlnx,axi-dma-1.00.a";
interrupt-names = "mm2s_introut", "s2mm_introut";
interrupt-parent = <&intc>;
interrupts = <0 32 4 0 33 4>;
reg = <0x40400000 0x10000>;
...
};
⚠️ 千万不要在 system-user.dtsi 里手动覆盖 axi_dma_0 节点!
我一开始手动写了一段,结果编译报错
Reference to non-existent node or label "gic"。原因是 ZYNQ 7000 的 GIC 标签是intc不是gic,而且中断号我猜错了(写成 59/60,实际是 32/33)。自动生成的节点完全正确,手动覆盖只会引入错误。
3.4 编译 + 打包
petalinux-build
petalinux-package --boot --fsbl images/linux/zynq_fsbl.elf \
--fpga images/linux/system.bit \
--u-boot images/linux/u-boot.elf \
--output images/linux/BOOT.BIN --force
四、部署到 SD 卡(U-Boot + TFTP)
用 TFTP 更新 system.dtb、system.bit、zImage,BOOT.BIN 建议用读卡器直接拷贝(风险最高):
# U-Boot 命令行
tftpboot 0x2000000 system.dtb && fatwrite mmc 0:1 0x2000000 system.dtb ${filesize}
tftpboot 0x3000000 system.bit && fatwrite mmc 0:1 0x3000000 system.bit ${filesize}
tftpboot 0x2080000 zImage && fatwrite mmc 0:1 0x2080000 zImage ${filesize}
五、板端验证
# 1. DMA 通道设备节点
ls /sys/class/dma/
# 期望: dma1chan0 (MM2S), dma1chan1 (S2MM)
# 2. 驱动加载日志
dmesg | grep -i "xilinx\|dma"
# 期望: xilinx-vdma 40400000.dma: Xilinx AXI DMA Engine Driver Probed!!
# 3. 中断注册
cat /proc/interrupts | grep xilinx-dma
# 期望: mm2s(IRQ64), s2mm(IRQ65)
六、回环测试驱动(DMA Engine API)
6.1 核心 API 流程
// 1. 申请通道 (DMA_SLAVE mask, Xilinx DMA 只注册此能力)
dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask);
tx_chan = dma_request_channel(mask, xilinx_dma_filter, NULL); // MM2S
rx_chan = dma_request_channel(mask, xilinx_dma_filter, NULL); // S2MM
// 2. 分配一致性内存
tx_buf = dma_alloc_coherent(dev, size, &tx_dma, GFP_KERNEL);
rx_buf = dma_alloc_coherent(dev, size, &rx_dma, GFP_KERNEL);
// 3. 先启动 S2MM (接收), 再启动 MM2S (发送)
rx_desc = dmaengine_prep_slave_single(rx_chan, rx_dma, len, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
rx_desc->callback = dma_rx_callback;
dmaengine_submit(rx_desc);
dma_async_issue_pending(rx_chan);
udelay(10); // 确保 S2MM 就绪
tx_desc = dmaengine_prep_slave_single(tx_chan, tx_dma, len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
tx_desc->callback = dma_tx_callback;
dmaengine_submit(tx_desc);
dma_async_issue_pending(tx_chan);
// 4. 等待完成
wait_for_completion_timeout(&tx_done, 5*HZ);
wait_for_completion_timeout(&rx_done, 5*HZ);
// 5. 数据校验
memcmp(tx_buf, rx_buf, size);
6.2 通道过滤函数
static bool xilinx_dma_filter(struct dma_chan *chan, void *param)
{
struct device *dev = chan->device->dev;
const char *name;
bool match = false;
if (!dev) return false;
name = dev_name(dev);
// 策略 1: compatible 匹配
if (dev->of_node && of_device_is_compatible(dev->of_node, "xlnx,axi-dma-1.00.a"))
match = true;
// 策略 2: 设备名含基地址 40400000
if (name && strstr(name, "40400000"))
match = true;
return match;
}
6.3 大传输拆分(sg-length-width 限制)
设备树中 xlnx,sg-length-width = <0xe>(14 位),单次传输最大 2^14 - 1 = 16383 字节。超过必须拆分:
#define AXI_DMA_MAX_XFER 4096 // 不用 16383, 用 4096 避免 FIFO 溢出
for (offset = 0; offset < size; offset += chunk_len) {
remaining = size - offset;
chunk_len = (remaining > AXI_DMA_MAX_XFER) ? AXI_DMA_MAX_XFER : remaining;
dma_xfer_chunk(tx_dma + offset, rx_dma + offset, chunk_len);
}
七、踩坑记录(4 个坑)
坑 1:failed to request MM2S channel
现象:dma_request_channel(DMA_MEM_TO_DEV) 返回 NULL。
原因:Xilinx AXI DMA 驱动只注册 DMA_SLAVE 能力,不注册 DMA_MEM_TO_DEV / DMA_DEV_TO_MEM。
解决:改用 DMA_SLAVE mask 申请通道。AXI DMA 通道顺序固定:chan0=MM2S,chan1=S2MM。
坑 2:16384 字节差 1 字节
现象:传输 16384 字节,最后 1 字节(index 16383)丢失,tx=0xFF,rx=0x00。
原因:sg-length-width=14 限制单次传输最大 16383 字节,驱动截断了最后 1 字节。
解决:拆分传输,每块 ≤ 16383 字节。
坑 3:65536+ 字节超时 + Channel has errors 10
现象:chunk 大小用 16383 时,大传输间歇性超时,DMA 通道报错误。
原因:AXI Stream Data FIFO 深度有限,16383 字节 chunk 可能导致 FIFO 溢出(MM2S 写入快于 S2MM 读取,共享 HP0 端口竞争)。
解决:chunk 大小改为 4096 字节(远小于 FIFO 深度,稳定可靠)。
坑 4:chunk 开头 terminate_all 导致 4096 也超时
现象:在每个 chunk 传输前调用 dmaengine_terminate_all() 重置通道,结果 4096 字节也超时。
原因:terminate_all 重置通道后,立即 prep_slave_single + submit 会失败,通道未就绪。
解决:去掉 chunk 开头的 terminate_all,只在出错时才调用清理。
八、测试结果
|
缓冲区大小
|
结果
|
| --- | --- |
|
256 字节
|
✅ PASSED
|
|
1024 字节
|
✅ PASSED
|
|
4096 字节
|
✅ PASSED
|
|
8192 字节
|
✅ PASSED
|
|
16384 字节
|
✅ PASSED(拆分为 4×4096)
|
|
65536 字节
|
✅ PASSED(拆分为 16×4096)
|
|
1,048,576 字节 (1MB)
|
✅ PASSED(拆分为 256×4096)
|
|
4096 字节 × 10 次连续
|
✅ 全部 PASSED
|
九、总结
AXI DMA 回环实验的核心要点:
时钟域分离:控制口 100MHz,数据口 200MHz,不能搞错
设备树不手动覆盖:pl.dtsi 自动生成的 axi_dma_0 节点完全正确
用 DMA_SLAVE 申请通道:Xilinx DMA 驱动只注册此能力
大传输拆分成 4096 字节 chunk:同时规避 sg-length-width 限制和 FIFO 溢出
不要在 chunk 开头 terminate_all:会导致通道未就绪
完整驱动源码在 apps/axi_dma_test/ 目录下,可直接编译使用。