
在实际嵌入式开发、硬件驱动编写和实时系统设计中直接内存访问DMA是一个绕不开的核心技术。它允许硬件子系统如外设直接与主内存进行数据交换而无需中央处理器CPU的持续干预。这种机制对于需要高速、大批量数据传输的场景至关重要例如网络数据包处理、音频流播放、图像传感器数据采集等。如果CPU需要亲自搬运每一个字节的数据其计算资源将被大量占用导致系统整体性能急剧下降甚至无法满足实时性要求。本文将以一个具体的工程视角深入探讨DMA特别是围绕“26 DMA 26DMA-11”这一可能指向特定芯片型号或DMA通道标识的上下文。我们将从DMA的基本工作原理讲起逐步深入到其在典型微控制器如STM32系列中的实际配置与应用。无论你是刚开始接触底层开发的嵌入式新手还是希望梳理DMA配置细节的有经验开发者通过本文你将能够理解DMA的工作机制掌握在具体项目中配置和使用DMA的完整流程并学会排查DMA传输中常见的故障。1. 理解DMA为什么它是性能的关键在深入代码之前必须厘清DMA解决的根本问题以及它是如何工作的。这决定了你能否在正确的场景使用它并能在出问题时找到方向。1.1 CPU与DMA的分工解放CPU的核心思想想象一个场景一个串口UART每秒接收1兆字节1MB/s的数据。如果让CPU来负责它需要不断轮询串口的状态寄存器检查是否有新数据到达然后执行“读取数据寄存器 - 写入内存目标地址”的指令。这个过程中CPU的绝大部分时间都花在了等待和搬运数据这种简单重复劳动上无法执行更有价值的计算任务如协议解析、算法处理。DMA控制器就是专门负责这类“搬运工”工作的独立硬件单元。它的工作流程可以概括为初始化CPU告诉DMA控制器三件事“数据从哪里来”源地址、“数据到哪里去”目标地址以及“要搬多少”传输数量。启动传输CPU启动DMA传输然后就可以去执行其他任务。传输执行当外设如串口准备好数据或需要发送数据时会向DMA控制器发送一个请求信号。DMA控制器接管系统总线直接在内存和外设之间完成一次数据传输。传输完成当约定的传输数量完成后DMA控制器会发出一个中断信号通知CPU“你要的数据都搬完了可以来处理了”。这个过程实现了CPU与外设工作的解耦和并行化极大地提升了系统效率。1.2 DMA传输模式与关键概念根据数据流方向DMA传输主要分为内存到外设Memory-to-Peripheral常用于发送数据如通过DMA将内存中的音频缓冲区数据发送到I2S接口。外设到内存Peripheral-to-Memory常用于接收数据如通过DMA将ADC转换的结果存入内存数组。内存到内存Memory-to-Memory纯粹的内存数据搬运可以由DMA控制器完成进一步减轻CPU负担。理解以下几个关键概念对配置至关重要通道ChannelDMA控制器通常有多个独立的通道每个通道可以服务于一个特定的外设如UART1的发送、ADC1的转换等。26DMA-11这类标识很可能指代某个具体的DMA通道号例如DMA2的通道11。数据流Stream在一些高级的DMA架构如STM32的DMA2D中数据流是比通道更细粒度的概念一个通道可能包含多个数据流用于更复杂的数据流管理。传输宽度Data Width指单次传输操作的数据单位大小如字节8位、半字16位、字32位。源和目标的宽度通常需要匹配或由DMA控制器处理打包/解包。仲裁器Arbitrator当多个DMA通道同时请求传输时仲裁器根据预设的优先级软件可配置决定哪个通道先使用总线。循环模式Circular Mode在此模式下当一次传输完成后DMA会自动重置传输计数器并重新开始形成一个“乒乓”缓冲区。这对于需要连续不断传输数据的场景如音频流非常有用程序员只需处理已经填满的缓冲区即可。2. 环境准备与项目结构我们以广泛使用的ARM Cortex-M系列微控制器如STM32为例展示DMA的配置流程。虽然不同厂商的库函数和寄存器名称可能不同但核心思想是相通的。2.1 硬件与软件环境硬件平台一块支持DMA的微控制器开发板如STM32F4 Discovery、Nucleo系列。集成开发环境IDESTM32CubeIDE、Keil MDK或IAR Embedded Workbench。开发库HAL库STM32CubeMX生成或标准外设库LL库。本文示例基于STM32 HAL库因其抽象程度高便于理解。调试工具ST-LINK、J-Link等调试器用于下载程序和单步调试。2.2 使用STM32CubeMX进行图形化配置对于初学者使用STM32CubeMX工具可以直观地配置DMA并自动生成初始化代码是快速上手的绝佳方式。选择芯片型号在CubeMX中创建新工程选择你的具体MCU型号。启用外设和DMA在Pinout Configuration标签页启用你需要使用的外设例如USART1并设置其工作模式异步通信。切换到DMA Settings标签页或在对应外设的配置中寻找DMA选项。点击Add为USART1_RX接收和USART1_TX发送分别添加DMA请求。配置DMA参数Direction选择Peripheral To Memory接收或Memory To Peripheral发送。Priority根据需求选择LowMediumHighVery High。Mode选择Normal单次传输或Circular循环模式。Data Width根据外设数据寄存器宽度选择通常ByteUART或Half Word/WordADC。生成代码配置时钟树后点击Generate Code。CubeMX将生成包含DMA初始化的main.c、stm32f4xx_hal_msp.c等文件。2.3 手动编码的项目结构如果你选择手动编码或需要深入理解一个典型的DMA项目包含以下关键部分Your_Project/ ├── Core/ │ ├── Inc/ │ │ └── main.h // 全局变量、缓冲区定义 │ ├── Src/ │ │ ├── main.c // 主循环、DMA启动逻辑 │ │ ├── stm32f4xx_it.c // 中断服务函数DMA完成中断 │ │ └── syscalls.c │ └── Startup/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F4xx_HAL_Driver/ └── ... (其他配置文件)核心关注的文件是main.c初始化与启动和stm32f4xx_it.c中断处理。3. 实战配置UART使用DMA接收不定长数据我们以一个经典且实用的案例为例使用UART串口以DMA方式接收不定长度的数据帧。难点在于DMA需要知道传输数量但串口数据是异步、不定长的。解决方案是使用DMA循环模式接收数据到环形缓冲区并结合串口空闲中断Idle Interrupt来判定一帧数据接收完成。3.1 初始化步骤与代码首先在main.c中定义必要的全局变量和缓冲区。/* Private variables ---------------------------------------------------------*/ UART_HandleTypeDef huart1; DMA_HandleTypeDef hdma_usart1_rx; #define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE]; // DMA循环接收缓冲区 volatile uint16_t rx_len 0; // 接收到的数据长度 volatile uint8_t rx_flag 0; // 数据接收完成标志在main函数初始化部分调用HAL库函数进行初始化。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); // 初始化DMA控制器 MX_USART1_UART_Init(); // 初始化UART1 UART_DMA_Init(); // 自定义的UART DMA接收初始化函数 // ... 其他初始化 while (1) { if(rx_flag 1) { // 处理接收到的数据数据在rx_buffer中长度为rx_len Process_UART_Data(rx_buffer, rx_len); // 处理完成后重新启动DMA接收为下一帧数据做准备 UART_Start_DMA_Receive(); rx_flag 0; } // ... 主循环其他任务 } }关键的初始化函数UART_DMA_Init实现如下void UART_DMA_Init(void) { // 1. 关联DMA句柄与UART接收DMA请求 __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); // 2. 配置DMA用于UART接收 hdma_usart1_rx.Instance DMA2_Stream2; // 根据数据手册USART1_RX可能对应DMA2 Stream2 hdma_usart1_rx.Init.Channel DMA_CHANNEL_4; hdma_usart1_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址不递增 hdma_usart1_rx.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 hdma_usart1_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode DMA_CIRCULAR; // 循环模式 hdma_usart1_rx.Init.Priority DMA_PRIORITY_MEDIUM; hdma_usart1_rx.Init.FIFOMode DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(hdma_usart1_rx) ! HAL_OK) { Error_Handler(); } // 3. 启动DMA循环接收 if(HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); } // 4. 使能UART的空闲线路中断Idle Line Interrupt __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); }3.2 中断服务函数处理空闲中断DMA在后台默默搬运数据到rx_buffer。我们需要在串口空闲中断中判断一帧数据是否结束。空闲中断在串口线路上一段时间通常是一个字符传输时间没有新数据时触发。在stm32f4xx_it.c中找到USART1的中断服务函数USART1_IRQHandler并添加空闲中断处理逻辑。void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ // 检查是否是空闲中断 if((__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除空闲中断标志 // 关键计算获取当前DMA已经搬运了多少数据 // 当前写入位置 缓冲区总大小 - DMA剩余传输计数 uint16_t temp_len RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 计算本次接收的数据长度考虑循环缓冲区的回绕 // 这是一个简化逻辑实际项目需要更健壮的环形缓冲区管理 rx_len temp_len; // 设置接收完成标志通知主循环处理 rx_flag 1; // 注意这里没有停止DMA因为它是循环模式会继续接收下一帧数据。 // 主循环在处理完数据后需要知道新的“起始点”这需要更复杂的缓冲区管理。 } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }3.3 主循环数据处理与DMA重启主循环检测到rx_flag后处理数据。由于是循环缓冲区直接使用rx_buffer和rx_len是不安全的因为DMA可能正在覆盖尚未处理的数据。一个更健壮的做法是使用“双缓冲区”或计算有效的缓冲区段。一个简化的重启函数UART_Start_DMA_Receive在普通模式下更常用可能如下所示但在我们的循环模式例子中它主要用于重置状态void Process_UART_Data(uint8_t* data, uint16_t len) { // 在这里处理你的数据例如通过另一个串口打印出来 HAL_UART_Transmit(huart2, data, len, 1000); // 或者解析协议等 } void UART_Start_DMA_Receive(void) { // 对于循环模式DMA一直在运行无需重启。 // 此函数主要用于重置软件状态标志或切换到新的缓冲区。 // 更完善的实现需要管理读/写指针。 rx_flag 0; rx_len 0; // 如果需要可以在这里重新指定DMA的目标内存地址双缓冲区切换 // HAL_UART_Receive_DMA(huart1, another_buffer, RX_BUFFER_SIZE); }4. 关键配置详解与参数选择上面的代码包含了多个关键配置理解它们为何如此设置是避免错误的核心。4.1 DMA初始化结构体成员解析下表详细解释了hdma_usart1_rx.Init中各成员的作用和常见设置参数含义常见设置UART接收为例配置错误的影响Instance指定使用哪个DMA数据流/通道。DMA2_Stream2传输根本无法启动或服务于错误的外设。Channel指定该数据流关联的外设请求通道。DMA_CHANNEL_4同Instance错误必须查阅芯片数据手册的DMA请求映射表。Direction数据传输方向。DMA_PERIPH_TO_MEMORY数据流向反导致数据写入错误位置。PeriphInc外设地址是否在每次传输后递增。DMA_PINC_DISABLE外设如UART数据寄存器只有一个固定地址绝不能递增。MemInc内存地址是否在每次传输后递增。DMA_MINC_ENABLE如果禁用所有接收的数据会堆叠在同一个内存地址只有最后一字节有效。PeriphDataAlignment外设数据宽度对齐。DMA_PDATAALIGN_BYTE必须与外设数据寄存器宽度一致。UART是8位。MemDataAlignment内存数据宽度对齐。DMA_MDATAALIGN_BYTE通常与PeriphDataAlignment一致或为内存访问效率设为字。ModeDMA工作模式。DMA_NORMAL单次或DMA_CIRCULAR循环单次模式传输完需手动重启循环模式需注意缓冲区管理。Priority通道优先级。DMA_PRIORITY_MEDIUM多个DMA同时竞争总线时低优先级通道可能被“饿死”。FIFOMode是否启用FMA FIFO。DMA_FIFOMODE_DISABLE对于简单外设可禁用启用可用于数据打包或防止外设数据溢出。4.2 外设与DMA的关联Link__HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx);这行宏至关重要。它将DMA句柄的指针赋值给UART句柄结构体中的对应成员hdmarx。这样当调用HAL_UART_Receive_DMA时HAL库内部就知道该使用哪个DMA控制器来为这个UART的接收服务。忘记链接会导致DMA无法响应外设请求。4.3 循环模式下的缓冲区管理挑战循环模式带来了便利也带来了复杂性。中断服务函数中计算temp_len的公式RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(...)获取的是DMA当前写指针相对于缓冲区起始点的偏移量。但在长时间运行中DMA的写指针可能已经绕回并覆盖了未读数据溢出。因此生产代码必须实现一个完整的环形缓冲区维护独立的读指针软件控制和写指针由DMA当前位置推导并确保读指针始终追赶写指针且不会重叠。5. 运行验证与调试技巧编写代码后需要通过实际测试来验证DMA是否按预期工作。5.1 基础验证步骤编译与下载确保代码无错误编译并下载到开发板。发送测试数据使用串口调试助手如Putty、SecureCRT向开发板的UART1发送一串数据例如Hello DMA!。观察结果如果配置了处理函数将数据转发到另一个串口UART2打印则在连接UART2的PC上应看到相同的Hello DMA!。或者你可以在Process_UART_Data函数内设置断点观察data和len是否正确。压力测试以最高波特率连续发送大量数据观察是否丢包。同时监控CPU使用率可以通过翻转一个GPIO引脚并用逻辑分析仪测量其空闲时间占比来粗略估算应与未使用DMA时有显著下降。5.2 使用调试器深入观察当传输不成功时调试器是强大的工具检查外设和DMA时钟确保__HAL_RCC_USART1_CLK_ENABLE()和__HAL_RCC_DMA2_CLK_ENABLE()已被调用。时钟未开启是常见错误。查看寄存器状态在调试器中查看DMA控制寄存器如DMA_SxCREN位是否置1传输完成中断TCIE是否使能UART控制寄存器DMA接收使能位DMAR是否置1中断状态寄存器检查是否有错误标志如传输错误TEIF、FIFO错误FEIF被置起。观察内存在rx_buffer的地址上设置内存观察点看当串口收到数据时该内存区域的内容是否发生变化。6. 常见问题排查清单DMA配置相对复杂以下是一些典型问题及排查思路。问题现象可能原因检查点与解决方案DMA根本不动数据收不到1. DMA或外设时钟未使能。2. DMA通道/数据流映射错误。3. 外设与DMA未正确链接__HAL_LINKDMA。4. DMA传输未启动未调用HAL_UART_Receive_DMA。1. 在main初始化早期确认时钟使能函数被调用。2. 查阅芯片参考手册的“DMA请求映射”表核对Instance和Channel。3. 检查__HAL_LINKDMA宏是否在DMA初始化后、外设启动前被调用。4. 单步调试确认启动函数被成功执行。只能收到第一个或最后一个数据1. 内存地址递增MemInc配置错误。2. 传输数量NDTR设置过小或未正确重置。1. 确认hdma_usart1_rx.Init.MemInc DMA_MINC_ENABLE。2. 检查HAL_UART_Receive_DMA的第三个参数Size是否正确。在普通模式下每次传输完成后需要重新启动并设置Size。数据错乱或对齐错误1. 外设与内存的数据宽度DataAlignment不匹配。2. 源/目标地址对齐不符合硬件要求。1. 确保PeriphDataAlignment与外设寄存器宽度一致MemDataAlignment与缓冲区类型匹配如uint8_t用BYTE。2. 确保内存缓冲区地址符合对齐要求例如字传输时地址需4字节对齐。进入DMA传输完成中断但数据不全1. 传输完成中断TC过早触发。2. 外设如UART本身未收到足够数据线路问题。3. 在循环模式下TC中断在每个缓冲区循环完成时都会触发不代表一帧结束。1. 检查传输数量设置是否正确。2. 使用空闲中断而非TC中断来判断不定长数据帧结束。3. 在循环模式中避免依赖TC中断处理业务数据。系统不稳定偶尔卡死1. DMA与CPU或其他总线主控如另一个DMA访问内存冲突。2. 中断嵌套或优先级配置不当导致死锁。3. 缓冲区溢出循环模式下读指针未及时追赶。1. 确保DMA访问的内存区域是可共享的如SRAM且非核心代码正在执行的关键区域。2. 合理配置DMA中断和UART中断的NVIC优先级。3. 实现带溢出检查的环形缓冲区并提高数据处理速度或增大缓冲区。使用CubeMX生成代码后仍需手动修改CubeMX的DMA配置可能不完整特别是高级功能。1. 生成代码后务必检查main.c中的MX_DMA_Init函数和对应外设的MX_*_Init函数看DMA句柄是否被正确初始化并链接。2. 中断使能如空闲中断通常需要手动添加代码。7. 最佳实践与扩展方向掌握了基础用法后以下实践能让你的DMA应用更稳健、高效。7.1 针对生产环境的建议启用错误中断除了传输完成中断务必使能DMA的传输错误中断TEIE。在错误中断服务函数中记录错误类型可通过HAL_DMA_GetError获取并实现安全恢复机制如重置DMA通道。双缓冲区Ping-Pong Buffer对于高速连续数据流使用两个缓冲区。DMA交替填充这两个缓冲区当其中一个满时产生中断通知CPU处理同时DMA无缝切换到另一个缓冲区继续工作。这几乎消除了处理延迟导致的丢数据风险。内存一致性如果CPU和DMA会访问同一块内存区域CPU读DMA写的数据需要注意缓存一致性问题尤其在带有Cache的Cortex-M7等内核中。确保使用可缓存性正确的内存区域如DTCM或SRAM1或在CPU访问前执行缓存无效化SCB_InvalidateDCache_by_Addr操作。超时与看门狗在等待DMA传输完成的循环中如果使用轮询模式一定要加入超时机制。同时确保DMA中断服务函数执行时间尽可能短避免影响系统实时性必要时配合看门狗。7.2 扩展学习方向内存到内存传输尝试使用DMA在内部SRAM或外部SDRAM的两个区域之间搬运大块数据测量其与CPUmemcpy的性能差异。多通道与优先级配置两个外设如ADC和UART同时使用DMA并设置不同的优先级观察在高负载下低优先级通道的传输是否被延迟。链接传输模式Linked List在一些高级DMA控制器中可以预先配置一个传输描述符链表DMA会自动按顺序执行多个不同源/目标地址和长度的传输实现复杂的数据流编排。与RTOS结合在FreeRTOS或ThreadX等实时操作系统中DMA传输完成中断通常用于释放信号量或发送消息给任务从而将耗时的数据处理工作转移到低优先级任务中保持中断响应速度。DMA是嵌入式系统释放CPU潜力的利器。从理解其“搬运工”的本质开始通过仔细配置通道、方向、模式等参数并妥善处理中断与缓冲区管理你就能可靠地将它集成到各种数据密集型应用中。开始时建议从CubeMX生成的代码框架入手通过调试器观察寄存器与内存的变化逐步加深对底层机制的理解最终实现稳定高效的数据传输子系统。