STM32 DMA原理详解:从工作机制到实战避坑指南

发布时间:2026/8/6 13:31:01
STM32 DMA原理详解:从工作机制到实战避坑指南 1. 从“搬运工”到“指挥官”DMA在STM32中的角色定位如果你刚开始接触STM32可能已经习惯了用HAL_UART_Transmit发送数据或者在定时器中断里手动搬运ADC的采样值。这种方式下CPU就像个勤勤恳恳的“搬运工”数据每移动一个字节它都得亲自跑一趟放下手里的计算任务去处理这些重复、机械的“体力活”。当数据量一大比如要发送一帧图像或者高速采集传感器数据时你就会发现CPU忙得不可开交主程序卡顿实时性大打折扣。这时候你就需要请出那位隐藏在芯片内部的“超级搬运工”——DMA。DMA全称Direct Memory Access直接存储器访问。它的核心思想极其精妙让数据在内存与外围设备或内存与内存之间“自动”搬家完全绕过CPU的干预。你可以把CPU想象成公司的总经理DMA则是一位高效、专业的物流主管。总经理CPU只需要在项目开始时给物流主管DMA下达一份清晰的“运输任务单”配置DMA通道、源地址、目标地址、数据量等然后就可以去处理更重要的战略决策运行核心算法、响应复杂事件。物流主管会严格按照任务单指挥卡车数据总线将货物数据从仓库A源地址运到仓库B目标地址并在全部运输完成后发个消息触发中断通知总经理“任务已完成”。在整个运输过程中总经理完全不用操心每一辆卡车的装卸。在STM32的世界里DMA的身影无处不在。无论是串口收发大量数据、ADC多通道扫描转换、SPI/I2C与外部器件通信还是TIM生成PWM或捕获输入只要涉及数据的批量移动DMA几乎都是提升系统效率和实时性的首选方案。它解放了CPU使得CPU可以专注于非数据搬运的核心任务这对于电池供电的设备意味着更低的功耗对于需要快速响应的控制系统则意味着更高的性能。网上很多教程会直接给你一段Cubemx配置图和代码告诉你“这样配就能用”。但这就像只给你一张填好的快递单却不告诉你收件人、地址、物品重量该怎么填。这篇内容我想和你聊聊在STM32中玩转DMA你真正需要弄明白的那几件事它到底是怎么工作的各种模式普通、循环、双缓冲该怎么选为什么我的DMA发送完成中断只进了一次数据对齐的坑在哪里我们不止步于“配置”更要深入“原理”和“避坑”让你能从“照抄代码”变成“心中有数手中有术”。2. DMA的核心工作机制通道、请求与仲裁要驾驭DMA首先得理解它的组织架构和工作流程。STM32的DMA控制器就像一个拥有多条专用流水线的物流中心。2.1 通道与请求映射谁有资格使用DMA不是任何外设都能随时调用DMA。DMA控制器提供了多个独立的通道Channel。每个通道在某一时刻只能服务于一个外设的特定请求。例如在STM32F1系列中DMA1有7个通道每个通道可以被映射到多个可能的外设请求上但需要程序员通过配置寄存器来指定当前服务于哪一个。关键概念硬件请求与软件触发硬件请求这是DMA工作的主要方式。外设在准备好数据如USART的发送数据寄存器空或需要数据如USART的接收数据寄存器非空时会向DMA控制器发出一个硬件信号请求。DMA控制器检测到这个请求后如果该通道已使能且优先级最高就会启动一次数据传输。例如配置USART1_TX使用DMA那么每当USART的数据寄存器空TXE标志置位就会自动向DMA“要数据”DMA则响应这个请求从内存搬一个数据到USART-DR寄存器。软件触发某些DMA流/通道也支持通过软件置位特定寄存器位来启动传输。这在内存到内存的传输中很常见。你需要查阅具体的《STM32参考手册》中的“DMA请求映射”表格这是你的“交通法规”。比如你想用DMA传输ADC1的规则组数据查表发现ADC1对应DMA1的通道1。那么你就必须将DMA1的通道1配置给ADC1使用不能乱分配。2.2 传输事务的三要素源、目的和搬运量配置一次DMA传输本质上是定义了一个“传输事务”。这个事务包含三个核心要素理解它们对避免错误至关重要源地址Source Address数据从哪里来。可以是某个外设的数据寄存器地址如USART1-DR也可以是内存中某个数组的地址如adc_buffer。目的地址Destination Address数据到哪里去。同样可以是外设寄存器地址或内存地址。传输数据量Data Amount通常通过两个参数共同决定数据宽度Data Width每次传输操作移动多少位的数据。可选8位字节、16位半字、32位字。这里有一个大坑源和目的的数据宽度可以不同DMA控制器会自动进行打包/解包但这需要配合外设的数据格式配置错误会导致数据错乱。例如ADC是12位分辨率结果寄存器是16位的通常右对齐如果你设置DMA的数据宽度为8位那么一次ADC转换的结果就会被拆成两次传输完全错误。通常源和目的的数据宽度应保持一致。传输次数Number of Data Items / Data Count需要执行多少次上述“宽度”的传输。例如你要传输一个包含100个uint16_t元素的数组数据宽度设置为16位传输次数就设置为100。DMA控制器内部有两个重要的计数器当前数据地址寄存器和剩余数据数量寄存器。每成功完成一次传输源/目的地址会根据你的配置递增、递减或固定自动更新剩余数据数量减1。当剩余数量减到0时一次传输事务就完成了。2.3 优先级与仲裁当多个搬运工抢道时一个DMA控制器有多个通道如果多个通道同时发出请求谁先被服务这就涉及到优先级仲裁。软件优先级每个通道可以设置为4个等级非常高、高、中、低。当请求同时到来时优先级高的通道先被服务。硬件优先级如果两个通道软件优先级相同则通道编号小的拥有更高的硬件优先级例如通道2优先于通道4。仲裁机制保证了在复杂系统中关键的数据流如实时音频DAC输出能获得更及时的DMA服务避免数据丢失。注意DMA传输占用的是系统总线AHB。在DMA传输期间如果CPU也需要访问总线比如取指令、读写内存总线仲裁器会介入。高优先级的DMA传输可能会暂时阻塞CPU的访问导致CPU等待Stall。这在极端高性能场景下需要考虑但对于大多数应用DMA带来的CPU解放收益远大于偶尔的总线竞争开销。3. 模式选择普通、循环与双缓冲应对不同场景DMA提供了几种工作模式应对不同的数据流场景。选对模式事半功倍选错模式调试到头秃。3.1 普通模式Normal Mode一次性的快递任务这是最直观的模式。你设置好传输总量比如1000个数据启动DMA。DMA会忠实地搬运完这1000个数据然后自动关闭该通道硬件将通道使能位EN清零并触发“传输完成中断”TC中断。之后该通道不再响应任何请求除非你重新配置并启动它。适用场景发送一段固定的命令或数据帧。例如通过SPI DMA发送一屏LCD的初始化命令序列。触发一次性的数据采集。例如手动触发ADC通过DMA采集1024个点后停止。坑点提醒 在普通模式下传输完成后通道被禁用。如果你需要在中断服务程序里再次启动传输务必先清除相应的中断标志位再重新设置传输数据量NDTR寄存器并重新使能通道EN1。HAL库中HAL_DMA_Start函数通常会帮你做一部分但理解底层操作很重要。3.2 循环模式Circular Mode永不停止的传送带在循环模式下DMA在传输完设定的数据量后不会停止而是自动将源/目标地址和传输数据量计数器重置为初始值然后从头开始新一轮传输。整个过程就像一个环形的传送带周而复始。适用场景持续不断的数据流。例如实现一个“回声”功能将USART通过DMA接收到的数据实时地通过DMA发送回去。周期性数据采集与处理。例如用ADC通过DMA循环采集模拟信号填充一个固定大小的缓冲区。你的主程序或另一个DMA内存到内存可以在后台处理这个缓冲区中“已经装满”的数据而ADC的采集永不停止实现了连续采样。坑点提醒 循环模式下“传输完成中断”TC依然会在每一轮传输结束时触发。这为你提供了精确的“缓冲区换帧”时间点。例如你设置了一个1000元素的ADC采样缓冲区工作在循环模式。每次TC中断触发都意味着ADC已经写满了整个缓冲区一次可能覆盖了旧数据。你可以在TC中断里将缓冲区指针切换到一个备份缓冲区进行处理实现“乒乓操作”的雏形。3.3 双缓冲模式Double Buffer Mode高效的“乒乓操作”硬件实现双缓冲模式是循环模式的一个高级变种专门为解决“数据处理速度跟不上采集速度”而设计。它需要两个大小相同的缓冲区缓冲区0和缓冲区1。工作原理DMA配置为指向两个缓冲区M0AR和M1AR寄存器分别存储两个内存地址并工作在双缓冲模式。启动后DMA首先使用缓冲区0进行传输。当缓冲区0传输满达到一半数据量或全部取决于配置时DMA自动切换到使用缓冲区1同时可以触发一个“半传输完成中断”HT中断。此时CPU可以安全地处理已经填满的缓冲区0的数据因为DMA正在操作缓冲区1。当缓冲区1也传输满时DMA再次切换回缓冲区0并触发“传输完成中断”TC中断。此时CPU可以处理缓冲区1的数据。 如此往复DMA在缓冲区0和1之间来回切换形成“乒乓”操作。CPU总有一个完整的、未被DMA写入的缓冲区可供处理完美避免了处理数据时缓冲区被覆盖的风险。适用场景音频流处理一边通过I2S DMA接收音频数据填充缓冲区A一边处理之前已满的缓冲区B的数据如添加音效然后输出。摄像头数据流连续接收一帧图像数据双缓冲可以确保在处理上一帧时下一帧的接收不受影响。任何需要连续、实时处理大数据流的场景。配置关键 在HAL库或标准库中配置双缓冲模式需要正确设置DMA_InitStruct.Mode为双缓冲模式并分别给M0AR和M1AR赋值。同时要充分利用HT和TC中断来管理缓冲区指针。一个常见的编程模式是// 在DMA中断服务函数中 if (hdma-Instance-ISR DMA_FLAG_HT1) { // 半传输完成缓冲区0满 // 处理 buffer0 的数据 __HAL_DMA_CLEAR_FLAG(hdma, DMA_FLAG_HT1); } if (hdma-Instance-ISR DMA_FLAG_TC1) { // 传输完成缓冲区1满 // 处理 buffer1 的数据 __HAL_DMA_CLEAR_FLAG(hdma, DMA_FLAG_TC1); }4. 实战拆解以USART DMA发送与接收为例理论说得再多不如一行代码。我们以最常用的USART DMA传输为例看看如何配置并深入那些容易出错的细节。这里以STM32F4系列和HAL库为例但原理通用。4.1 USART TX DMA如何可靠地发送一帧数据假设我们要通过USART1发送一个字符串Hello, DMA!\r\n。步骤一Cubemx图形化配置使能USART1模式选择“Asynchronous”。在DMA设置选项卡为USART1_TX添加一个DMA流Stream。选择通道通常自动匹配。方向设为“Memory To Peripheral”。模式Mode根据需求选择“Normal”发一次或“Circular”循环发慎用。优先级Priority默认“Low”即可除非有更高实时性要求。数据宽度Data Width这是关键。USART数据寄存器是8位或9位的取决于字长。如果我们发送的是char数组这里选“Byte”。如果内存中是uint16_t数组但想以两个字节发送也选“Byte”DMA会分两次搬运一个16位数据。使能“Memory Increment”内存地址递增外设地址不递增。步骤二生成代码与关键API分析Cubemx会生成MX_DMA_Init()和MX_USART1_UART_Init()。发送数据的核心函数是HAL_UART_Transmit_DMA()。char tx_data[] Hello, DMA!\r\n; HAL_UART_Transmit_DMA(huart1, (uint8_t*)tx_data, strlen(tx_data));这个函数内部做了几件事将目标数据地址和长度装载到DMA寄存器。使能USART的DMA发送请求USART_CR3寄存器中的DMAT位。使能DMA流。步骤三判断发送完成与中断处理调用HAL_UART_Transmit_DMA后函数立即返回数据发送在后台由DMA进行。如何知道发送完了轮询方式不推荐会阻塞。但可以用HAL_DMA_PollForTransfer或检查__HAL_DMA_GET_FLAG(hdma, DMA_FLAG_TCx)。中断方式推荐使能DMA流的“传输完成中断”TCIE。在生成的代码中Cubemx可能已经帮我们使能了全局中断但我们需要自己编写中断回调函数。在stm32f4xx_it.c中找到DMAx_Streamy_IRQHandler中断服务函数它已经调用了HAL_DMA_IRQHandler。我们需要在用户文件中重写弱定义的传输完成回调函数// 在 main.c 或其它用户文件中 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // USART1 DMA 发送完成可以在这里进行下一步操作例如点亮一个LED或者准备下一包数据 // 注意在Normal模式下发送完成后DMA通道已自动禁用。 } }一个经典问题“DMA发送完成中断”到底在什么时候触发这个问题在搜索热词里高频出现。触发点不是USART物理引脚上的最后一个比特发送完毕而是DMA控制器将最后一个数据从内存搬运到USART-DR寄存器的时刻。也就是说当DMA把最后一个字节塞进串口的发送数据寄存器后它就认为任务完成触发TC中断。此时这个字节可能还在串口的发送移位寄存器中尚未完全发出到TX引脚。 如果你需要在最后一个比特也真正发送出去后才进行某些操作例如切换IO方向就需要再等待USART本身的“发送完成”TC标志位。可以结合使用void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { // 等待最后一个字节从移位寄存器发出 while((huart-Instance-SR USART_SR_TC) 0) {}; // 现在可以安全地进行后续操作了比如关闭使能等 }4.2 USART RX DMA连续接收不定长数据IDLE中断法接收不定长数据是串口通信的常见需求。DMA串口空闲中断IDLE是最高效的方案之一。原理将USART RX配置为DMA循环接收模式指向一个足够大的缓冲区如rx_buffer[256]。使能USART的IDLE中断当串口总线上一段时间没有收到数据时产生。启动DMA接收HAL_UART_Receive_DMA(huart1, rx_buffer, 256)。当一帧数据到来DMA会自动将数据搬运到rx_buffer。数据发送完毕后总线空闲触发USART的IDLE中断。在IDLE中断服务程序中我们可以计算出本次接收到的数据长度数据长度 预设缓冲区长度 - DMA当前剩余未传输次数。然后处理这一帧数据并重置DMA重新设置数据长度和缓冲区地址以准备接收下一帧。配置与代码实现Cubemx中使能USART全局中断NVIC并在代码中手动使能IDLE中断__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);在USART中断服务函数中处理IDLE事件void USART1_IRQHandler(void) { if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除IDLE标志位 // 计算接收到的数据长度 uint16_t rx_len BUFFER_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 处理 rx_buffer 中前 rx_len 个字节的数据 process_data(rx_buffer, rx_len); // 重新启动DMA接收指向缓冲区开头准备下一次接收 // 需要先禁用再使能或者使用HAL库提供的重启函数 HAL_UART_DMAStop(huart1); HAL_UART_Receive_DMA(huart1, rx_buffer, BUFFER_SIZE); } HAL_UART_IRQHandler(huart1); // 调用HAL库默认中断处理 }重要提示HAL_UART_Receive_DMA在循环模式下如果缓冲区满了DMA会从缓冲区头部重新开始覆盖写入根据指针是否递增。因此你的处理速度必须快于数据接收速度或者缓冲区要足够大否则会丢失数据。IDLE中断法完美解决了“不定长”和“实时处理”的问题。5. 进阶话题与避坑指南掌握了基本操作我们来看看那些容易让人栽跟头的进阶问题和细节。5.1 数据对齐与传输宽度不匹配的陷阱这是DMA错误中最隐蔽的一类。假设你的源数据是内存中的一个uint32_t数组32位而目标外设比如一个32位的数据寄存器也期望32位数据。如果你错误地将DMA的数据宽度配置为8位Byte会发生什么DMA会认为你要传输4倍数量的8位数据。例如你想传输1个uint32_t数据值为0x12345678。如果配置为32位宽度一次传输即可完成。如果配置为8位宽度DMA会执行4次传输第一次送0x78到目标地址第二次送0x56第三次送0x34第四次送0x12。如果你的外设寄存器是32位只写寄存器那么只有最后一次写入0x12是有效的前三次写入可能被忽略或覆盖最终寄存器里只有0x12数据完全错误。黄金法则在配置DMA时务必确保源地址、目的地址以及对应的数据宽度与实际的数据类型、外设寄存器宽度相匹配。如果不确定查阅芯片数据手册中外设寄存器的描述。5.2 内存到内存传输加速数据搬运的利器DMA不仅可以服务外设还可以在两个内存区域之间快速搬运数据且不消耗CPU周期。这在图像处理、缓冲区拷贝、数据重排等场景下非常有用。配置要点方向设置为“Memory To Memory”。通常需要使能MEM2MEM模式某些系列是独立的模式位。源和目的的内存地址递增都需要根据情况使能。触发方式为软件触发调用HAL_DMA_Start或类似函数。示例使用DMA快速初始化一片内存区域// 将常量数组 const_data 拷贝到目标数组 target_array HAL_DMA_Start(hdma_memtomem_dma2_stream0, (uint32_t)const_data, (uint32_t)target_array, DATA_SIZE); // 等待传输完成 HAL_DMA_PollForTransfer(hdma_memtomem_dma2_stream0, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);注意内存到内存传输会占用大量总线带宽可能会暂时影响CPU和其它外设的访问性能。5.3 DMA与Cache的一致性难题针对Cortex-M7等带Cache内核对于STM32F7/H7等使用Cortex-M7内核的系列它们有数据缓存D-Cache。这引入了一个复杂问题CPU和DMA看到的内存可能不是同一份。CPU写DMA读的场景CPU处理完的数据放在缓存里还没来得及写回内存Write-Back策略。此时启动DMA去发送这片内存的数据DMA会直接从内存而非缓存读取读到的就是旧数据。DMA写CPU读的场景DMA从外设接收数据直接写入内存。但CPU读取数据时可能先访问缓存缓存里是旧数据导致CPU看不到DMA刚写入的新数据。解决方案使用非缓存内存区域在链接脚本中定义一块SRAM区域并通过MPU配置为“Non-Cacheable”。将DMA缓冲区放在这块内存中。这是最彻底的方法。手动维护缓存一致性在关键操作前后使用SCB系统控制块提供的缓存维护指令DMA传输开始前CPU写内存后调用SCB_CleanDCache_by_Addr()确保CPU缓存中的数据写回内存。DMA传输结束后CPU读内存前调用SCB_InvalidateDCache_by_Addr()使CPU缓存失效强制从内存重新加载数据。5.4 调试技巧当DMA不工作时如何排查检查时钟DMA控制器和外设的时钟是否都已使能__HAL_RCC_DMAx_CLK_ENABLE()和__HAL_RCC_USARTx_CLK_ENABLE()。检查通道映射确认DMA流/通道与外设请求的映射关系是否正确。查参考手册的“DMA请求映射表”。检查NVIC中断如果使用了中断是否在NVIC中使能了对应的DMA流中断优先级配置是否合理检查传输完成标志在调试器中查看DMA寄存器如ISR中断状态寄存器中的TCIFx传输完成中断标志是否置位NDTR剩余数据计数寄存器是否在递减检查外设的DMA使能位例如USART的CR3寄存器中的DMAT发送DMA使能或DMAR接收DMA使能位是否置1简化测试先尝试最简单的内存到内存传输排除外设配置的影响。再逐步增加外设和中断逻辑。DMA是STM32单片机性能飞跃的关键组件。从生疏到熟练必然要经历一些调试和踩坑的过程。但一旦你掌握了它就能设计出响应更及时、运行更高效、功耗更低的应用。它让你从繁琐的数据搬运中解脱出来真正专注于产品的核心逻辑。希望这篇内容能帮你建立起对DMA清晰而深入的理解在项目中放心大胆地使用这个强大的工具。