DMA与AXI总线协同工作原理:从概念到嵌入式系统高效数据传输实践

📅 2026/8/4 3:21:22
DMA与AXI总线协同工作原理:从概念到嵌入式系统高效数据传输实践
如果你正在开发一个嵌入式系统尤其是基于 ARM Cortex-M 或 Zynq 这类 SoC 的平台那么“DMA”和“AXI”这两个词一定不会陌生。它们经常出现在芯片手册、参考代码和调试日志里但很多开发者对它们的理解可能还停留在“DMA 能搬数据不占 CPU”和“AXI 是个总线”的层面。这恰恰是问题的关键。当你的串口 DMA 发送数据后如何精准判断“发送完成”为什么使用 AXI 总线的 DMA 控制器性能更高在 Zynq 平台上如何利用 AXI DMA 在 PS 和 PL 之间高效传输数据这些问题如果只停留在概念层面一旦遇到实际项目就会立刻变成棘手的 Bug 和性能瓶颈。本文要解决的正是这个“知道但用不好”的痛点。我们将以“DMA 与 AXI 总线的协同工作原理”为核心深入探讨 DMA 控制器如何通过 AXI 总线协议高效工作。这不是一篇简单的概念科普而是一份从原理到实践的深度指南。你将彻底理解DMA 的“传输完成”中断到底在哪个时刻触发它与外设状态有何关系AXI 协议中的关键机制如 Outstanding、Burst如何显著提升 DMA 传输效率如何针对 STM32、GD32 等 MCU 以及 Xilinx Zynq 等 FPGA SoC编写稳定可靠的 DMA 驱动代码。无论你是在调优一个音频播放器的 IIS DMA 双缓冲还是在实现 FPGA 与处理器间的高速数据流理解 DMA 与 AXI 的协同工作都是突破性能瓶颈、写出稳定驱动代码的关键一步。1. 这篇文章真正要解决的问题在嵌入式开发中我们使用 DMA 的初衷很明确把 CPU 从繁重的数据搬运工作中解放出来提升系统整体效率。然而理想很丰满现实却很骨感。很多开发者会遇到以下典型问题问题一传输完成判断不准。代码配置了串口 DMA 发送也开启了 DMA 传输完成中断TC。中断触发后立即操作了发送缓冲区或关闭了发送使能结果发现最后一两个字节没有真正从串口发送出去。这是因为“DMA 传输完成”不等于“外设发送完成”。DMA 只负责把数据从内存搬到外设的数据寄存器如 USART-DR而数据从寄存器到物理引脚上的移位输出是由外设自己完成的。这个时间差就是坑。问题二性能达不到预期。同样是用 DMA 搬运内存数据在有些平台上速度飞快在另一些平台上却提升有限。这背后往往是总线协议的差异。简单的“存储器到存储器”DMA 和通过高效总线如 AXI进行 DMA在协议开销、并发能力上有天壤之别。问题三系统复杂度高调试困难。在 Zynq 或类似 SoC 上数据流可能需要在处理器PS的可编程逻辑PL之间通过 DMA 传输。这时会涉及 AXI DMA IP 核、AXI Stream 接口、Scatter-Gather 等概念。一旦数据流卡住面对复杂的 AXI 信号和 DMA 控制器寄存器新手往往无从下手。本文的核心目标就是穿透表层 API深入 DMA 控制器与总线特别是 AXI协同工作的机制让你不仅能“用上”DMA更能“用好”DMA写出高效、稳定的底层驱动。我们将重点关注两个最核心的交互点DMA 如何通过总线访问内存和外设以及如何与外设状态正确同步。2. 基础概念与核心原理在深入细节之前我们需要统一对几个核心概念的理解。2.1 DMA 控制器数据搬运的专职管家DMADirect Memory Access控制器是一个独立的硬件模块。它的核心功能是在不需要 CPU 介入的情况下在内存与内存之间、或者内存与外设之间搬运数据。关键点在于“不需要 CPU 介入数据搬运”。CPU 只需要初始化 DMA 控制器告诉它源地址、目标地址、数据量然后就可以去执行其他任务。DMA 控制器会“窃取”总线的控制权完成数据传输完成后通过中断通知 CPU。一个典型的 DMA 传输涉及以下几个角色Initiator发起者通常是外设如 USART 接收数据或软件启动内存拷贝。DMA 控制器接收请求执行传输。总线Bus数据流通的公路如 AHB、APB、AXI。Target目标数据的目的地如内存SRAM或外设的数据寄存器。2.2 AXI 总线高性能系统的数据高速公路AXIAdvanced eXtensible Interface是 ARM 公司推出的 AMBAAdvanced Microcontroller Bus Architecture协议的一部分是目前高性能 SoC如 Cortex-A 系列、Zynq中最主流的总线协议。与早期的 AHB、APB 总线相比AXI 的优势在于分离的地址/数据通道读地址、读数据、写地址、写数据、写响应通道相互独立允许并行操作极大提高了吞吐率。支持 Outstanding 传输主设备可以在未收到前一个事务响应的情况下发出后续事务的地址从而隐藏总线延迟提高效率。支持 Burst 传输只需一次地址握手即可传输一“批”Burst数据非常适合 DMA 这种连续大数据块搬运的场景。灵活的互联结构易于构建复杂的多主多从系统。DMA 控制器本身就是一个 AXI Master。当它工作时它会通过 AXI 总线向存储器控制器访问 DDR或外设发起读写请求。因此一个支持 AXI 的 DMA 控制器如 Xilinx 的 AXI DMA IP 或 ARM 的 PL330 DMA其性能潜力远超传统总线上的 DMA。2.3 核心交互DMA、总线与外设的“接力赛”理解三者的关系至关重要。我们以“串口通过 DMA 发送数据”为例CPU 初始化CPU 配置串口配置 DMA源地址内存缓冲区目标地址串口数据寄存器传输长度N。DMA 启动CPU 启动 DMA 传输然后去处理其他任务。DMA 工作总线阶段DMA 控制器作为 AXI Master通过 AXI 总线发起一次读操作从内存缓冲区读取数据。然后再通过 AXI 总线可能经过总线矩阵转换到 APB发起一次写操作将数据写入串口的数据寄存器USART-DR。这个过程重复 N 次。外设工作物理阶段串口外设收到 DMA 写入 DR 寄存器的数据后启动其内部的移位寄存器将数据一位一位地通过 TX 引脚发送出去。完成通知DMA 传输完成TC当 DMA 控制器成功将第 N 个数据写入 USART-DR 后它认为自己的任务完成了可以触发 TC 中断。外设发送完成只有当串口内部的移位寄存器将最后一个比特也发送出去后串口的“发送完成”标志如 TC或“发送寄存器空”标志如 TXE才会置位。这里的“坑”就在于步骤 4 滞后于步骤 3。DMA TC 中断触发时最后一个字节可能还在串口的移位寄存器里并未真正发送到线路上。如果在 TC 中断里立即复用发送缓冲区或关闭串口就会导致数据丢失。3. 环境准备与前置条件为了后续的实践和理解我们需要明确讨论的软硬件背景。本文的示例和思路主要覆盖以下两类常见平台微控制器MCU平台芯片系列STM32F1/F4/H7 GD32 等基于 Cortex-M 内核的芯片。开发环境STM32CubeMX Keil MDK IAR Embedded Workbench 或 VSCode ARM GCC。库HAL 库、标准外设库LL库或之前的 SPL。关键外设USART SPI I2S ADC 等搭配 DMA。FPGA SoC 平台芯片系列Xilinx Zynq-7000 Zynq UltraScale MPSoC。开发环境Vivado硬件设计 Vitis软件开发 PetaLinux。关键 IPAXI DMA IP AXI VDMA IP 自定义 AXI Stream 外设。软件裸机程序或运行 Linux如使用xdma驱动。版本说明本文重点在于通用原理和设计模式代码示例将使用伪代码和典型 API 风格。具体项目请以你所使用的芯片参考手册、HAL/LL 库用户手册以及 Vivado/Vitis 版本对应的文档为准。4. 核心流程拆解以串口 DMA 发送为例让我们把一个完整的“串口 DMA 发送”流程拆解开看清每一个环节。4.1 流程总览[应用程序准备数据] - [配置并启动UART DMA发送] - [DMA通过总线搬运数据至UART DR] - [DMA传输完成中断] - [等待UART实际发送完成] - [清理/回调]4.2 分步详解步骤1外设与 DMA 初始化这是基础确保串口和 DMA 控制器时钟开启GPIO 配置正确DMA 通道与流映射正确。步骤2配置 DMA 传输这是核心配置。你需要告诉 DMASourceAddress内存缓冲区的地址如tx_buffer[0]。DestinationAddress串口数据寄存器的地址如huart1.Instance-DR。DataLength要发送的字节数。Mode是否循环模式地址是否递增内存地址递增外设地址不递增。Priority传输优先级。步骤3启动传输调用HAL_UART_Transmit_DMA()或类似的启动函数。这个函数内部会将步骤2的配置写入 DMA 控制器的寄存器。使能 DMA 通道。使能串口的 DMA 发送请求通常是设置 USART-CR3 寄存器中的 DMAT 位。一旦DMAT位被置位串口每当其发送数据寄存器TDR为空TXE时就会向 DMA 控制器发出一个传输请求。DMA 控制器收到请求后便开始执行一次“内存读 - 外设写”的操作。步骤4DMA 的搬运过程总线操作对于每次请求DMA 控制器执行以下总线事务通过总线如 AXI 转 AHB/APB读取SourceAddress处的数据。通过总线将数据写入DestinationAddress即 USART-DR。SourceAddress根据配置递增DataLength递减。 这个过程持续进行直到DataLength变为 0。步骤5传输完成中断与状态同步当DataLength递减到 0 时DMA 控制器会自动关闭传输如果配置了单次模式。触发其通道的“传输完成”TC中断。 此时CPU 进入 DMA 的 TC 中断服务函数ISR。但是正如前文所述此时最后一个字节可能还在串口的移位寄存器中。因此在 DMA TC 中断中绝对不能立即认为数据已发送完毕。正确的做法是方法A查询法在 TC 中断中清除中断标志然后轮询串口的“发送完成”标志如 USART_ISR 寄存器中的 TC 位。当 TC1 时才表示最后一个字节的停止位也已发送完毕。方法B中断法不依赖 DMA TC 中断转而使能串口自身的“发送完成”中断TCIE。当串口 TC 中断触发时数据一定已完全发出。DMA TC 中断可以用于提前释放部分资源或进行其他通知但不能作为发送完成的最终依据。5. 完整示例与代码实现下面我们分别以 STM32 HAL 库和 Xilinx Zynq 裸机为例展示关键代码。5.1 STM32 HAL 库USART DMA 发送与可靠完成判断我们以实现“方法B”使用串口 TC 中断作为完成依据为例。// 文件main.c (部分关键代码) UART_HandleTypeDef huart1; DMA_HandleTypeDef hdma_usart1_tx; uint8_t tx_buffer[] Hello, CSDN DMA!\r\n; volatile uint8_t uart_tx_complete 0; int main(void) { // HAL 初始化系统时钟、GPIO、UART、DMA 初始化代码略 // ... 假设 huart1 和 hdma_usart1_tx 已正确初始化 // 使能串口发送完成中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_TC); // 启动 DMA 传输 if (HAL_UART_Transmit_DMA(huart1, tx_buffer, sizeof(tx_buffer)-1) ! HAL_OK) { Error_Handler(); } while (1) { // 主循环处理其他任务 if (uart_tx_complete) { uart_tx_complete 0; // 此时可以安全地复用 tx_buffer 或开始下一次发送 // 例如准备新数据并再次调用 HAL_UART_Transmit_DMA } // ... 其他逻辑 } } // 文件stm32f4xx_it.c (中断服务函数) void USART1_IRQHandler(void) { // 处理串口中断 HAL_UART_IRQHandler(huart1); } // 文件重写 HAL 库的弱定义回调函数 // 当串口发送完成TC中断发生时此函数被调用 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { uart_tx_complete 1; // 设置完成标志 // 注意这里 DMA 传输早已完成我们等待的是串口物理发送完成。 } } // DMA 传输完成中断服务函数可选 void DMA2_Stream7_IRQHandler(void) { HAL_DMA_IRQHandler(hdma_usart1_tx); } // DMA 传输完成回调可选用于资源管理但不能作为发送完成依据 void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart) { /* 半传输完成 */ } // void HAL_UART_TxCpltCallback 已经在上面定义了它由串口 TC 中断触发而非 DMA TC。关键逻辑解释我们使能了UART_IT_TC发送完成中断而不是依赖 DMA 中断。HAL_UART_Transmit_DMA启动后DMA 在后台搬运数据到 USART-DR。当最后一个字节从 USART-DR 加载到移位寄存器时DMA 传输结束但数据未发完。当移位寄存器发送完最后一个比特包括停止位后硬件置位 TC 标志触发中断。HAL_UART_TxCpltCallback被调用此时数据已物理上发送完毕可以安全进行后续操作。5.2 Xilinx Zynq 裸机AXI DMA 回环测试在 Zynq 平台上AXI DMA 常用于在 PS 端 DDR 内存和 PL 端逻辑通过 AXI Stream 接口之间传输数据。以下是一个简化的内存到内存MM2S 到 S2MM回环示例。// 文件main.c #include xaxidma.h #include xparameters.h #define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID #define RX_BUFFER_BASE (0x01000000) // DDR 中的地址 #define TX_BUFFER_BASE (0x01010000) #define MAX_PKT_LEN (1024) static XAxiDma axiDma; int main() { int status; XAxiDma_Config *cfgPtr; // 1. 查找并初始化 AXI DMA 驱动 cfgPtr XAxiDma_LookupConfig(DMA_DEV_ID); status XAxiDma_CfgInitialize(axiDma, cfgPtr); if (status ! XST_SUCCESS) { /* 错误处理 */ } // 2. 确保 Scatter-Gather 模式被禁用简单模式 if (XAxiDma_HasSg(axiDma)) { /* 错误需要配置为简单模式 */ } // 3. 准备测试数据 for (int i0; iMAX_PKT_LEN; i) { *((u8*)TX_BUFFER_BASE i) i % 256; } // 4. 启动接收通道S2MM准备接收数据 status XAxiDma_SimpleTransfer(axiDma, (UINTPTR)RX_BUFFER_BASE, MAX_PKT_LEN, XAXIDMA_DEVICE_TO_DMA); if (status ! XST_SUCCESS) { /* 错误处理 */ } // 5. 启动发送通道MM2S开始发送数据 status XAxiDma_SimpleTransfer(axiDma, (UINTPTR)TX_BUFFER_BASE, MAX_PKT_LEN, XAXIDMA_DMA_TO_DEVICE); if (status ! XST_SUCCESS) { /* 错误处理 */ } // 6. 等待传输完成轮询方式 while ((XAxiDma_Busy(axiDma, XAXIDMA_DMA_TO_DEVICE)) || (XAxiDma_Busy(axiDma, XAXIDMA_DEVICE_TO_DMA))) { // 等待或进行其他任务 } // 7. 验证数据 status memcmp((void*)TX_BUFFER_BASE, (void*)RX_BUFFER_BASE, MAX_PKT_LEN); if (status 0) { xil_printf(AXI DMA Loopback Test PASSED!\r\n); } else { xil_printf(AXI DMA Loopback Test FAILED!\r\n); } return 0; }关键逻辑解释XAxiDma_CfgInitialize初始化 DMA 控制器驱动。XAxiDma_SimpleTransfer启动一次简单传输非 Scatter-Gather。需要指定内存地址、长度和方向DMA_TO_DEVICE对应 MM2SDEVICE_TO_DMA对应 S2MM。在 Zynq 中PL 端的 AXI Stream 接口逻辑通常会将 MM2S 的数据直接环回到 S2MM从而实现回环测试。XAxiDma_Busy轮询检查 DMA 通道是否忙碌。在实际应用中更推荐使用中断方式。此例中AXI DMA 控制器通过 AXI 总线具体是 AXI HP 或 AXI ACP 接口与 PS 端的 DDR 控制器通信实现高速数据传输。AXI 协议的 Burst 和 Outstanding 特性在这里被充分利用。6. 运行结果与效果验证6.1 STM32 串口 DMA 示例验证硬件连接将 STM32 开发板的 USART1 TX 引脚连接到 USB 转串口工具的 RX 引脚。编译下载使用 Keil/IAR/STM32CubeIDE 编译上述代码下载到开发板。观察现象打开串口调试助手如 Putty、SecureCRT配置正确的波特率。复位或启动开发板。你应该看到串口助手稳定、完整地接收到 “Hello, CSDN DMA!\r\n” 字符串没有任何丢失或乱码。深度验证你可以尝试在HAL_UART_TxCpltCallback中立即修改tx_buffer的内容并启动下一次发送或者切换 GPIO 引脚电平并用逻辑分析仪捕捉 TX 引脚波形和 GPIO 变化的时间点。你会看到 GPIO 变化发生在整个数据包包括停止位发送完毕之后这证明了我们的完成判断是准确的。6.2 Zynq AXI DMA 示例验证硬件设计在 Vivado 中需要配置一个 Zynq PS并添加一个 AXI DMA IP 核。将其M_AXI_MM2S和M_AXI_S2MM连接到 PS 的 HP高性能或 ACP一致性端口。将M_AXIS_MM2S和S_AXIS_S2MM直接连接形成 PL 内部的环回。生成比特流并导出硬件平台.xsa 文件。软件开发在 Vitis 中创建应用项目导入硬件平台编写上述测试代码。运行与调试将比特流下载到 FPGA。在 Vitis 中运行或调试应用程序。通过串口或 Vitis 终端查看打印信息。如果看到 “AXI DMA Loopback Test PASSED!”则证明 AXI DMA 传输功能正常PS 与 PL 之间的数据通路正确。性能观测你可以在代码中插入时间戳计算传输MAX_PKT_LEN字节所花费的时间从而估算出 DMA 通过 AXI 总线访问 DDR 的实际带宽。7. 常见问题与排查思路问题现象可能原因排查方式解决方案DMA 传输启动后数据没有发送/接收。1. DMA 或外设时钟未使能。2. DMA 通道与外设请求映射错误。3. 外设的 DMA 使能位未设置如 USART-CR3 的 DMAT/RXNEIE。4. 源/目标地址或数据长度配置错误。1. 检查 RCC 相关寄存器或 CubeMX 配置。2. 查阅芯片数据手册的 DMA 请求映射表。3. 调试时在启动后检查外设控制寄存器如 USART-CR3。4. 检查初始化代码中的地址和长度参数。1. 正确配置时钟。2. 根据手册修正通道映射。3. 确保调用正确的 HAL 函数或手动设置使能位。4. 仔细核对地址和长度。串口 DMA 发送数据不完整丢失最后几个字节。经典问题在 DMA 传输完成中断TC中误认为发送完成过早关闭了发送或操作了缓冲区。1. 用逻辑分析仪抓取 TX 引脚波形看停止位后是否还有预期数据。2. 检查代码看是在哪个中断DMA TC 还是 UART TC中进行完成处理。改用串口自身的“发送完成”TC中断或查询其 TC 标志位作为最终完成依据。DMA 传输似乎只发生了一次无法连续传输。1. DMA 配置为单次Normal模式而非循环Circular模式。2. 传输完成后未重新配置和启动 DMA。1. 检查 DMA 初始化结构体中的Mode字段。2. 检查传输完成回调函数中是否有重新启动的逻辑。1. 对于连续流数据如音频使用循环模式。2. 对于非循环模式需在每次传输完成回调中重新启动下一次传输。AXI DMA 传输卡住无法完成。1. DDR 内存地址未对齐到 Cache Line通常 32 字节。2. 传输长度不符合 AXI 突发传输要求。3. PL 端的 AXI Stream 从设备未准备好TREADY。4. Scatter-Gather 描述符配置错误。1. 检查地址是否 32 字节对齐。2. 检查长度特别是使用 Scatter-Gather 时。3. 在 Vivado 中仿真观察 AXI Stream 接口的 TREADY 信号。4. 检查描述符链表。1. 使用memalign或 Xilinx 提供的XVirtMem分配对齐内存。2. 确保长度是合适的。3. 检查 PL 逻辑确保其在复位后能正确提供 TREADY。4. 仔细核对描述符设置或先使用简单模式测试。使用 DMA 时系统出现异常或 HardFault。1. 缓冲区地址非法如指向了代码区或未映射的区域。2. 传输过程中缓冲区被意外修改内存踩踏。3. 中断嵌套或优先级配置不当。1. 检查缓冲区地址范围。2. 使用调试器观察缓冲区内容是否在传输中被改变。3. 检查 NVIC 中断优先级。1. 确保使用有效的内存区域如 SRAM、DDR。2. 使用volatile或确保在 DMA 传输期间 CPU 不会访问该缓冲区。3. 合理配置 DMA 和 UART 中断优先级。8. 最佳实践与工程建议始终区分“DMA 完成”和“外设完成”这是嵌入式 DMA 编程的第一原则。对于发送以外设的“发送完成”为准对于接收可以结合 DMA 完成中断和外设的“空闲中断”IDLE来可靠地接收不定长数据。善用双缓冲Double Buffer技术对于连续数据流如音频 I2S、ADC 采样配置 DMA 为循环双缓冲模式。当一半缓冲区传输完成时HT 中断处理已满的一半数据当全部传输完成时TC 中断处理另一半数据。这能实现无缝的数据处理。内存对齐与缓存一致性对于 Cortex-M 系列确保 DMA 缓冲区地址和长度符合总线对齐要求通常是 4 字节或 32 字节对齐以获取最佳性能。对于 Cortex-A 系列如 Zynq当 DMA 访问的缓冲区位于 CPU 带有缓存Cache的内存中时必须在 DMA 传输前后调用缓存维护操作Xil_DCacheFlush用于 CPU 写后 DMA 读Xil_DCacheInvalidate用于 DMA 写后 CPU 读否则会看到数据不一致。优先使用中断而非轮询虽然轮询简单但在 RTOS 或多任务环境中它会阻塞其他任务。使用中断回调函数的方式可以让出 CPU 资源。为 DMA 通道合理分配中断优先级DMA 中断通常用于通知传输完成其处理应快速。避免在 DMA 中断服务函数中进行复杂、耗时的操作。如果必须处理大量数据应设置标志位在更低优先级的任务或主循环中处理。在 Zynq/FPGA SoC 项目中从简单模式开始先使用 AXI DMA 的简单寄存器模式禁用 SG验证基本数据通路。理解 AXI 信号学会使用 Vivado 的 ILA集成逻辑分析仪抓取 AXI 和 AXI Stream 信号这是调试 PL 端问题的终极武器。考虑使用 VDMA如果是视频流等二维数据Xilinx 的 AXI Video DMAVDMAIP 提供了帧缓冲、异步时钟域转换等更专业的功能。代码健壮性每次 DMA 传输前检查 DMA 通道是否处于忙碌状态。在传输完成回调函数中进行必要的错误检查如 DMA 错误标志。对于关键数据考虑在 DMA 传输前后增加 CRC 校验。掌握 DMA 与 AXI本质上是在掌握嵌入式系统数据流动的底层语言。它让你从“调用库函数”的层面下沉到“理解硬件如何协作”的层面。这种理解带来的收益是巨大的你能写出更高效、更稳定的驱动能精准地定位那些最棘手的硬件相关 Bug也能在系统架构设计上做出更优的选择。建议你将本文中的示例代码在真实的开发板上运行、修改、观察现象。然后尝试去挑战更复杂的场景用 DMA 双缓冲实现一个 I2S 音频播放器或者用 Zynq 的 AXI DMA 实现一个高速数据采集系统。在实践中你可能会遇到本文未提及的问题那时芯片的参考手册和协议标准如 AMBA AXI将成为你最好的朋友。