十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

DMA详解:从工作原理到串口/ADC实战,一文搞懂嵌入式DMA

DMA详解:从工作原理到串口/ADC实战,一文搞懂嵌入式DMA 搞嵌入式这行跟串口、ADC、SPI打交道久了迟早会撞上DMA这个词。尤其当你发现CPU一边要处理业务逻辑一边还要吭哧吭哧搬运数据系统一忙就丢包、卡顿、响应慢的时候DMA就成了绕不开的救星。这玩意儿全称Direct Memory Access直译就是直接存储器访问核心作用一句话让数据在外设和内存之间自己跑别去打扰CPU。我这几年调过的板子从STM32到国产PY32从裸机到RTOS几乎每个项目都离不开DMA。这篇就把DMA的工作流程、传输模式和典型应用场景一次性讲透包括那些网上翻半天也找不到的坑——比如串口DMA接收不定长数据怎么配合空闲中断、发送时到底要不要等上一轮发完、ADC多通道DMA采样为什么数据老是错位。不管你是刚入门的大学生还是被项目逼着查DMA的工程师这篇都值得你花十分钟看完很多经验是真金白银踩出来的。1. DMA到底是什么它解决的从来不是“快”而是“省”1.1 没有DMA的日子CPU是怎么被拖垮的先算一笔账。假设你用串口以115200bps收数据一个字节大约耗时86.8微秒。如果用中断方式接收每个字节进来CPU都要停下手头的活保存现场、进中断服务函数、把数据从数据寄存器搬到内存缓冲区、清标志位、恢复现场。这一套下来即使效率高也要2到3微秒看起来占比不高但问题是一旦数据速率上到1Mbps甚至更高或者同时开了串口、SPI、ADC好几路数据流CPU绝大部分时间就在做“从寄存器到内存”的纯搬运工作。还有一个更隐蔽的问题中断频繁触发会打乱CPU的执行流。你正在跑一个关键算法突然被串口中断打断等回来的时候缓存、流水线状态全变了上下文切换的开销远不止那几微秒。我见过一个项目就是因为串口中断太频繁导致主循环里的实时控制任务周期性抖动查了好久才发现罪魁祸首是中断风暴。DMA的出发点不是“让传输更快”而是“让CPU从传输中抽身”。数据要搬但搬的人不该是CPU而应该是一个专门干这活的硬件小工。这个小工就是DMA控制器。1.2 DMA的核心思想CPU只当管理者不当搬运工打个比方。一家餐厅里CPU是厨师数据是食材外设和内存分别是仓库和灶台。没有DMA的时候厨师每炒一道菜都要亲自跑一趟仓库取食材菜越做越多厨师全耗在路上了。DMA相当于请了一个传菜员厨师只需要说一句“把仓库里那批食材搬到灶台”然后继续专心炒菜传菜员干完了再喊一声“搬完了”。这个“喊一声”就是DMA传输完成中断。整个过程中CPU只在开始的时候配置一下DMA控制器的源地址、目的地址、传输长度然后该干嘛干嘛去等DMA搬完了通过中断或者轮询标志位来确认结果。如果是循环模式甚至不需要CPU介入DMA会自己一遍遍搬搬完一轮自动从头再来CPU连“确认完成”都可以省了。1.3 DMA、中断、轮询三者的定位完全不一样很多新手搞不清什么时候用DMA什么时候用中断什么时候干脆轮询。我的经验是这么区分的轮询适用于极低速、极简单的场景比如按键扫描、查询传感器状态。CPU闲着也是闲着顺手读一下寄存器就行。中断适用于事件型、偶发性的数据比如收到一个命令、检测到一个边沿。数据量小事件频率不高中断完全扛得住。DMA适用于批量、连续、高频的数据流比如串口收一帧几百字节的数据、ADC连续采样几千个点、SPI刷一块屏幕。这类场景数据量大且规律性强是DMA的主场。一句话总结轮询是“盯着看”中断是“被叫去处理”DMA是“派个人去代办”。三者不是竞争关系而是可以配合使用的。最常见的就是DMA做完一批传输后触发中断CPU在中断里只做“结果处理”不碰数据搬运各司其职。2. DMA工作流程拆解从配置到完成的完整链路2.1 一个DMA传输的四个阶段DMA听起来玄乎实际上一次完整的传输就四个阶段请求、响应、传输、结束。我一个个拆开讲。第一个阶段是请求。外设准备好数据了或者内存里有一块数据等着搬走谁来发起这个动作如果是外设到内存外设会拉高DMA请求信号比如串口接收寄存器非空就会向DMA控制器发出请求。如果是内存到内存则由软件把DMA使能位置1相当于软件发起请求。第二个阶段是响应。DMA控制器收到请求后会向总线仲裁器申请总线控制权。这里要注意同一时间总线只有一个主人CPU在用总线DMA就得等着DMA拿到总线CPU就得暂停访问。好在DMA传输通常是突发式、短时间的对CPU的影响微乎其微。仲裁器会根据优先级来决定先让谁用总线这就是为什么高优先级通道能“插队”的原因。第三个阶段是传输。这是核心动作。DMA控制器从源地址读一个数据单元写入目的地址。数据单元的宽度可以是字节、半字16位、字32位具体由你配置。每传完一个单元源地址和目的地址根据方向决定是自增、自减还是保持不变传输计数器减一。这个“搬运一个单元”的动作会循环执行直到计数器归零。第四个阶段是结束。计数器归零后DMA控制器会置位传输完成标志如果使能了中断还会向CPU发出中断请求。至此一次完整的DMA传输结束。如果是循环模式传输计数器会自动重载初始值DMA又开始下一轮搬运周而复始。2.2 以STM32为例看一次DMA传输的寄存器操作序列光讲概念不过瘾我以最常用的STM32 HAL库为例展示一次内存到内存搬运的完整操作序列。这段代码是把一个数组的数据通过DMA复制到另一个数组uint32_t src_buf[64]; uint32_t dst_buf[64]; // 初始化DMA需要先开启DMA1或DMA2的时钟 __HAL_RCC_DMA1_CLK_ENABLE(); DMA_HandleTypeDef hdma; hdma.Instance DMA1_Channel1; hdma.Init.Direction DMA_MEMORY_TO_MEMORY; hdma.Init.PeriphInc DMA_PINC_ENABLE; hdma.Init.MemInc DMA_MINC_ENABLE; hdma.Init.PeriphDataAlignment DMA_PDATAALIGN_WORD; hdma.Init.MemDataAlignment DMA_MDATAALIGN_WORD; hdma.Init.Mode DMA_NORMAL; hdma.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma); // 启动DMA传输src是源dst是目的长度64个字 HAL_DMA_Start(hdma, (uint32_t)src_buf, (uint32_t)dst_buf, 64); // 等待传输完成 HAL_DMA_PollForTransfer(hdma, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);这里有个关键点很多人没意识到在HAL库里外设地址叫Periph内存地址叫Mem但方向是内存到内存时PeriphInc和MemInc必须都使能否则数据搬完只有第一个字是有效的。我见过不止一个新手卡在这个坑里搬完数据后发现目标数组只有第一个元素变了其他全是0就是因为忘了开地址自增。2.3 关键状态位与中断标志怎么判断DMA干完活了DMA控制器内部有几个状态位搞懂它们排查问题就成功了一半。以常见MCU为例几个最重要的标志是传输完成标志DMA搬完所有数据后置位代表这一批任务结束。半传输标志传输数量过半时置位常用于双缓冲切换。比如ADC正在采后半段数据时CPU可以处理前半段两边不打架。传输错误标志总线错误、非法地址等异常情况下置位必须优先处理否则DMA可能一直卡住。使用中断方式时进入DMA中断服务函数第一件事就是读取中断标志并清除。但要注意有些芯片的标志清除顺序有讲究比如必须先读状态寄存器再清标志顺序反了会导致中断反复触发系统直接卡死在中断里。这部分必须看芯片参考手册不能想当然。2.4 DMA连续请求与循环模式两者是什么关系热词里有个“DMA continuous requests”中文叫连续请求或持续请求。这个概念在部分MCU的DMA配置中存在比如有些芯片的DMA支持“循环缓冲区连续请求”模式。简单说普通模式下外设每产生一个请求DMA传一个数据单元传完就等下一个请求。而在连续请求模式下DMA只要启动就会不断搬数据不再等外设请求。连续请求和循环模式经常搭配使用。循环模式管的是“搬完一轮后自动回到起点继续搬”连续请求管的是“不需要外设逐个触发”。两者叠加的效果就是DMA一直在后台搬数据搬完一圈从头再来CPU完全不操心。典型应用就是ADC连续采样ADC不停采DMA不停搬数据在一个环形缓冲区里循环覆盖。3. DMA传输模式全解析模式选错性能天差地别3.1 按方向分三种传输方向对应三类场景DMA传输方向是配置时最先要确定的参数直接决定了你的源和目的是谁。第一种是内存到内存典型应用是大块数据拷贝、图像缓冲翻转、协议栈报文重组。这种模式下源和目的都在内存地址空间数据经过DMA控制器内部的临时寄存器中转从一个内存区域搬到另一个内存区域。特点是CPU完全不用参与适合搬大块数据但会占用总线带宽如果和实时性要求高的任务抢总线要谨慎评估时序。第二种是外设到内存这是MCU开发中最常见的。串口接收、SPI接收、ADC采样、I2C接收本质都是外设的数据寄存器收到数据后由DMA搬到内存缓冲区。这里有一个关键配置外设地址固定不变内存地址自增。外设地址永远是那个数据寄存器每搬一个数据内存地址往后挪一格这样才能形成连续的数据序列。第三种是内存到外设典型应用是串口发送、SPI发送、DAC波形输出。DMA从内存缓冲区取出数据写入外设的数据寄存器。这时候内存地址自增外设地址固定。配上定时器触发DMA还可以按固定节奏输出数据比如产生PWM波形、播放音频采样点。3.2 按工作模式分单次、循环、突发各有用武之地单次模式是最基本的配置好源地址、目的地址、长度触发一次搬一次搬完拉倒计数器归零DMA通道自动禁用。适合一次性的数据搬移比如把传感器校准参数从Flash搬到RAM。循环模式是单次模式的强化版计数器归零后自动重载初始值DMA通道一直保持使能。典型应用就是ADC连续采样和串口接收环形缓冲。但循环模式有个隐患如果软件没有及时消费缓冲区里的数据新数据会把旧数据覆盖掉。所以必须配合半传输中断、传输完成中断或空闲中断来分时处理。突发模式则是以“块”为单位传输而不是一个数据单元一个数据单元地搬。比如一次突发传输8个字DMA会连续读8个字再连续写8个字。这种模式效率高因为减少了总线仲裁次数但需要源和目的地址都满足对齐要求而且对总线的占用时间更长可能在瞬间抢占大量总线带宽影响系统的实时性。用突发模式前一定要算好带宽余量。3.3 按触发方式分硬件触发与软件触发DMA传输需要触发源。软件触发就是写寄存器启动适用于内存到内存搬运、或者定时批量处理。硬件触发则是外设产生事件来触发DMA搬运比如串口收到字节、ADC转换完成、定时器更新事件。硬件触发有个细节必须注意DMA传输的数据宽度要和触发外设的数据宽度匹配。比如串口数据寄存器是8位的你配置DMA搬运宽度却是32位那每次触发实际搬入的数据会错位收到的东西完全对不上。这类问题非常隐蔽因为它不报错只产生“脏数据”。定时器触发DMA是非常实用的技巧。比如你要输出一组正弦波查表数据到DAC简单粗暴的做法是CPU定时把下一个采样点写给DAC但这样CPU会被绑死。换成定时器触发DMA配置好DMA内存地址自增、DAC地址固定定时器每次更新事件触发一次DMA搬运CPU只需启动一次剩下的交给硬件。采样率再高也不怕CPU完全解放。3.4 不同平台上的DMA架构差异MCU、Linux、UFS这些都得知道搞嵌入式不能只盯着STM32。Linux系统里有通用的DMA引擎框架设备驱动程序通过DMA API申请DMA缓冲区、映射地址、提交传输请求底层由平台相关的DMA控制器完成实际搬运。这里最核心的差异是Linux场景下DMA缓冲区涉及到物理地址、虚拟地址和缓存一致性映射代码里必须处理更复杂的地址转换。热词里还有UFS DMA。UFSUniversal Flash Storage作为一种高速存储接口它的数据传输基本都要靠DMA来搬运因为UFS的速率动辄每秒几百MB甚至上GBCPU压根没法逐字节参与。UFS控制器内部集成了专用的DMA引擎负责在UFS设备和系统内存之间搬数据同时处理中断聚合、高级电源管理等。分布式DMA则是另一个方向常见于多核处理器或异构计算平台。每个计算核心拥有独立的DMA引擎可以在局部内存之间搬运数据减少跨核心访问总线的开销。这种架构在通信基带、AI推理芯片中很常见。对于大部分做MCU开发的人来说可能用不到这么复杂的架构但理解这个思路有助于你迁移到更高级的平台。4. 典型应用场景与实操要点串口、ADC、SPI一次讲清4.1 UART DMA接收不定长数据空闲中断才是关键这是被问得最多的场景串口接收的数据长度不固定怎么用DMA接方案是“DMA空闲中断”。所谓空闲中断就是串口在一段时间内没有收到新数据时触发的中断它标志着一帧数据已经接收完整。具体做法分三步。第一步配置UART的DMA接收让DMA把数据持续搬运到一个足够大的缓冲区使用循环模式。第二步开启UART的空闲中断。第三步在空闲中断里计算本次接收的数据长度然后处理数据。// 配置UART接收DMA循环模式 __HAL_RCC_DMA1_CLK_ENABLE(); hdma_uart_rx.Instance DMA1_Channel5; hdma_uart_rx.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_uart_rx.Init.PeriphInc DMA_PINC_DISABLE; hdma_uart_rx.Init.MemInc DMA_MINC_ENABLE; hdma_uart_rx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_uart_rx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_uart_rx.Init.Mode DMA_CIRCULAR; // 循环模式 HAL_DMA_Init(hdma_uart_rx); __HAL_LINKDMA(huart1, hdmarx, hdma_uart_rx); HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUFFER_SIZE); // 开启空闲中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 空闲中断回调中处理数据 void HAL_UART_IDLE_Callback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t len RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(hdma_uart_rx); process_frame(rx_buffer, len); } }这段代码的核心是__HAL_DMA_GET_COUNTER它返回DMA还剩多少数据没搬。用缓冲区总长度减去剩余计数就是实际收到的数据长度。注意这里有个坑如果使用循环模式处理完一帧数据后不要傻傻地把DMA停掉重新配置再启动那样会丢数据。正确做法是让DMA一直在循环跑你只需要在空闲中断里计算长度并拷贝数据。缓冲区大小有讲究。它必须大于项目里可能出现的最大一帧数据的长度同时要考虑极端情况下收到的干扰数据。我习惯设成最大帧长的2到3倍宁可浪费一点内存也不能让数据溢出覆盖。4.2 UART DMA发送到底要不要等上一轮发完热词里“dma串口发送需要等待上一轮数据发送完吗”这个问题说明大家都在这上面栽过跟头。直接给结论要等但不等不等于一定出错只是风险极大。为什么每次调用DMA发送时你需要把源缓冲区地址和长度告诉DMA控制器。如果上一次发送还没结束你就修改了缓冲区内容或者启动了新一轮发送DMA可能正在读一个被修改了一半的缓冲区发出去的数据就会错乱。还有一种更隐蔽的情况配置新传输时DMA计数器被重置上一轮没发完的尾巴突然断了。我的处理习惯是维护一个“发送忙”标志。启动DMA发送时置位在DMA发送完成中断里清除。下次要发送前先检查这个标志如果还忙着就进入等待队列或直接丢弃。如果你用的是类似HAL_UART_Transmit_DMA这样的接口它内部其实会检查UART的状态但裸机环境下依然建议自己做一层保护。这里特别提醒一个细节DMA发送完成后缓冲区的数据还在但DMA已经释放了总线。如果你的缓冲区是局部变量函数返回后栈空间被释放此时DMA还在搬数据就会读到垃圾值。所以DMA发送的源缓冲区必须保证在发送完成之前一直有效最好用全局数组或静态数组。4.3 ADC多通道DMA采样数据错位问题怎么破ADC多通道DMA是另一个高频场景热词里“stm32 hal库adc单通道dma多次采样”也直接命中了这个需求。ADC转换结果通过DMA自动搬运到内存数组CPU只负责定期处理数组这是标准做法。单通道DMA多次采样比较简单配置ADC为连续转换模式DMA循环搬运缓冲区里就是连续的一串采样值。但多通道就麻烦了因为多个通道的转换结果会依次推进DMA缓冲区。比如你开了通道0、1、2三个通道DMA缓冲区里的数据顺序是ch0、ch1、ch2、ch0、ch1、ch2……你取数据时必须按组取不能简单地认为数组第0到99个都是ch0。我踩过的坑是通道顺序和DMA缓冲区初始化不一致导致的数据错位。解决思路有两个一是严格按ADC的通道顺序手动编排DMA缓冲区取数据用取模分组二是启用ADC的DMA半字模式确保每个采样值在缓冲区中的位置固定。实测下来用双缓冲DMA半传输中断最稳后半段采样期间处理前半段数据处理和采样完全并行。关于采样频率和缓冲区大小一定要算好。假设ADC采样率是1kHzDMA缓冲区设1000个点缓冲区每1秒才会被循环覆盖一遍。如果你在后续处理中耗时超过1秒有些数据就被覆盖了。合理做法是根据处理耗时反推缓冲区大小留足余量。4.4 SPI刷屏、I2C读写、内存搬运几个高频场景速查SPI刷屏是DMA的经典应用。LCD屏幕刷新一帧数据动辄几百KB用CPU逐像素发送帧率上不去还卡界面。用SPIDMA发送只需把显存地址配成源地址SPI数据寄存器配成目的地址长度设为总像素数乘每个像素字节数一帧刷完CPU完全无感。I2CDMA相对少用因为I2C有ACK应答机制每个字节传输后需要检查应答DMA模式下出错了不容易恢复。如果必须用建议只在无ACK校验的情况下使用或者配合超时机制。实际项目中我更多用I2C中断方式数据量不大时足够用。内存到内存搬运在嵌入式场景也很有用。比如协议栈里需要把报文的头部和数据区拼接在一起再发送用DMA搬运可以省一次CPU拷贝。还有一个容易被忽视的点双缓冲音频播放。一段音频被分成两个缓冲区一个在播放另一个在填充新数据DMA在两者之间切换实现无缝播放。这个思路跟ADC双缓冲一模一样。4.5 低成本MCU上的DMA实践以PY32F003为例热词里出现PY32F003这是国产一款性价比很高的M0内核MCU资源不多但DMA该有的功能都有。用它做串口DMA接收思路和STM32完全一致唯一需要注意的是它内部DMA通道数量有限外设映射关系也跟STM32不完全相同配之前必须翻芯片手册确认哪个DMA通道可以服务哪个外设。我实际用PY32F003做过一个低功耗传感器采集节点串口接收采用DMA空闲中断CPU大部分时间进入睡眠只有空闲中断唤醒处理数据帧整体功耗压得很低。这算是DMA在低功耗场景下的额外优势数据接收根本不需要CPU醒来攒够一帧再说。4.6 进阶场景FreeModbusDMA、DMA测速验证FreeModbus这种协议栈场景下用DMA要格外小心。Modbus RTU帧有严格的3.5字符时间间隔作为帧间隔判断如果你用DMA空闲中断接收空闲中断的判定时间和3.5字符时间不一定匹配可能导致粘帧或者断帧。我踩过的坑是空闲中断的触发时机比Modbus要求的帧间隔略长导致两个连续的Modbus帧被当成一帧接收报文解析直接失败。后来做法是关闭DMA改回中断接收因为Modbus的帧速率本来就不高中断接收足够用了。DMA测速这个需求核心是验证DMA实际吞吐量是否达到预期。简单做法是配置一个内存到内存的DMA搬运大块数据用定时器记录搬运耗时算出实际带宽。还可以对比同样操作下CPU逐字节搬运的耗时效果直观震撼。我实测过一次在72MHz主频的MCU上DMA搬运1KB数据只需要几个微秒CPU逐字节搬运要几百微秒差距接近两个数量级。5. DMA疑难杂症与排查技巧实录5.1 常见问题速查表我在多个项目里总结了一张高频问题速查表每次调试DMA出问题先对着查一遍往往能快速定位。现象可能原因排查方向接收到的数据全是0地址自增未使能数据宽度不匹配检查PeriphInc/MemInc配置、对齐方式数据错位、通道顺序错乱ADC多通道时缓冲区编排错误按通道顺序重新排列缓冲区或分组取数DMA传输完成后数组没更新Cache未失效或未刷新检查缓存一致性做Cache Clean/Invalidate空闲中断频繁误触发串口波形有毛刺或DMA缓冲区太小检查硬件、增大缓冲区、调整空闲判定时长DMA发送的数据只有前半段源缓冲区被提前释放确认缓冲区生命周期覆盖整个发送过程DMA中断里死循环标志清除顺序不对按参考手册要求先读后清或先清后读循环模式下数据被覆盖处理速度跟不上DMA速度改用双缓冲半传输中断或调大缓冲区5.2 缓存一致性与对齐问题高性能MCU上最容易翻车如果你用的是带Cache的高性能MCU比如Cortex-M7内核DMA有一个大坑必须知道Cache和DMA的数据一致性。CPU写数据到内存时数据可能还停留在Cache里没有真正落到内存DMA搬运时直接读内存读到的可能是旧数据。反过来DMA从外设收到的数据写进内存CPU读的时候可能命中Cache里的旧值读到脏数据。解决办法是在DMA操作前后做Cache维护操作。在STM32H7等系列上HAL库有对应的接口比如SCB_InvalidateDCache_by_Addr和SCB_CleanDCache_by_Addr。规则很简单DMA写入数据后CPU读取前做一次Invalidate使Cache失效强制从内存读CPU写入数据后DMA读取前做一次Clean把Cache内容刷到内存。这一步漏了数据就是你怀疑人生的根源。对齐问题同样隐蔽。很多DMA控制器要求源地址、目的地址、传输长度满足特定对齐要求比如按字对齐。如果不满足轻则传输效率下降重则直接触发总线错误进入HardFault。常规做法是设计数据结构时就把缓冲区安排到对齐地址上或者使用编译器的对齐属性声明。我习惯性把DMA缓冲区都用4字节对齐养成习惯后能省掉一大半奇葩问题。5.3 数据错位、中断丢失、DMA卡死的排查思路数据错位的排查第一步先关掉DMA的所有优化用最简单的方式验证固定一个源缓冲区内容填递增序号DMA搬运后检查目标缓冲区是否也是递增序号。如果序号错乱说明地址步进方向或宽度配置有问题如果序号正确但周期性地缺失说明缓冲区长度和触发频率不匹配。中断丢失的排查思路要先确认中断是否真的没触发。在中断服务函数入口设置一个GPIO翻转用示波器看波形比纯日志排查高效得多。如果GPIO有翻转但你的处理逻辑没执行那就是中断优先级或标志位处理的问题如果GPIO根本没翻转那就是DMA到中断这一链路断了查DMA使能、NVIC使能、中断标志是否被过早清除。DMA卡死的排查要从“请求-响应”链路下手。DMA启动后一直没动静先确认外设的DMA请求是否产生信号路径上任何一环没打通都会卡死。我遇到过一个案例DMA通道配置完全正确但外设的DMA请求映射选错了通道DMA控制器根本收不到请求自然不干活。这种问题不查芯片手册的映射表光看代码永远找不到原因。5.4 调试DMA的几个实用手法调试DMA我最常用的工具不是调试器而是示波器和逻辑分析仪。用GPIO引脚翻转来标记关键节点比如DMA启动、传输完成、空闲中断触发把这些信号引到示波器上整个时序一目了然。这比盯着调试器的寄存器窗口直观多了尤其适合定位时序相关的疑难问题。还有一个技巧是用仿真器实时查看DMA计数器的变化。启动DMA后在调试环境里观察剩余计数值是否在递减。如果递减说明DMA在干活问题出在数据内容上如果纹丝不动说明DMA根本没被触发往回查请求源。最后一个习惯所有DMA缓冲区在调试阶段都先填上固定pattern比如0xA5。这样只要看到数据不对马上能判断是“没被写入”还是“写入但被覆盖”排查方向完全不一样。这个习惯帮我节省了无数排查时间。DMA这东西用熟了之后你会发现自己再也回不去了。以前写串口接收每收一个字节CPU都要响应一次中断现在配好DMA和空闲中断CPU该睡睡、该算算数据自己安安静静地躺在缓冲区里等着处理。这种感觉就像请了个勤快的传菜员后厨的火候、切配、摆盘你都能专心照顾再不用跑上跑下。我个人在实际操作中最大的体会是DMA的难点从来不在API怎么调而在于你愿不愿意花时间把外设的请求路径、内存的对齐要求、中断的交互时序这些底层细节彻底搞清楚。只要啃下这块硬骨头无论是串口不定长接收、ADC连续采样还是SPI刷屏、双缓冲音频在DMA面前都是同一个套路。希望这篇能把你的DMA之路铺平一点少踩几个我已经替你踩过的坑。
返回列表