Xilinx Zynq AXI DMA回环开发完整指南
前言
- 本文目标:本指南记录了在Xilinx Zynq-7020平台上实现AXI DMA回环测试的完整开发过程,包括从硬件设计确认到最终C++应用程序实现的全部步骤。硬件平台文件依赖Zynq-7000-完整硬件平台构建与验证权威指南
目标系统配置
- 硬件平台: XC7Z020-2CLG484I
- 软件版本: Vivado/Vitis/Petalinux 2023.2
- 系统内存: 1GB DDR3
- CMA内存: 256MB
核心名词解释
AXI DMA相关
- AXI DMA: Advanced eXtensible Interface Direct Memory Access,Xilinx提供的高性能DMA IP核
- MM2S: Memory-Mapped to Stream,内存到流方向的DMA传输通道
- S2MM: Stream to Memory-Mapped,流到内存方向的DMA传输通道
- AXI4-Stream: 高速流式数据传输协议,适用于数据流应用
系统框架
DMA: DMA控制器是独立的硬件单元,它直接连接到内存总线,绕过CPU进行数据传输。DMA硬件内部没有地址翻译单元(MMU),因此只能理解和使用物理地址
虚拟内存:虚拟内存是操作系统为用户程序提供的抽象,它存在于软件层面。硬件设备无法访问这个软件抽象层
- 每个程序都以为自己独占整个内存,实际上是操作系统的障眼法,通过MMU(内存管理单元)翻译成物理地址
物理内存:真实的DDR内存芯片的地址
UIO: Userspace I/O,Linux内核框架,允许用户空间直接访问硬件寄存器
CMA: Contiguous Memory Allocator,连续内存分配器,为DMA提供连续物理内存,对应设备树的如下配置
1
2
3
4
5
6
7
8reserved-memory {
dma_4k_pool: dma-pool@30000000 {
compatible = "shared-dma-pool";
reg = <0x30000000 0x10000000>; // 256MB
reusable;
linux,cma-default; // ← 这里启用了CMA
};
};- 普通的内存分配包括
/dev/mem
在内,无法保证物理地址的连续性。Linux内核会将物理内存分割成小块分配给不同程序,导致物理内存碎片化。DMA传输通常需要大块连续的物理内存,如果内存不连续,DMA硬件无法正确传输数据 - 直接使用
/dev/mem
访问任意物理地址,可能会意外覆盖正在使用的内核内存,出现数据安全问题,CMA预留区域确保这块内存专门给DMA使用,不会被内核或其他程序占用
- 普通的内存分配包括
DMAEngine: Linux内核的统一DMA管理框架
Device Tree: 设备树,描述硬件配置的数据结构
第一阶段:系统编译
第一步:创建PetaLinux项目
1 | # 创建基于Zynq模板的项目 |
说明: 选择zynq模板为Zynq 7000系列芯片提供基础配置框架。
第二步:导入硬件配置
1 | # 创建硬件文件目录 |
重要事项:
- 使用相对路径导入,确保路径正确性
- 如果弹出配置菜单,通常可以直接保存退出
- 导入后会在
project-spec/hw-description/
生成硬件信息
第三步:系统配置
1 | petalinux-config |
关键配置项:
缓存配置
参考PetaLinux-2023-2-离线缓存与加速编译配置
启动配置为SD卡
- 路径:
Subsystem AUTO Hardware Settings
->SD/SDIO Settings
- 设置: 确认
Primary SD/SDIO (ps7_sd_0)
已选择 Image Packaging Configuration -> Root filesystem type
,从INITRD
改为EXT4 (SD/eMMC/SATA/USB)
串口配置
- 路径:
Subsystem AUTO Hardware Settings
->Serial Settings
- 配置(此处是因为开发板的底板串口为串口1,此处配置要看开发板):
FSBL Serial stdin/stdout (ps7_uart_1)
DTG Serial stdin/stdout (ps7_uart_1)
System stdin/stdout baudrate for ps7_uart_1 (115200)
以太网配置
- 路径:
Subsystem AUTO Hardware Settings
->Ethernet Settings
- 配置:
Primary Ethernet (ps7_ethernet_0)
- 确认已选择[*] Obtain IP address automatically
- 启用DHCP- MAC地址保持默认即可
启动参数配置
路径:
DTG Settings -> Kernel Bootargs
设置为手动设置参数,设置如下1
console=ttyPS0,115200 earlycon root=/dev/mmcblk0p2 ro rootwait uio_pdrv_genirq.of_id=generic-uio
第四步:内核配置
1 | petalinux-config -c kernel |
必需启用的驱动模块:
DMA引擎支持
- 路径:
Device Drivers
->DMA Engine support
- 启用:
<*> Xilinx AXI DMAS Engine
- 状态: 通常默认已启用
UIO支持(用户空间IO访问)
路径:
Device Drivers
->Userspace I/O drivers
必须启用:
<M> Userspace I/O platform driver with generic IRQ handling
,该项无法修改为*
<*> Userspace platform driver with generic irq and dynamic memory
重要性: 用于用户空间访问BRAM控制器等自定义IP
UIO驱动的作用
Userspace I/O (UIO) 的优势:
允许用户空间程序直接访问硬件寄存器
避免编写复杂的内核驱动
适用于自定义IP核的快速原型开发
支持中断处理和内存映射
适用场景:
- BRAM控制器的用户空间访问
- 自定义AXI IP核的控制
- 硬件加速器的用户空间接口
以太网驱动
- 路径:
Device Drivers
->Network device support
->Ethernet driver support
- 启用:
<*> Xilinx 10/100/1000 AXI Ethernet support
PHY驱动(关键!)
- 路径:
Device Drivers
->Network device support
->PHY Device support and infrastructure
- 必须启用:
[*] Micrel Phys
- 重要性: 没有正确的PHY驱动,网卡无法工作
第五步:根文件系统配置
1 | petalinux-config -c rootfs |
推荐配置:
基础系统
- 路径:
Filesystem Packages -> base -> busybox
[*] busybox
- 基础系统工具集[*] busybox-udhcpc
- DHCP客户端(用于自动获取IP)
网络支持
- SSH服务默认已配置,无需额外设置
自动登录配置(重要!)
- 路径:
Image Features
- 必须启用,在该开发板上使用UART1登录时root密码登录会出现一直失败的情况,可能是BUG:
-*- empty-root-password
- 设置root用户空密码[*] serial-autologin-root
- 串口自动以root身份登录
第六步:设备树文件配置
编辑
project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi
添加如下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/include/ "system-conf.dtsi"
/ {
reserved-memory {
ranges;
// 为多路4K视频处理预留256MB CMA内存
dma_4k_pool: dma-pool@30000000 {
compatible = "shared-dma-pool";
reg = <0x30000000 0x10000000>; // 256MB
reusable;
linux,cma-default;
};
};
};
// UIO设备绑定
&axi_bram_ctrl_0 {
compatible = "generic-uio";
};
&axi_bram_ctrl_1 {
compatible = "generic-uio";
};
&axi_cdma_0 {
compatible = "generic-uio";
};
&axi_dma_0 {
compatible = "generic-uio"; // 从xilinx驱动改为UIO
};设备树配置原理
compatible = "generic-uio"
: 告诉Linux使用UIO框架- UIO框架自动创建
/dev/uioX
设备文件 - 支持用户空间直接访问硬件寄存器和中断
第七步:系统构建
1 | # 完整构建(首次构建20-60分钟) |
构建说明:
- 首次构建会下载并编译大量软件包
- 构建过程包括:内核编译、根文件系统生成、设备树编译等
- 如果出现网络下载错误,根据URL下载并补充在downloads目录下,详情参考PetaLinux-2023-2-离线缓存与加速编译配置
第八步:生成启动镜像
1 | 打包启动镜像 |
- 生成的关键文件:
BOOT.BIN
(~5MB) - 包含FSBL、FPGA比特流、U-Boot、设备树image.ub
(~76MB) - FIT格式,包含Linux内核和根文件系统boot.scr
(~3.5KB) - U-Boot启动脚本
- 注意,2023.2版本似乎不会在后续编译的时候生成boot.scr,因此该文件时间在后续构建是旧的
第九步:SD卡制作
先查看自己的sd卡名称,我的设备是sdb1
1
sudo fdisk -l
挂载SD卡分区(需要提前分区:FAT32启动分区 + EXT4根文件系统分区)
1
2sudo mount /dev/sdb1 /mnt/boot
sudo mount /dev/sdb2 /mnt/rootfs复制启动文件到boot分区,解压根文件系统到rootfs分区
1
2
3
4sudo cp images/linux/BOOT.BIN /mnt/boot/
sudo cp images/linux/image.ub /mnt/boot/
sudo cp images/linux/boot.scr /mnt/boot/
sudo tar -xzf images/linux/rootfs.tar.gz -C /mnt/rootfs/同步并卸载
1
2sync
sudo umount /mnt/boot /mnt/rootfs
第二阶段:项目背景和硬件信息确认
硬件设计分析
连接关系
1 | AXI DMA连接关系: |
中断连接
1 | 中断处理链路: |
关键参数
- FIFO深度: 1024 (32位宽度,最大缓冲4KB)
- 地址空间: 0x40400000 - 0x4040FFFF (64KB)
- 开发方案: UIO + CMA混合方案(用户空间完全控制,适合生产环境)
第三阶段:硬件信息发现
步骤1:查找设备树信息
核心命令
1 | # 查看设备树结构 |
解析结果
1 | 设备发现结果: |
步骤2:中断号转换规则
重要概念
Linux中断号计算公式:
1 | Linux中断号 = 设备树中断号 + 32 |
实际映射
1 | AXI DMA中断映射: |
步骤3:验证驱动状态
1 | # 检查驱动加载 |
第四阶段:设备树配置修改
FIFO深度与传输关系分析
1 | FIFO深度影响分析: |
多路4K视频内存需求
1 | 性能需求分析: |
设备树配置
1 | /include/ "system-conf.dtsi" |
编译流程
1 | # 修改设备树 |
第五阶段:UIO设备验证
验证步骤
1 | # 1. 检查UIO设备节点 |
设备映射结果
1 | UIO设备映射表: |
CMA内存验证
1 | # 检查CMA配置 |
第六阶段:C++应用程序开发
完整代码
1 |
|
代码详细解析
代码架构概览
整体设计思路
1 | class AXI_DMA_Controller { |
这个类采用了RAII (Resource Acquisition Is Initialization) 设计模式,确保资源的自动管理和异常安全。
类成员变量解析
硬件资源抽象
1 | private: |
设计解析:
uio_fd_
: 连接到/dev/uio3
,提供对AXI DMA寄存器的访问权限reg_base_
: 通过mmap()
将物理寄存器地址映射到用户空间的虚拟地址cma_fd_
: 访问/dev/mem
以分配连续物理内存dma_buffer_
: 用户空间可访问的DMA缓冲区虚拟地址dma_phys_
: DMA控制器使用的物理地址(硬件直接访问)
为什么需要物理地址
DMA控制器是硬件设备,它绕过CPU和MMU直接访问内存,因此:
- 用户程序使用虚拟地址访问数据
- DMA硬件使用物理地址访问同一块内存
- 两个地址指向同一物理内存区域,但表示方法不同
寄存器定义和硬件映射
AXI DMA寄存器布局
1 | // MM2S通道寄存器 (内存到流) |
控制位定义解析
1 | // 控制寄存器位定义 |
位操作原理:
(1 << n)
: 创建第n位为1的掩码- 用于设置、清除和检查特定的控制位
- 硬件通过这些位与软件通信状态和控制信息
构造函数详细解析
初始化序列
1 | explicit AXI_DMA_Controller(size_t buffer_size = 1024 * 1024) |
初始化列表的作用:
- 将所有指针和文件描述符初始化为无效值
MAP_FAILED
是mmap()
的错误返回值- 确保在构造失败时析构函数能正确清理
UIO设备打开
1 | uio_fd_ = open("/dev/uio3", O_RDWR | O_SYNC); |
标志解析:
O_RDWR
: 读写模式打开O_SYNC
: 同步I/O,确保寄存器操作的时序正确性
寄存器空间映射
1 | reg_base_ = mmap(nullptr, 0x10000, PROT_READ | PROT_WRITE, |
mmap参数解析:
nullptr
: 让系统选择映射地址0x10000
: 64KB地址空间(与硬件设计中的AXI DMA地址空间匹配)PROT_READ | PROT_WRITE
: 可读可写权限MAP_SHARED
: 共享映射,多个进程可以访问同一物理内存uio_fd_
: UIO设备文件描述符0
: 从设备的起始地址开始映射
内存分配策略深度解析
CMA内存分配原理
1 | void allocate_dma_buffer() { |
- cma_fd_ 是整个物理内存的访问权限证
/dev/mem
就像是一个巨大图书馆的总入口cma_fd_
就是您的借书证,允许您进入这个图书馆- 有了借书证,您就可以访问图书馆里的任何书架(任何物理地址)
- mmap 的映射过程
- “我要借特定位置的书”:指定物理地址 0x38000000
- “给我一个书桌编号”:系统分配虚拟地址给 dma_buffer_
- “建立对应关系”:虚拟地址 dma_buffer_ ↔ 物理地址 0x38000000
为什么选择0x38000000?
1 | 系统内存布局分析: |
内存锁定的重要性
1 | if (mlock(dma_buffer_, total_size) != 0) { |
mlock作用:
- 防止操作系统将DMA缓冲区交换到磁盘
- 确保物理地址在DMA传输期间保持不变
- 提高DMA传输的可靠性和性能
寄存器操作函数解析
读寄存器函数
1 | uint32_t read_reg(uint32_t offset) { |
写寄存器函数
1 | void write_reg(uint32_t offset, uint32_t value) { |
关键技术细节
volatile关键字的作用
- 防止编译器优化: 告诉编译器这个内存位置可能被硬件修改
- 强制每次访问: 确保每次读写都直接访问硬件寄存器
- 避免缓存问题: 防止CPU缓存导致的数据不一致
reinterpret_cast的必要性
reg_base_
是void*
类型,需要转换为具体的指针类型static_cast<char*>
+offset
:字节级地址计算reinterpret_cast<volatile uint32_t*>
:转换为32位寄存器指针
内存屏障指令
1 | asm volatile("dsb sy" : : : "memory"); |
dsb (Data Synchronization Barrier): ARM架构的内存屏障指令
- 内存屏障是一种强制内存操作按特定顺序执行的机制。它解决的是现代计算机系统中一个很微妙但很重要的问题:内存操作的顺序可能不是您期望的那样,防止计算机优化,导致命令的执行先后顺序不一样
sy (System): 影响整个系统的内存访问顺序
“memory”: 告诉编译器内存内容可能已改变
DMA复位流程解析
复位序列
1 | void reset_dma() { |
复位流程解析
- 软件复位: 设置RESET位触发硬件复位
- 等待复位完成: 硬件会自动清除RESET位
- 确认停止状态: 检查HALTED位确保DMA完全停止
- 避免忙等待: 使用
sleep_for
减少CPU占用
DMA传输核心逻辑
传输启动序列
1 | bool loopback_test(size_t transfer_size) { |
关键设计决策解析
- 缓冲区布局
1 | DMA缓冲区内存布局: |
- 测试数据模式
1 | src_buffer[i] = static_cast<uint8_t>(i & 0xFF); |
- 生成0-255循环的测试模式
- 便于验证数据完整性
& 0xFF
确保值在0-255范围内
- S2MM先启动的原因
在AXI4-Stream协议中:
- 发送方: 必须等待接收方准备好
- 接收方: 需要先设置好接收缓冲区
- FIFO缓冲: 提供临时存储,但容量有限
- 背压机制: 如果S2MM未准备好,MM2S会暂停
- 64位地址处理
1 | // 分别设置高32位和低32位 |
- Zynq-7020支持64位地址空间
- 需要分别配置高低32位寄存器
& 0xFFFFFFFF
确保只取低32位
状态监控和错误处理
完成检测逻辑
1 | bool wait_for_completion(int timeout_ms = 5000) { |
状态检测策略
- 双通道检测: 必须同时检测MM2S和S2MM状态
- 错误优先: 先检查错误状态,避免误判
- 轮询间隔: 100μs的检测间隔平衡响应性和CPU占用
- 超时保护: 5秒超时避免无限等待
性能测量
1 | auto start_time = std::chrono::high_resolution_clock::now(); |
性能计算解析:
transfer_size * 8.0
: 字节转换为位duration.count()
: 微秒数- 结果单位: Mbps (兆位每秒)
数据验证机制
完整性检查
1 | bool data_match = true; |
验证策略
- 逐字节比较: 确保每个字节都正确传输
- 早期退出: 发现第一个错误即停止,提高效率
- 详细错误信息: 显示具体的错误位置和数据值
资源管理和RAII
析构函数
1 | ~AXI_DMA_Controller() { |
RAII原则体现
- 获取即初始化: 构造函数中分配所有资源
- 自动清理: 析构函数自动释放资源
- 异常安全: 即使发生异常也能正确清理
- 状态检查: 只清理已成功分配的资源
主函数测试框架
测试策略设计
1 | std::vector<size_t> test_sizes = { |
渐进式测试的意义
- 基本功能验证: 1KB测试基本的DMA传输能力
- FIFO边界测试: 4KB测试FIFO满载情况
- 流控制验证: 16KB测试大于FIFO深度的传输
- 性能评估: 更大的数据量评估峰值性能
- 稳定性测试: 不同大小的数据验证系统稳定性
错误处理策略
1 | bool all_passed = true; |
快速失败策略: 一旦发现错误立即停止,避免浪费时间和可能的系统损坏。
性能测试
1 | std::vector<size_t> test_sizes = { |
性能测试结果
吞吐量分析
1 | 测试结果统计: |
性能特点分析
- 随数据量增长: 吞吐量显著提升,符合DMA特性
- 延迟稳定: 控制在30-50μs,满足实时要求
- FIFO效应: 4KB(FIFO深度)后性能加速明显
- 峰值性能: 63.5Gbps足以支持数十路4K视频处理
状态寄存器解读
1 | 最终DMA状态: |
关键问题解答
问题1:设备树中断信息解析
问题: 如何从hexdump输出判断哪个设备对应哪个中断?
解决方法:
1 | # 分别查看各设备中断 |
问题2:UIO方案性能影响评估
问题: 视频处理应用的性能损失如何?
分析结果:
1 | 性能影响评估: |
问题3:DMA启动顺序重要性
关键发现: S2MM必须先于MM2S启动,否则可能数据丢失
正确方法:
1 | // 1. 先准备接收端 |