ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 DMA配置完全指南:从原理到串口收发与ADC采集

STM32 DMA配置完全指南:从原理到串口收发与ADC采集 这两年只要聊到STM32绕不开的话题里必有一个DMA。我遇到过不少朋友刚接触时觉得这东西玄乎一堆参数又是外设地址又是内存地址还要管数据宽度和优先级直接就被劝退了。其实本质很简单它就是一个快速搬运数据的硬件小工帮CPU把数据从A点挪到B点搬完跟CPU说一声“完事了”。这篇博客从原理到实操把DMA配置中最容易卡壳的地方全部拆开讲清楚适合刚学完串口和中断、准备接触DMA的初学者也适合已经在用但偶尔被奇怪Bug困扰的开发人员。1. DMA到底干了什么事先把它当成一个搬运工1.1 为什么需要DMA先从一次串口发送说起很多人在没有DMA之前串口发数据通常是这样的CPU把要发送的字节一个一个往数据寄存器里写写一个查一次状态位确认发送完成再写下一个。如果发几十个字节还好一旦要发几百上千字节CPU就基本啥也干不了全耗在这了。中断方式稍好一点每个字节触发一次发送完成中断但本质上还是CPU亲自去搬只不过搬一步歇一口气中断频繁的时候CPU开销依然很大。DMA的存在就是为了解决这个“CPU亲自搬砖”的问题。它本质上是一个独立于CPU的硬件模块专门负责在内存和外设之间搬运数据搬的过程不需要CPU参与。你只要在启动之前把“从哪里搬、搬到哪去、搬多少、搬完干什么”这四件事告诉它它就能自动干完然后在传输完成或出错时通过中断通知你。整个过程里CPU可以转身去处理别的任务比如跑算法、刷新界面、处理按键效率完全不在一个量级。生活化的理解是以前你CPU要亲自开车把一批货从仓库送到客户门口一趟一趟跑DMA就像你雇了个专职司机你把装货地址、卸货地址、货运量和交接方式交代清楚他就一趟趟帮你运完运完才回来跟你报告。你要做的就是制定规则而不是亲自开车。1.2 DMA的三个传输方向和数据流模型DMA搬运数据的方向一共就三种内存到外设、外设到内存、内存到内存。对应到实际场景串口发送是内存到外设把内存里的字符串搬到UART发送寄存器串口接收是外设到内存把UART接收寄存器里的数据搬到内存数组而内存到内存则常用于两个缓冲区之间的拷贝比如把采集缓存里的数据搬到处理缓存区。STM32的DMA控制器里还有几个概念容易混数据流Stream和通道Channel特别是F4系列以后的型号同时存在多个数据流每个数据流又可能关联多个通道。CubeMX里新建工程时配置外设的DMA请求其实是自动分配数据流和通道的不需要你手工去查到底是哪个通道对应哪个外设这点对初学者太友好了。但理解层面我们要知道不同的外设请求通过不同的通道进入DMA控制器控制器内部再按优先级和配置把请求分配给对应的数据流。一个外设一旦在CubeMX里勾选了DMA请求它使用的通道号是固定的不需要你操心但在阅读代码或旧项目移植时要能看懂这是谁和谁的对应关系。正是因为DMA绕过了CPU的参与速度上限非常高。在STM32F4上DMA的搬运吞吐量远高于串口外设本身能消费数据的速度所以用DMA做串口收发非常稳几乎没有丢数据的风险。这也是高性能项目里普遍选择DMA而不是中断接收的重要原因。2. 动手前必须吃透的DMA配置参数2.1 一组配置参数就是一个搬运任务清单DMA配置之所以让很多人觉得难是因为参数太多不知道每个参数到底是干什么的。这里统一梳理一遍。以CubeMX里看到的配置项为准我们逐项拆解。先说传输方向Direction对应我们上面讲的三种。然后是外设地址Peripheral和内存地址Memory这两个是DMA搬运的源和目的。外设地址一般是外设的数据寄存器地址比如USART1的DR寄存器、ADC的DR寄存器这些不需要你手动填HAL库已经帮你封装好了。内存地址就是你程序里定义的一个数组的首地址也就是你要存放数据的缓冲区的地址。接下来是传输数据量BufferSize或者Data Length就是本次搬运总共要搬多少个数据单元。这个值在启动DMA传输的时候传入配置界面里看不到因为它是动态的。每次串口接收到一包数据后下次再想接收新的一包要重新调用一次接收启动函数把BufferSize重新设置。数据宽度Data Width这个概念也常被忽略。它决定了一个数据单元是8位、16位还是32位。外设和内存的数据宽度可以不同但要遵循一个规则内存数据宽度比外设数据宽度大或等于时数据不会被拆分重排如果外设是8位内存设置成16位或32位也能工作但一般建议保持一致避免数据错位。ADC采集多通道时尤其要注意因为ADC的数据寄存器是16位的如果你定义的是uint16_t数组数据宽度就都设为Half Word。地址增量模式同样是常见配置。外设地址增量和内存地址增量要分开勾选。一般来说外设地址固定不变比如USART数据寄存器就那一个位置你往同一个寄存器写数据它自动发出到线上去而内存地址必须递增因为接收到的每个字节要依次填到数组的不同位置。外设地址增量模式偶尔用于某些特殊外设连续读取多个寄存器但初学者遇到的情况里基本都是外设地址不递增、内存地址递增。循环模式也非常关键。开启后DMA传输完设定的数据量不会停下来而是把内存地址自动重新指向数组开头继续下一次传输。这样串口接收时只要启动一次DMA之后数据会源源不断写入同一个缓冲区。如果BufferSize是100第101个字节会重新写入buf[0]覆盖旧数据因此需要我们及时处理buffer里的内容。有的项目用环形缓冲加DMA的循环模式实现无丢包的串口接收原理就在这里。优先级设置比较简单High、Medium、Low多个DMA数据流同时需要服务时优先级高的先执行。同级时就看硬件通道编号编号小的优先。实际项目里如果同时有音频采集、串口通信、SD卡写入等多路DMA把实时性要求最高的配置为High其余按需配置。2.2 这些参数之间怎么搭配单独讲参数很容易晕我们看几组实际最常见的组合。第一种串口发送。外设地址为USART数据寄存器、内存地址为要发送的字符串数组地址方向内存到外设数据宽度8位内存地址递增外设地址固定不开启循环模式发送完成后产生传输完成中断。这组配置是串口打印最标准的样子。第二种串口接收。方向外设到内存外设地址为USART数据寄存器、内存地址为接收缓冲区BufferSize设置一次能接收的最大数据长度数据宽度8位内存地址递增外设固定开启循环模式同时开启空闲中断。这套组合下数据一帧一帧进入缓冲区空闲中断告诉我们一帧数据是否结束。第三种ADC多通道采集。外设地址为ADC的DR寄存器内存地址为uint16_t数组方向外设到内存数据宽度16位内存地址递增外设固定开启循环模式传输数量等于通道数乘以每通道采样次数。配置好之后ADC在后台周期性地把采样结果直接写入数组CPU完全不用管。表格是最好的对照我做了一个常用的配置速查表方便在实际开发中快速对齐。配置项串口发送串口接收ADC多通道采集传输方向内存到外设外设到内存外设到内存外设地址USART-DRUSART-DRADC-DR内存地址字符串数组接收缓冲数组uint16_t采集数组数据宽度ByteByteHalf Word外设地址增量关闭关闭关闭内存地址增量开启开启开启循环模式关闭开启开启3. 最经典的实战串口DMA收发彻底告别阻塞式打印3.1 用CubeMX快速配置一个可用的工程我们直接以STM32F103系列或者F4系列为例操作流程几乎一致。第一步打开CubeMX选定芯片型号配置时钟树到最大主频然后找到USART1或你手头板子上的串口在Mode里选择Asynchronous异步模式波特率按自己需求设置一般115200。这一步和普通串口工程没有区别。接着是关键的DMA配置。切到DMA Settings标签页点击Add添加请求你会看到USART1_TX和USART1_RX两个选项分别对应发送和接收全部添加。选中USART1_RX在下方把Mode改为Circular循环模式如果默认就是Circular就不用改选中USART1_TXMode保持Normal即可。因为接收要用循环模式不断填数据发送是一次性的任务搬完就停。这里有一个小小的注意点DMA的中断也要一并开启只有在DMA中断开启的前提下接收完成、发送完成等相关回调函数才会被触发。最后不要忘了在NVIC设置里使能USART1全局中断因为接收不定长数据需要用到串口空闲中断而串口空闲中断对应的是USART1全局中断不是DMA中断。这样配置完成后生成工程代码。提示CubeMX里把DMA模式和中断都配置好后它会自动帮你生成DMA初始化函数MX_DMA_Init以及串口初始化里关联DMA句柄的代码不需要手写底层初始化。新手最容易犯的错是只配了DMA但没开DMA相关中断导致回调函数永远不执行。3.2 收发代码怎么写不定长数据接收的完整套路串口DMA发送代码其实就一行HAL_UART_Transmit_DMA(huart1, (uint8_t *)message, strlen(message));只要配置没毛病send函数调用后CPU立刻返回DMA在后台搬运发送完成会触发HAL_UART_TxCpltCallback回调你可以在里面做个标志位或者翻转一个LED提示发送完毕。这个send函数不是阻塞的连续多次调用时要注意上一次传输是否结束否则可能会出现数据覆盖的情况。稳妥的做法是在上一次发送完成回调里置一个标志发送前检查这个标志。真正的难点在于接收不定长数据。DMA接收的特点是你启动接收时设定了一个缓冲区长度DMA会持续往缓冲区里搬数据但这不代表每搬完一帧你就会感知到。DMA只有搬满整个BufferSize才会触发接收完成回调如果对方发来的是不定长的小包缓冲区没满那数据就一直堆积在缓冲区里你不知道什么时候一包数据结束了。解决办法是配合串口的空闲中断IDLE。它的意思是串口线上出现一段空闲时间即一个字节时间没有新数据就认为当前帧结束了。结合DMA循环接收就能做到DMA一直往缓冲区存数据而当串口空闲时立刻进入空闲中断在中断里读取DMACNDT寄存器或者其他方式算出当前地址相对缓冲区首地址的偏移量这个偏移量就是这一帧数据的长度然后立即处理这部分数据处理完将DMA内存地址指针重新指向缓冲区开头继续接收下一帧。HAL库在较新版本1.5.0之后提供了封装好的接口HAL_UARTEx_ReceiveToIdle_DMA它把“DMA接收”和“空闲中断”捆绑在一起我们只需要在启动时调用一次HAL_UARTEx_ReceiveToIdle_DMA(huart1, rx_buffer, BUFFER_SIZE);接收完成时回调HAL_UARTEx_RxEventCallback会被调用参数Size就是这一帧数据的实际长度。这个函数代替了传统的HAL_UART_Receive_DMA加手动空闲中断处理的繁琐流程。比如这样写void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // 把接收到的Size个字节复制到协议解析缓冲区 memcpy(protocol_buffer, rx_buffer, Size); protocol_rx_flag 1; // 重新启动下一次DMA空闲接收 HAL_UARTEx_ReceiveToIdle_DMA(huart1, rx_buffer, BUFFER_SIZE); } }很多初学者在这里踩过一个坑在回调里调用启动函数时没有再调用一次__HAL_UART_CLEAR_IDLEFLAG(huart1)清空闲标志或者没有把HAL_UARTEx_ReceiveToIdle_DMA放在回调末尾导致第二次接收开始时立即又触发了一次空闲中断造成逻辑混乱。HAL库的新封装在内部会处理一部分标志位但我们仍需按照“用完即清清了再开”的通用套路保证空闲标志是干净的。再补充一个细节启动DMA循环接收时bufffer大小不是随便填的它决定了DMA缓冲区能缓存多长的单帧数据。如果一帧最大可能是256字节那你BufferSize至少256留点余量更保险。BufferSize太小长帧会把缓冲区撑爆数据被覆盖太大了浪费RAM在内存紧张的MCU上要考虑内存分配比如STM32G0这种小内存芯片BufferSize设成512都可能紧张。4. 另一个高频场景ADC多通道DMA采集4.1 什么时候需要用ADCDMA什么时候真没必要ADC采集是DMA在工业控制里用得最多的场景之一。最简单的单通道单次采集完全没必要上DMA用HAL_ADC_Start HAL_ADC_PollForConversion阻塞读取或者加个中断都够用了。真正体现DMA价值的是多通道连续采样。比如同时采集三个电流传感器加两个电压传感器每路要连续采样多次再做平均滤波如果用中断方式每采完一路都要进一次中断CPU频繁被打断还要在中断里维护通道切换和计数逻辑程序复杂度成倍上升。DMA方案就简单得多ADC配置成连续扫描模式DMA把每次转换结果自动搬运到数组里。比如5个通道每个通道连续采集10次就定义一个50个元素的uint16_t数组配置DMA数据量为50开启循环模式。ADC每转换完一路结果自动存进数组一个位置50个位置存满后DMA自动从头开始覆盖。因为开启了循环模式相当于采集永远在后台跑CPU随时可以从数组里读最近一次的数据。这个方案里CPU和ADC各干各的互不干扰。4.2 多通道数据错位、DMA填充顺序的坑多通道ADC加DMA最经典的问题是填充顺序。配置ADC时通道扫描顺序是按规则组的通道编号排序的。比如你配置的顺序是通道0、1、2、3、4那么第一次转换的数据是ch0第二次是ch1依次类推。DMA传输的数据就是按这个顺序填充到数组里的。但如果你在ADC配置里把某个通道的采样时间设置得特别长或者有注入通道干扰就可能导致数据顺序和预期不一致。解决方法是打印出数组里的数据一一核对确认通道的对应关系再处理数据。另一个容易踩的坑是数据对齐。STM32的ADC数据寄存器默认是左对齐或右对齐CubeMX里一般配置为右对齐对应的数据宽度是Half Word。如果你定义了uint16_t数组那没问题如果你误定义成uint8_t数组数据宽度又设成Byte就会出现两个通道的数据互相拆开重新排列采到的数据完全不对。碰到这种“数据全乱”的诡异现象先检查数据宽度再检查数组类型基本能排查掉一半问题。DMA传输完成中断在ADC应用里也很常用。比如我们采集了50个数据需要在采集满一轮之后做滤波处理可以在DMA传输完成中断回调里处理void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { // 一轮采集完成处理数组里的50个采样值 process_adc_data(adc_buffer, 50); } }注意如果配置了循环模式DMA传输完成回调会反复触发每次缓冲区填满一轮都会进入。如果处理耗时太长可能影响下一轮的数据采集节奏。解决办法是只在回调里挂标志位主循环里做实际处理或者开启DMA的半传输完成中断一半一半地处理数据。对于F4系列可以更进一步利用DMA的双缓冲模式。配置两个缓冲区DMA先往buffer0搬数据搬满后自动切换到搬buffer1同时触发中断告诉CPU“buffer0满了快来处理”CPU处理buffer0期间DMA继续往后写buffer1互不干扰。这种双缓冲方式能彻底解决数据处理耗时导致丢数据的问题缺点是RAM占用翻倍适合内存充裕的场景。5. 踩坑记录调试DMA时最常见的几个问题5.1 配置了DMA却没响应、数据乱码的排查思路我调试DMA这么久遇到过很多次“明明配置了却不工作”的情况总结下来有几类高频原因。第一类是时钟问题。DMA外设时钟没有使能。CubeMX生成的代码一般不会漏但在把DMA相关代码手工移植到老工程时非常容易忘记在MX_DMA_Init里先调用__HAL_RCC_DMA1_CLK_ENABLE()或__HAL_RCC_DMA2_CLK_ENABLE()。没有时钟DMA外设完全处于关断状态配置了也白配。检查方法很简单直接单步走观察DMA初始化函数有没有被调用到。第二类是中断优先级问题。DMA中断和串口中断的优先级互相冲突导致回调不执行。常见的表现是DMA搬运数据本身是正常工作的数据确实进了缓冲区但你的接收完成回调或者空闲中断回调里看不到数据说明中断一直没被响应。把NVIC中DMA相关中断优先级调高一点或者确认全局中断已使能一般就解决了。第三类是HAL库版本引发的差异。老版本HAL库没有HAL_UARTEx_ReceiveToIdle_DMA处理空闲中断要自己操作寄存器新版本库内部实现也偶有微调升级库版本后代码行为可能发生变化。如果项目是从旧工程升级来的遇到串口DMA收发行为异常考虑是不是库版本切换导致的接口差异。第四类是Cache问题这个在带D-Cache的高性能芯片比如STM32H7系列上特别常见。DMA不经CPU直接往内存里写数据而CPU读取该内存时可能读的是Cache里的旧数据导致认为“DMA根本没写入”。解决办法是DMA接收缓冲区定义在非Cacheable内存区或者在DMA写入完成后调用SCB_InvalidateDCache_by_Addr使Cache失效。这个问题在F1、F4系列不存在只有在Cortex-M7及以上的芯片才会遇到但如果碰到了会非常困惑。第五类是初始化顺序问题。DMA和外设的初始化顺序在CubeMX生成的代码里是固定的先是DMA初始化再是外设初始化。因为外设初始化时可能要关联DMA句柄。如果你手工改代码调换了初始化顺序偶尔会出现外设关联DMA失败、启动DMA传输直接报错的奇葩问题。所以能不动初始化顺序就尽量别动有个洁癖式的习惯所有硬件初始化严格按CubeMX生成的顺序来。5.2 区分DMA传输完成和DMA中断的概念这一条一定要单独拎出来讲。DMA传输完成是个状态它发生在硬件层面比如串口缓冲区数据全部接收完DMA确实把最后一个字节放进了内存数组。但“DMA传输完成中断”是通知CPU的渠道只有使能了DMA中断传输完成状态才会触发中断服务程序然后HAL库里的回调函数才会被调用。这两件事并不等价。我们在排查问题时要学会区分如果数据本身已经正确进入缓冲区但你的回调没执行那就是中断链路出了问题如果数据本身没进入缓冲区那才可能是DMA配置问题。按照这个思路排查效率提高很多。还有一个常见误区是把HAL_ADC_ConvCpltCallback当成只在全部数据转换完成时调用一次。在循环模式里它实际上是每次缓冲区满都会触发。有的同学在回调里做单次初始化操作结果发现执行了很多次其实这不是Bug是循环模式的本意。如果只想在第一次转换完成时做一次处理需要在回调里加个标志位或者计数判断。5.3 问题排查速查表最后整理一个排查速查表是我实际项目里最常翻的清单遇到DMA问题时按顺序逐项核对多数情况几分钟就能定位。现象大概率原因排查方法完全没有数据搬运DMA时钟未使能或请求未开启启动函数是否调用外设DMA请求是否使能数据有搬运但回调不执行DMA中断优先级过低或未使能检查NVIC配置和DMA中断开关串口收到的数据乱码错位数据宽度不匹配或内存类型不对核对外设和内存的数据宽度设置字节多了或少了BufferSize设置和实际数据不匹配打印实际接收长度对照BufferSizeF7/H7上数据全是旧值D-Cache未失效调用Cache Invalidate操作多通道ADC数据顺序错乱通道顺序配置或数据对齐问题打印数组逐个核对通道对应关系第二次接收不再触发空闲标志未清除或未重新启动接收在回调末尾清除标志位并重新调用启动函数6. 写在最后的经验总结DMA这个东西第一次接触的时候可能觉得参数又糊又乱感觉和中断、外设地址这些概念搅在一起很没底。但只要你亲手用CubeMX从头到尾配一次串口接收、再配一次ADC采集整个过程跑通了再去回看数据手册里的DMA框图会发现很多疑问自然而然就解开了。我个人的体会是学DMA最好的方式不是死记参数而是先接受“它是帮你搬数据的助手”这个设定然后从最简单的串口发送开始一步步把接收、多通道采集、双缓冲这些高级功能加进来遇到问题再回到手册里查细节比一开始硬啃手册高效得多。最后再分享一个自己调试时的小技巧凡是DMA相关的数据问题第一件事不是翻代码而是把数据缓冲区里的原始内容直接读出来看。数据是乱的、是空的、还是顺序不对这三种状态指向的排查方向完全不同。亲眼看到数据从错到对的变化过程会让你对DMA的理解深入很多下次再遇到类似问题心里基本就有底了。
返回列表