ARTICLE DETAIL

资讯详情

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

嵌入式DMA深度解析:从工作原理到串口/ADC实战与疑难排查

嵌入式DMA深度解析:从工作原理到串口/ADC实战与疑难排查 搞嵌入式这些年DMA 几乎是每个外设工程师都绕不开的模块。串口收发、ADC 连续采样、SPI 刷屏、存储器搬运……DMA 就像一位任劳任怨的搬运工能在外设和内存之间搬数据搬完再喊你一声“搞定了”。可现实是不少同学对 DMA 的理解停留在“它能把数据拷过去”这个层面真正配置起来却频繁翻车DMA 怎么一直不触发收到的数据莫名其妙错位发送一帧数据后紧接着第二帧把第一帧覆盖了ADC 多通道的数组顺序对不上这些问题我在项目里都踩过后面会逐一展开。这篇总结我用“一次 DMA 传输的内部流程”为起点把工作流程、传输模式、典型应用场景和疑难杂症全部串起来讲。内容偏实战覆盖串口 DMA 不定长接收、DMA 发送时序、ADC 多通道采样、Freemodbus 与 RS485 方向控制、缓存一致性、UFS DMA、分布式 DMA 等方向。适合正在调外设的 MCU 工程师、刚入门固件开发的读者也适合想搞清楚“DMA 到底快在哪”的软件工程师。1. DMA 到底是怎么工作的——从一次传输看完整流程很多教程一开始就甩寄存器配置却很少讲清楚 DMA 内部到底发生了什么。我建议先建立一幅完整的工作流程图再去碰寄存器效率会高很多。1.1 DMA 搬运工与 CPU 老板的分工先打个比方。DMADirect Memory Access直接存储器访问本质上是一个专门干“搬运”的硬件模块。CPU 是老板DMA 是搬运工外设和内存是仓库。没有 DMA 时串口每收到一个字节CPU 就要停下手头的活进一次中断把数据从串口数据寄存器拿出来再放到内存某个数组里。一个月下来老板累死什么正事都干不了。有了 DMA 之后老板只需要提前告诉搬运工“把串口接下来 256 个字节放到内存 0x20000100 这个地址去。”搬运工就会在外设准备好事自己一次次把数据搬过去搬够了再通知老板“活干完了”。整个搬运过程 CPU 完全不参与这就是 DMA 最核心的价值转移数据不占用 CPU 的运算时间而不是让搬运本身变得更快。这个区分非常重要。很多人以为 DMA 比 CPU 直接拷贝更快其实在不少 MCU 上DMA 搬数据和 CPU 用循环搬数据的速度差距并没有想象中那么大。DMA 的真正优势是CPU 可以去跑协议栈、算 CRC、处理业务逻辑而不是把时间耗在逐字节搬运上。高负载系统里这往往意味着实时性翻倍。1.2 一次 DMA 传输的五个关键步骤无论芯片厂商是谁DMA 一次完整传输都绕不开这五个环节请求、仲裁、读取、写入、计数与完成。第一步外设发出 DMA 请求。外设串口、ADC、定时器等产生一个事件比如串口收到一个字节、ADC 转换完成、定时器更新。这个事件会被连接到 DMA 控制器的请求线上。有些外设还有“连续请求”模式相当于外设不停告诉 DMA“我一直有货你一直搬”后续会详细说。第二步DMA 仲裁器决定谁先搬。一个 DMA 控制器往往有多个通道同一时刻可能多个外设都想搬数据。仲裁器会根据通道优先级决定先响应哪个请求。这有点像多个搬运工同时向仓库大门挤门卫要按编号放行。第三步DMA 从源地址读取数据。源地址可以是外设数据寄存器也可以是内存地址。DMA 会按照配置好的数据宽度字节、半字、字一次读一个单位。第四步DMA 把数据写入目的地址。目的地址可能是内存也可能是外设寄存器。如果配置了地址自增DMA 会在每次传输后把相应地址加 1、加 2 或加 4这样连续的数据就能依次落到缓冲区不同位置。第五步计数器减一直到归零触发完成信号。DMA 控制器内部有一个传输计数器每搬一个单位减一减到 0 说明一轮传输完成。此时可以产生传输完成中断告诉 CPU“缓冲区里的数据已经就绪可以过来取货了”。我建议读者在调试任何 DMA 问题时都用这五个环节去定位。比如“DMA 没触发”大概率是第一步外设请求没配置对“数据只搬了几个就停了”大概率是第五步计数器配置成了单次模式“数据地址错乱”大概率是第三步和第四步的地址自增没开或者地址宽度不对。1.3 缓存一致性DMA 搬运工和 CPU 老板的“账单分歧”这个坑在带 D-Cache 的高性能 MCU 上特别常见比如 Cortex-M7 系列的 STM32H7、i.MX RT 等。CPU 和 DMA 都会访问内存但 CPU 可能先经过 Cache。如果 CPU 之前读过某个内存区域这块数据的副本就留在 Cache 里了DMA 直接把新数据写进物理内存CPU 再去读时读到的还是 Cache 里的旧值看起来就是“DMA 没搬成功”。反过来也很麻烦CPU 写了数据要发给外设数据还躺在 Cache 里没写回物理内存DMA 去内存搬时拿到的可能是旧数据。解决思路有两个方向。一是 DMA 传输完成后CPU 主动做 Cache 无效化操作比如SCB_InvalidateDCache_by_Addr把对应地址的 Cache 行作废强制从物理内存重新读取。二是发送数据前主动做 Cache 清理也就是写回操作让数据真正落到物理内存。更省心的做法是把 DMA 缓冲区放到不可缓存的区域很多芯片的链接脚本里会有专门的 non-cacheable 段。这个知识点在普通 M0/M3 上不太容易遇到但一旦上了 M7 或者 Linux 驱动就是必考题。2. DMA 的传输模式一次讲透配置 DMA 时最让人头疼的就是一堆模式选择单次还是循环、突发还是单次、硬件触发还是软件触发、地址自增还是固定。把这些模式理解透才能真正看懂数据手册。2.1 单次传输与循环传输一锤子买卖和永动机的区别单次传输Normal Mode很好理解DMA 按配置好的长度搬运一轮计数器减到 0 就停住必须由 CPU 重新配置启动才能开始下一轮。适合一次只处理一帧数据的场景比如发送一段固定长度的日志、搬运一张图片到屏幕。循环传输Circular Mode就更有意思了。DMA 在计数器减到 0 后会自动把计数器重新加载成初始值源地址和目的地址也回到初始位置继续开启下一轮搬运。这样外设的数据就能不停歇地循环写入内存缓冲区非常适合串口不定长接收、ADC 连续采样这类场景。循环模式配合“读取剩余计数器”的技巧在嵌入式里非常常用3.1 节我会给出具体操作。但要注意循环模式下如果 CPU 处理速度跟不上DMA 会绕过缓冲区头部继续写把旧数据覆盖掉。所以一般会结合半传输中断Half Transfer和传输完成中断让 CPU 在两个半区之间交替处理也就是常说的双缓冲。2.2 突发传输与连续请求为什么搬一次数据要“憋个大招”先解释突发传输Burst。普通传输模式下DMA 每搬一个单位就要重新申请一次总线控制权搬完立刻释放。这个过程的仲裁开销虽然不大但数据量很大时累积起来就很可观。突发传输的思路是DMA 一旦拿到总线控制权就连着搬 4 个、8 个、16 个单位再释放总线。相当于搬运工把小车装满再走而不是一次端一杯水。突发传输对上位外设的要求也高一些。外设端最好用 FIFO 做缓冲否则 DMA 一次搬 16 个单位外设可能根本没准备好那么多数据。所以配置突发长度时要同时看 DMA 的 FIFO 能力和外设的 FIFO 深度不能盲目开最大突发。再解释连续请求Continuous Requests。这个特性在不少 DMA 控制器包括部分 ARM 内核 SoC 的 DMA 和 Linux dmaengine 驱动里都有体现。常规模式下DMA 每搬一个单位都要等到外设再次发出请求才继续。连续请求开启后一旦 DMA 被触发它就会在整轮传输中持续向总线发起访问不再逐个等待外设请求信号。这种模式适合外设数据持续产生的场景能显著提高搬运效率但要注意总线占用时间变长可能影响 CPU 或者其他外设的总线访问延迟。2.3 传输方向与触发源选择M2M、M2P、P2M、P2P 怎么选按传输方向分DMA 通常有四种组合内存到内存M2M、内存到外设M2P、外设到内存P2M、外设到外设P2P。M2M一般用软件触发。因为内存本身不会主动产生“我有数据了”这种请求必须由 CPU 写一个触发位DMA 才开始搬。典型用途是内存拷贝、Flash 数据搬到 RAM、图形缓冲区整体搬移。M2P和P2M是最常用的分别对应 DMA 发送和 DMA 接收。P2P用得少但有些场景很香比如定时器直接触发 DMA 把数据从一个外设搬到另一个外设全程不需要 CPU 插手。触发源选择上硬件触发和软件触发要分清楚。串口、ADC、定时器这类外设的请求都是硬件触发的DMA 配置里要把触发源设置成对应外设。软件触发则用于内存到内存传输或者在调试时需要手动触发一次搬运。很多人配 DMA 后一直不工作原因往往是把硬件触发的外设写成了软件触发一旦查到这里问题基本就水落石出。2.4 优先级、数据宽度与地址自增细节决定成败优先级主要影响多个 DMA 通道同时请求时的响应顺序。要注意的是DMA 优先级高不代表系统就一定跑得快如果 DMA 一直占着总线CPU 的中断响应和普通指令访问都会受影响。音频播放、电机控制这类对实时性要求高的场景建议 DMA 优先级适中不要盲目拉满。数据宽度指每次搬运的数据单位常见 8 位、16 位、32 位。这里有个经典错误ADC 是 12 位分辨率转换结果存在寄存器低 16 位有人把 DMA 数据宽度配成 32 位数组却定义成uint16_t adc_val[4]结果相邻数据错位。正确的是把 DMA 宽度配成半字16 位数组类型也用uint16_t。源地址、目的地址、数据宽度三者的匹配关系是排错时最先要检查的几个点。地址自增也容易理解内存缓冲区通常需要自增这样才能把连续数据写到不同单元外设数据寄存器通常固定因为每次都往同一个寄存器读写。配置时如果源地址和目的地址的自增位搞反就会出现“所有数据都堆在一个地址里”的诡异现象。3. 嵌入式场景里 DMA 的典型用例与操作要点这一节聊最常见的几个场景尤其是串口 DMA 接收不定长数据、DMA 发送时序、ADC 多通道采样和 Freemodbus 结合 DMA。这些场景覆盖了大部分 MCU 工程师的实际需求。3.1 串口 DMA 接收不定长数据空闲中断 CNDTR 精确算帧长串口接收是 DMA 最经典的应用。定长帧好办固定缓冲区大小就行不定长帧就麻烦了CPU 不知道一帧数据什么时候结束。目前最常用的方案是DMA 循环接收 串口空闲中断IDLE。空闲中断的含义是串口在一段时间内没有收到新字节就认为当前这一帧结束了。比如设备间通过串口发送 AT 指令每条指令长度不同但发完指令后线路上总会出现一段安静期这个安静期就是空闲中断触发点。收到空闲中断后CPU 只需要读取 DMA 当前剩余计数就能算出这一帧到底收了多少字节。以 STM32 HAL 库为例思路是这样#define RX_BUF_SIZE 256 uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_len 0; // 初始化时开启 DMA 循环接收 HAL_UART_Receive_DMA(huart1, rx_buf, RX_BUF_SIZE); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 串口中断处理 void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 停止 DMA防止继续写入覆盖数据 HAL_UART_DMAStop(huart1); // 读取 DMA 剩余计数 uint16_t remain __HAL_DMA_GET_COUNTER(huart1.hdmarx); rx_len RX_BUF_SIZE - remain; // 此时 rx_buf 里就是完整的一帧数据交给业务处理 process_frame(rx_buf, rx_len); // 重新开启 DMA 循环接收 HAL_UART_Receive_DMA(huart1, rx_buf, RX_BUF_SIZE); } }这段逻辑的核心就是那个remain。DMA 循环模式计数器会从初始值 256 往下减每收一个字节减一。空闲中断来临时剩余 200就说明这一帧收了 256 - 200 56 个字节。这个算法非常实用在各家 MCU 上思路一致区别只在于寄存器名字和库函数写法。实际操作中有两个容易踩的坑。第一个是缓冲区溢出如果一帧数据超过 RX_BUF_SIZEDMA 会绕回缓冲区头部继续写把前面的数据覆盖掉。所以缓冲区大小要按项目里最大帧长来定宁可大一点。第二个坑是空闲中断标志位的清除顺序有些 MCU 必须在读取数据寄存器之后清除才能彻底清干净不然会频繁误触发。每次换型号后第一步就是看一眼参考手册里 IDLE 标志的描述。3.2 串口 DMA 发送要不要等上一轮发完我踩过的坑直接回答要看情况但大多数情况下需要确保缓冲区不再被修改或者 DMA 已经搬完再启动新一轮发送。DMA 发送的特点是调用发送函数后数据还在内存缓冲区里DMA 会逐个把数据搬到串口数据寄存器。如果第一次发送还没搬完你就往同一块缓冲区写了新数据发送出去的内容就会新旧混合甚至直接出错。我在一个项目里遇到过一个非常隐蔽的问题用 HAL_UART_Transmit_DMA 连续发送两帧数据第一帧调用后立刻返回第二帧数据也被写到了同一个发送缓冲区结果不仅第一帧被破坏第二帧的开头还丢了几个字节。后来才发现问题根源就是没有等第一帧 DMA 搬完。解决办法有三种。第一种最简单发送完一帧后用一个标志位记录状态在 DMA 传输完成中断里置位主循环发送前先检查标志。第二种是给每帧数据分配独立缓冲区通过队列依次发送相当于发快递一样一车拉完再装下一车。第三种是双缓冲乒乓切换适合数据量很大、实时性要求高的场景同一时间一块缓冲在发送、另一块缓冲在填充。另外还有一个细节很多人忽略DMA 传输完成不等于串口已经把最后一个字节发出去了。DMA 只是把数据搬到了串口的发送数据寄存器移位寄存器还要一位一位往外移。如果下一帧 DMA 立刻写入前面没发完的数据可能会被挤掉。在普通调试中偶尔丢最后一个字节问题就出在这。严谨的做法是除了等 DMA TC 标志还要再等串口自身的 TCTransmission Complete标志。3.3 ADC 四通道 DMA 采样多通道连续转换的缓冲布局ADC 多通道 DMA 采样的核心思想是ADC 按照扫描顺序依次转换多个通道每转换完一个通道就触发一次 DMA 请求把结果搬到内存数组对应的位置。假设有四个通道按顺序是 CH0、CH1、CH2、CH3那就定义一个uint16_t adc_val[4]并开启 DMA 循环模式。每轮扫描结束后adc_val[0]存 CH0 的结果adc_val[1]存 CH1 的结果依此类推。如果开启连续转换DMA 会不停刷新这个数组CPU 需要的时候直接读数组即可。这里要特别注意两个配置。第一扫描模式Scan Mode必须打开否则 ADC 只会转换第一个通道后面的通道永远不会触发 DMA。第二DMA 传输长度不是总采样次数而是一轮扫描的通道数量如果 ADC 配置成多轮采样取平均缓冲区大小要相应乘上采样次数。还有个小技巧很多 MCU 的 ADC 转换结果寄存器只有 12 位有效数据右对齐存储。如果 DMA 数据宽度配成 32 位而数组定义成uint16_t数据会错位。我的习惯是DMA 宽度和数组类型保持完全一致ADC 结果寄存器是多少位有效就用多大宽度搬运。3.4 Freemodbus 与 DMA 结合帧间隔与 RS485 方向控制的坑Modbus RTU 是工业现场最常见的协议之一Freemodbus 则是开源实现的经典。RTU 模式对帧间隔有明确要求两个帧之间的空闲时间要大于 3.5 个字符时间一帧内部字符之间的间隔不能超过 1.5 个字符时间。用 DMA 接收时DMA 本身不关心这些时间间隔它只管往内存里塞数据所以帧定界必须靠其他手段。方案一是串口空闲中断 DMA和 3.1 节介绍的方法类似。空闲中断的时间阈值在不同 MCU 上有差异有些正好落在 3.5T 附近可以近似使用有些则需要额外用定时器做超时判断定时器里记录当前收到的累加字节数超过 3.5T 没收到新字节就判定为一帧结束。方案二是定时器 DMA 配合每次串口收到数据时除了 DMA 搬运还启动一个定时器。定时器超时说明线路空闲再读取 DMA 计数器算出帧长。这个方案对帧定界更精确代价是要占用一个定时器资源。发送方向还有个更隐蔽的坑RS485 半双工通信需要控制方向引脚发送前拉高 DE发送完成后拉低 DE。如果用 DMA 发送DMA 完成中断意味着数据已经全部搬到发送数据寄存器但移位寄存器可能还在发最后几个字节。这个时候立刻拉低 DE最后一个字节或停止位就会被硬生生截断接收端 CRC 校验必错。我的做法是DMA 完成中断里不直接切方向而是等待串口 TC 标志位置位再拉低 DE。不同 MCU 的 TC 标志读取方式有差异但原理一致。这里也建议给方向切换加个小的超时保护防止异常情况下 DE 一直拉高占用总线。4. 疑难杂症与排查技巧实录DMA 的问题往往不是原理难而是现象千奇百怪。这一节把我实际遇到过和帮别人排查过的高频问题整理成一份速查思路按症状分类排查效率会高很多。4.1 厂商 DMA 通道 Bug 与差异以 BAT32 为例的踩坑记录DMA 虽然各家实现思路相似但寄存器细节差异很大。尤其是国产 MCU 这些年用得越来越多很多开发者在 BAT32 这类 MCU 上遇到过“看起来配置没问题可就是工作异常”的情况。我印象比较深的是 BAT32 系列 DMA 通道的一些异常表现某些通道在循环模式下地址回绕偶尔异常缓冲区写了一圈儿之后源地址或目的地址没有回到初始位置导致数据错位。还有一些情况是 DMA 请求标志在中断处理完后没有完全清除导致中断反复触发CPU 被拖死。这属于厂商特定问题做法是先查官方的数据手册勘误表Errata确认当前芯片型号和批号是否存在类似问题再决定要不要绕过这个通道。我强烈建议在项目初期做一次DMA 通道最小验证测试写一个只用软件触发的 M2M 传输把固定长度数据从一个数组搬到另一个数组检查结果是否完全一致再换到硬件触发模拟真实外设请求。每个通道、每种模式都单独验一遍不要只在应用代码里带病运行。这样一旦后面出了问题能快速判断是芯片 bug 还是配置问题。4.2 缓存一致性排错看起来搬了其实是旧数据前面提到过 D-Cache 会导致 DMA 数据是旧值这里给一个具体的排查记录。一台基于 Cortex-M7 的设备ADC 通过 DMA 循环采样主循环里读adc_val[0]数值却始终不变重新初始化 DMA 后才变一次之后再无更新。排除 DMA 配置和触发源之后怀疑到了 Cache 上。用调试器查看物理内存地址里的数据发现其实 ADC 一直在更新DMA 工作完全正常。问题在于 CPU 访问时命中了 Cache读到的是旧副本。解决办法有两个一是配置 DMA 缓冲区到 non-cacheable 区域二是每次读数据前执行SCB_InvalidateDCache_by_Addr。我用的是第一种省心且不容易漏。缓存一致性问题在 Linux 驱动里体现得更明显。内核提供了dma_alloc_coherent这类接口分配的就是一致性内存而流式映射dma_map_single要求驱动在正确的时机做 invalidate 或 clean。如果驱动里 DMA 收到的数据和预期不一样先检查 Cache 操作是否缺失这条经验在 RTOS 上同样适用。4.3 DMA 与 CPU 总线的“抢道”在哪儿加延迟判断DMA 大量搬数据时会和 CPU 竞争总线访问。比如 SPI 屏幕刷新DMA 一下子把几百 KB 图像数据搬过去此时 CPU 要访问内存变量或 Flash就可能出现短暂延迟。对普通应用影响不大但遇到音频中断、伺服控制这类要求低延迟的场景问题就会放大。我的建议是DMA 通道优先级不要一味设最高根据数据实时性需求来权衡。如果数据的价值在于“必须搬完”比如显示刷新那优先级可以高一些如果数据的价值在于“CPU 要尽快响应其他中断”那 DMA 优先级就应该让路。还有就是在中断服务程序里尽量少做重活把耗时的处理挪到主循环或低优先级任务给总线留出余量。排查总线抢占问题时可以观察关键中断的响应时间是否抖动。用逻辑分析仪同时抓 DMA 中断输出和某个周期性任务引脚如果任务引脚出现毛刺基本能确定是总线竞争导致。4.4 DMA 测速与性能验证怎么证明 DMA 真的更快DMA 测速不能只看“搬完 1MB 数据用了多久”重点是对比 CPU 占用率。MCU 上最原始也最有效的方法是用 GPIO 翻转一个引脚在启动 DMA 前拉高DMA 完成后拉低用示波器量高电平时间同样数据量用 CPU 循环拷贝再测一次对比就出来了。比如用 DMA 搬 10KB 数据GPIO 高电平时间约 60us用for循环搬同样 10KB高电平时间约 180us。这个对比结果就说明了问题。还可以用 DMA 状态寄存器里的剩余计数配合一个定时器连续搬运一段时间后算平均带宽。Linux 下的思路类似。可以对比普通文件读写的耗时和 CPU 占用比如dd配合perf stat或者直接看iostat里的%util。很多存储控制器的带宽测试工具本质都是在统计 DMA 完成中断的频率。理解了这点你就不会被各种“测速软件”的界面唬住它们测的无非就是搬运耗时和 CPU 占用这两件事。5. 从 MCU 到存储与分布式系统DMA 的更大舞台MCU 里我们说的 DMA只是整个计算体系里搬运工的一种形态。往大了看UFS 存储控制器、网络适配器、分布式系统中处处都有 DMA 思想的影子。5.1 UFS DMA 与 PRDT存储控制器里的 DMA 引擎UFSUniversal Flash Storage是现代手机和移动设备里常用的闪存存储标准。UFS 主机控制器内部就有一个 DMA 引擎它的职责是把系统内存里的命令和数据搬到 UFS 设备或者把设备读出的数据直接写入系统内存。这里有一个概念叫 PRDTPhysical Region Descriptor Table物理区域描述符表。可以理解成一张“送货清单”每一项描述了一段物理内存区域的地址和长度。UFS 控制器根据这张清单自动完成复杂的内存搬运而不需要 CPU 逐块拷贝。这和 MCU 里 DMA 的“源地址、目的地址、传输长度”本质上是一回事只是规模更大、描述更复杂。从 MCU 转到存储方向的同学如果能理解 MCU DMA 的工作流程再看 UFS 的 PRDT、CMD 描述符这类概念会觉得非常亲切。核心都是CPU 把搬运任务描述清楚交给专门的硬件执行。5.2 分布式 DMARDMA绕过 CPU 的网络搬运分布式系统里常提到的 RDMARemote Direct Memory Access是把 DMA 的思想延伸到了网络层面。传统网络传输中数据要从应用内存拷贝到内核缓冲区再被网卡发送出去接收方向类似中间至少要经过一次甚至多次 CPU 拷贝。RDMA 的解决方案是让网卡上的 DMA 引擎直接读写应用内存数据从一台机器的应用内存直接搬上网络再从网络直接写入另一台机器的应用内存整个过程中 CPU 几乎不参与。这种“跨机器的 DMA 搬运”对低延迟、高带宽场景特别重要典型应用包括高性能计算、分布式存储、AI 训练集群等。虽然 RDMA 的实现细节比 MCU DMA 复杂得多但核心思想完全一致把数据搬运的任务从 CPU 转到专用硬件释放 CPU 去做更有价值的事情。5.3 学习 DMA 的三个阶段建议从我带人和自学的经验看掌握 DMA 大概会经历三个阶段。第一阶段是“能用”照着例程改改寄存器或 HAL 参数串口能收到数据、ADC 能采样了。这个阶段大部分人都会经历但也是最容易留下隐患的阶段因为只是“表面会用”。第二阶段是“懂传输流程”理解请求、仲裁、读取、写入、计数、完成中断这一整套闭环。到这个程度调 DMA 问题时你不再靠猜而是能顺着流程逐个排查。第三阶段是“能解决疑难杂症”面对缓存一致性、总线抢占、厂商 bug 这些深层问题时能够快速定位。这个阶段需要大量实战积累也需要对芯片体系结构有整体理解。我给出的建议很朴素先跑通一个最小的 DMA 工程用调试器观察源地址、目的地址、计数器这三个寄存器值的变化再看数据手册里的 DMA 框图把信号流向画在自己的笔记里最后才是堆项目经验。最后分享几点个人体会DMA 这个模块平时存在感不强但一旦出了问题往往能把人折腾到怀疑人生。我最早也是抄例程搞定的串口 DMA 接收后来在一次 RS485 方向切换丢数据的调试中才真正意识到“DMA 传输完成”和“数据真正发送完成”是两码事。从此养成了一个习惯每个外设 DMA 的组合都先用最简程序验证传输的每个环节再往业务里加逻辑。如果你正在被 DMA 的某个诡异问题困住我的建议是先把源地址、目的地址、传输宽度、地址自增、触发源、计数器初始值这六个参数挨个过一遍再看参考手册里 DMA 的时序框图和外设的请求映射表。八成的 DMA 问题根源都在这几个参数上。至于剩下的两成比如芯片 bug、缓存一致性、总线竞争那就需要靠调试器和勘误表来治了。希望这篇总结能帮你少走一些弯路。
返回列表