STM32 DMA循环模式与非循环模式:原理、选择与实战避坑指南 1. 项目概述从阻塞到解放DMA模式选择的实战意义搞过Stm32的朋友尤其是用过串口、ADC这些外设的肯定都经历过CPU被“绑死”的烦恼。比如用串口发送一长串数据程序就得傻傻地等着一个个字节发完或者用ADC连续采样CPU得不停地去搬运数据啥正经事都干不了。这时候DMA直接存储器访问简直就是救星。它像个勤快的搬运工能在内存和外设之间自动搬数据完全不需要CPU插手。但很多新手甚至一些有经验的朋友在配置DMA时面对“循环模式”和“非循环模式”也叫普通模式、单次模式这两个选项往往会犯迷糊到底该用哪个用错了会有什么后果我刚开始用DMA给串口发数据时就踩过坑。当时图省事想发一段固定长度的数据觉得用非循环模式发一次就完事了。结果程序跑起来数据只发了一次后面再触发发送就没反应了调试了半天才发现是模式没选对。这个经历让我深刻体会到理解这两种模式的区别绝不仅仅是看手册上那几句定义而是关系到你整个程序架构的稳定性和效率。今天我就结合自己踩过的坑和项目经验把DMA的循环模式和非循环模式掰开揉碎了讲清楚重点放在应用场景的选择、配置上的关键差异、以及那些手册上不会写的实战避坑指南。无论你是用标准库、HAL库还是LL库这些核心概念都是相通的。2. DMA工作模式的核心原理与本质区别要弄懂模式选择首先得抛开那些复杂的寄存器名字从DMA这个“搬运工”最根本的工作流程来理解。2.1 DMA传输的基本要素源、目的与“搬运清单”你可以把DMA控制器想象成一个听话的搬运队。当你需要它干活时必须明确告诉它三件事从哪里搬源地址比如是内存中的一个数组sendBuffer或者是某个外设的数据寄存器比如USART1-DR。搬到哪里去目的地址和上面相反如果是从内存搬到外设那目的地址就是外设寄存器如果是从外设搬到内存目的地址就是内存数组。搬多少数据数量你要搬运的数据量有多大单位可以是字节、半字16位或字32位。除此之外还要告诉搬运队搬完一趟之后源地址和目的地址要不要自动变化地址递增模式以及搬完指定的数量后接下来怎么办。这“接下来怎么办”就是循环模式和非循环模式要解决的核心问题。2.2 非循环模式一次性的精准投送非循环模式顾名思义就是“只干一次活”。你给DMA设置好要搬运的数据量比如100个字节启动传输后DMA就会吭哧吭哧地把这100个字节搬完然后自动停止并且会设置一个“传输完成”的标志位TCIF。同时它会把“剩余数据量”计数器CNDTR清零。注意这里有个非常关键的细节在非循环模式下一旦传输完成DMA通道就进入“失能”状态。如果你想再次使用这个通道进行传输必须重新配置数据数量CNDTR并且重新使能EN该DMA通道。很多初学者忘记重新使能导致后续传输无法触发就是这个原因。它的工作流程像一次性的快递任务你下单寄100个包裹设置数据量快递员DMA取走全部包裹并发货传输物流信息显示“已签收”传输完成标志然后这个订单就结束了。想再寄得重新下单重新配置并启动。非循环模式的核心特点单次性完成预设传输量后自动停止。状态明确有清晰的“传输完成”中断标志。需手动重启下次传输需软件重新初始化。2.3 循环模式不知疲倦的环形流水线循环模式则像是一个永不停歇的环形传送带。你同样设置一个要搬运的数据量比如100个字节。启动后DMA开始搬运。当它搬完第100个字节后不会停止而是自动将“剩余数据量”计数器CNDTR重新装载为初始值100然后从头开始下一轮的搬运。如此周而复始。在这个过程中不会产生“传输完成”中断除非你特意去查询并处理一些边界情况。它会产生的是“半传输完成”和“传输完成”中断但这些中断在循环模式下含义变成了“缓冲区处理过半”和“缓冲区处理完一轮”的通知用于配合软件进行双缓冲区Ping-Pong Buffer操作这是后话。它的工作流程像一个循环播放的磁带磁带长度是固定的数据量播放头DMA读/写指针走到尽头后自动回到开头重新播放。整个过程不需要你手动倒带重新配置。循环模式的核心特点自动循环传输到达终点后自动回到起点无限循环。无最终完成中断不会产生标志任务彻底结束的“传输完成”中断。适用于连续数据流完美匹配ADC连续采样、DAC连续输出等场景。2.4 模式对比与选择矩阵光讲原理可能还有点抽象我把它总结成下面这个表格你一眼就能看出区别和适用场景特性非循环模式 (Normal Mode)循环模式 (Circular Mode)传输行为传输指定数量后停止传输指定数量后自动从头开始无限循环计数器(CNDTR)传输完成后减至0每完成一轮自动重载为初始值传输完成中断(TC)会产生标志任务结束不会产生用于标志一轮结束典型应用场景1. 单次、定长的数据块传输如一次SPI Flash读写2. 需要明确知道传输完成的场合如文件传输3. 由事件触发的不定期传输如按键后发送数据1. 持续不断的数据流如ADC连续采样、音频流I2S2. 与定时器触发结合的周期性传输如用TIM触发DMA搬运DAC数据3. 实现环形缓冲区配合半传输和传输完成中断软件干预需求高。每次传输后需重新配置/使能低。配置一次永久工作除非主动关闭常见错误忘记重新使能导致后续传输失败误以为传输完成中断是结束信号导致逻辑错误选择心法问自己一个问题——“我的数据流是持续不断的还是明确分批次、有头有尾的” 如果是前者如传感器实时采样选循环模式如果是后者如响应命令发送一帧数据选非循环模式。3. 关键配置解析与不同模式下的实战代码理解了原理我们来看看在代码层面这两种模式是如何体现的。这里我以STM32CubeMX生成的HAL库代码为例因为HAL库抽象程度高更容易看清本质。标准库和LL库的逻辑是完全一致的。3.1 非循环模式配置以UART发送定长数据为例假设我们需要通过串口1非循环地发送一个100字节的数组txBuffer。CubeMX配置要点在DMA设置中为USART1_TX添加一个DMA通道如Channel 4。模式选择“Normal”。方向选择“Memory To Peripheral”内存到外设。数据宽度根据你的数据选择通常都是Byte。生成的初始化代码关键部分如下// DMA句柄自动配置了模式为NORMAL hdma_usart1_tx.Instance DMA1_Channel4; hdma_usart1_tx.Init.Direction DMA_MEMORY_TO_PERIPHERAL; hdma_usart1_tx.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址不增 hdma_usart1_tx.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 hdma_usart1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode DMA_NORMAL; // 关键配置非循环模式 hdma_usart1_tx.Init.Priority DMA_PRIORITY_LOW;发送数据的函数调用uint8_t txBuffer[100]; // ... 填充txBuffer ... // 启动一次DMA传输 if (HAL_UART_Transmit_DMA(huart1, txBuffer, 100) ! HAL_OK) { // 错误处理 } // 此时CPU可以立即去干别的事 do_something_else(); // 如果需要知道发送何时完成有几种方法 // 方法1轮询标志位不推荐浪费CPU // while(__HAL_DMA_GET_FLAG(hdma_usart1_tx, DMA_FLAG_TC4) RESET); // 方法2使用传输完成中断推荐 // 在HAL_UART_TxCpltCallback()回调函数中处理发送完成后的工作关键点与避坑HAL_UART_Transmit_DMA函数内部已经包含了启动DMA传输的逻辑。对于非循环模式这次调用就启动了一次性的传输。传输完成后hdma_usart1_tx这个DMA句柄的状态会改变。如果你需要再次发送直接再次调用HAL_UART_Transmit_DMA即可。HAL库会在函数内部检查状态如果DMA忙则返回错误如果DMA已停止则会重新启动它。这是HAL库带来的便利但你也必须理解其背后的机制在标准库中你可能需要手动清除标志位、重新设置数据数量并重新使能通道。中断处理如果你使能了传输完成中断在中断回调函数里千万不要再次调用HAL_UART_Transmit_DMA来启动传输除非你有特定的逻辑。因为中断函数应尽可能短。通常的做法是在回调函数里设置一个软件标志位在主循环中检查这个标志位再决定是否发起下一次传输。3.2 循环模式配置以ADC多通道连续采样为例这是循环模式的经典应用场景。我们希望ADC1连续扫描3个通道CH0, CH1, CH2并将采样结果通过DMA自动存放到一个数组adcBuffer[300]中形成一个持续更新的数据流。CubeMX配置要点ADC配置为连续转换模式Continuous Conversion Mode扫描模式Scan Mode使能。配置好需要采样的规则通道序列Rank1,2,3...。在DMA设置中为ADC1添加一个DMA通道。模式选择“Circular”。方向选择“Peripheral To Memory”外设到内存。数据宽度ADC通常是12位内存数组用16位uint16_t来存。内存地址递增一定要使能MemInc因为我们要把不同通道的数据存到数组的不同位置。生成的初始化代码关键部分// ADC DMA句柄 hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增至关重要 hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; // 外设半字对齐(16bit) hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; // 内存半字对齐 hdma_adc1.Init.Mode DMA_CIRCULAR; // 关键配置循环模式 hdma_adc1.Init.Priority DMA_PRIORITY_HIGH;启动采样的代码uint16_t adcBuffer[300]; // 假设采样100组每组3个通道 // 启动ADC的DMA连续传输 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBuffer, 300) ! HAL_OK) { // 错误处理 } // 启动后adcBuffer就会被DMA自动、循环地填充无需再干预数据读取与缓冲区管理 这才是循环模式下的精髓和难点。DMA在后台不停地循环写这个数组你的前台程序需要安全地读取数据且不能和DMA的写入位置冲突。这里通常有两种策略策略一双缓冲区Ping-Pong Buffer配合DMA半传输/传输完成中断这是最专业、最高效的方法。将adcBuffer[300]在逻辑上视为两个缓冲区每个150个元素。使能DMA的“半传输完成”HTIF和“传输完成”TCIF中断。当DMA填满前一半150个数据时触发半传输完成中断。在此中断回调中你可以安全地处理adcBuffer[0]到adcBuffer[149]的数据因为此时DMA正在写后一半150-299。当DMA填满整个缓冲区300个数据时触发传输完成中断。在此中断回调中你可以安全地处理adcBuffer[150]到adcBuffer[299]的数据因为此时DMA已经回到开头写前一半0-149。如此交替实现了数据生产和消费的无冲突并行。策略二定期拷贝法适合低速采样如果你的采样率不高可以在主循环中定期将整个adcBuffer拷贝到另一个处理数组adcBufferCopy中然后处理这个拷贝。拷贝操作要快使用memcpy并且最好在关闭全局中断的情况下进行以确保拷贝的数据是一致的一个“快照”。uint16_t adcBuffer[300]; uint16_t adcBufferCopy[300]; void ProcessADCData() { __disable_irq(); // 关中断防止拷贝过程中DMA修改数据 memcpy(adcBufferCopy, adcBuffer, sizeof(adcBuffer)); __enable_irq(); // 开中断 // 现在可以安全地处理 adcBufferCopy 了 for(int i0; i300; i3) { // 处理每一组的3个通道数据 ch0_val adcBufferCopy[i]; ch1_val adcBufferCopy[i1]; ch2_val adcBufferCopy[i2]; // ... 你的算法 ... } }实操心得对于高速数据流如音频、高频采样务必使用双缓冲区策略。定期拷贝法在高速下极易导致数据错乱你拷贝到一半时DMA可能已经覆盖了另一部分数据。循环模式下的DMA中断其意义从“任务完成通知”转变为了“缓冲区切换通知”这是思维上需要扭转的关键点。4. 混合应用与高级技巧模式切换与动态控制在实际项目中情况往往不是非黑即白的。你可能需要在不同场景下动态切换DMA的工作模式。4.1 动态切换模式的应用场景场景一先连续采样再触发单次处理设备待机时ADC以循环模式低速连续采样监控环境。当某个阈值被触发比如采样值超限你需要立即捕获触发点前后一段固定长度的高频采样数据进行分析。这时你需要停止循环模式的ADC DMA。将DMA模式改为非循环模式并设置要捕获的数据长度比如1024点。重新以更高的采样率启动ADC DMA。等待非循环DMA传输完成中断在中断里处理这1024个点的数据。处理完后恢复为低速循环采样模式。场景二突发数据传输平时串口DMA处于关闭或空闲状态。当需要发送一包数据时才以非循环模式启动DMA发送。发送完成后DMA自动停止不占用任何资源。这比让DMA一直处于循环模式等待数据更节能。4.2 在HAL库中动态修改DMA模式HAL库提供了相对清晰的接口。需要注意的是在修改DMA配置前必须先停止DMA传输。// 假设 hdma_usart1_tx 初始化为非循环模式现在要改为循环模式 HAL_DMA_Abort(hdma_usart1_tx); // 1. 首先中止当前DMA传输 hdma_usart1_tx.Init.Mode DMA_CIRCULAR; // 2. 修改模式为循环 // 可能还需要根据循环模式的需求调整其他参数如是否使能中断等 if (HAL_DMA_Init(hdma_usart1_tx) ! HAL_OK) // 3. 重新初始化DMA { Error_Handler(); } // 4. 重新关联到外设并启动以UART为例 __HAL_LINKDMA(huart1, hdmatx, hdma_usart1_tx); // 如果需要立即启动传输调用相应的HAL外设DMA启动函数注意事项动态切换模式是一个比较底层的操作务必小心。时序确保在DMA完全停止HAL_DMA_GetState()返回HAL_DMA_STATE_READY后再修改配置。HAL_DMA_Abort是阻塞式的会等待DMA停止。外设状态DMA是服务于外设的。修改DMA配置时也要考虑外设的状态。比如在切换ADC的DMA模式前最好先调用HAL_ADC_Stop_DMA()。中断模式改变后其中断行为也变了。记得同步更新中断使能设置__HAL_DMA_ENABLE_IT和中断服务函数中的处理逻辑。5. 常见问题排查与调试心得实录DMA调试不像GPIO点灯那样直观出了问题往往现象诡异。这里我罗列几个最典型的问题和排查思路。5.1 问题一非循环模式DMA只工作一次后续失效现象调用HAL_UART_Transmit_DMA发送数据第一次成功第二次调用返回HAL_BUSY或直接没反应。根本原因非循环模式传输完成后DMA通道自动失能并且TC标志位置位。HAL库的发送函数内部会检查DMA状态如果状态不是READY就会返回BUSY。解决方案使用HAL库的正确姿势确保每次传输前DMA句柄处于READY状态。对于UART最简单的方法就是每次都调用HAL_UART_Transmit_DMA。这个函数内部已经包含了必要的状态检查和重新初始化。不要试图手动去操作DMA的使能位。检查传输完成中断如果你在传输完成中断回调函数里进行了某些清理或标志设置确保没有错误地修改了DMA或UART的全局状态。标准库用户你需要在每次启动传输前手动清除DMA通道的TC标志位重新设置CNDTR数据数量然后置位EN使能位。5.2 问题二循环模式下数据缓冲区中的数据混乱或错位现象ADC循环采样数组里的数据不是按通道顺序排列的或者新旧数据混杂。排查步骤检查内存地址递增这是最常见的原因hdma_adc1.Init.MemInc必须设置为DMA_MINC_ENABLE。如果忘记设置DMA会把所有通道的采样值都写到数组的第一个元素地址上导致数据被不断覆盖。检查缓冲区大小与数据数量HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBuffer, N)中的N是总数据个数。如果你有3个通道希望采集100组那么N应该等于300数组大小也应该是300。如果N只设为100DMA只搬运100个数据就循环会导致每组数据不完整。检查数据对齐PeriphDataAlignment和MemDataAlignment必须匹配。ADC数据寄存器是16位的半字所以两边通常都设为半字对齐。双缓冲区冲突如果你使用了双缓冲区中断确保在中断回调函数中处理数据的速度足够快。如果处理太慢可能在处理完之前DMA又写入了新的数据造成冲突。此时应考虑增大缓冲区或者优化处理算法。5.3 问题三DMA传输似乎没启动或者偶尔丢数据现象配置都正确但内存里没有收到数据或者收到的数据量少于预期。排查思路时钟问题确保DMA控制器和外设的时钟都已使能__HAL_RCC_DMA1_CLK_ENABLE()__HAL_RCC_ADC1_CLK_ENABLE()等。CubeMX通常会帮你生成但手动移植代码时容易遗漏。触发源对于需要触发启动的DMA如ADC由定时器触发检查触发源是否正常工作。比如定时器是否启动触发事件是否使能。优先级与中断阻塞如果DMA传输过程中被更高优先级的中断长时间阻塞可能会导致数据丢失。特别是对于高速外设如SPI、I2S。适当提高DMA通道的优先级Init.Priority。内存访问速度如果DMA的目的地是速度较慢的内存比如STM32F1的CCM内存而传输速率又很快可能会出问题。确保DMA访问的内存区域是其时钟速度能跟上的。5.4 调试利器DMA传输计数器和状态标志当DMA行为异常时学会查看核心寄存器至关重要CNDTR寄存器这是“剩余数据数量”计数器。在非循环模式下它从初始值递减到0。在循环模式下它会在每轮传输结束时自动重载。在调试器中实时观察这个值可以清楚地知道DMA是否在干活以及干到哪一步了。ISR标志寄存器查看TCIF传输完成、HTIF半传输完成、TEIF传输错误等标志位可以判断DMA的当前状态和是否发生错误。使用HAL库的状态查询函数HAL_DMA_GetState(hdma)可以返回DMA的当前状态READY, BUSY, ERROR等是快速诊断的好帮手。我个人在调试复杂的DMA传输时习惯在关键位置如启动后、中断回调里打印或通过调试器查看CNDTR的值和ISR标志位这比盲目猜测有效得多。DMA是一个强大的工具但它的工作对于CPU来说是“透明”的因此清晰的逻辑和细致的调试是成功运用它的关键。理解了循环与非循环模式的本质差异你就能根据实际的数据流特点做出最合适的选择从而设计出高效可靠的嵌入式系统。