DMA核心原理解读:从STM32到高性能平台的配置实战
1. DMA 到底是什么一次讲清楚它的核心逻辑很多做嵌入式开发的朋友第一次接触 DMA 是在用 STM32 的时候。串口接收、ADC 采样、SPI 刷屏只要数据量一大CPU 就被中断淹没了。后来有人告诉你“用 DMA 吧”你一查手册看到一堆 Direct Memory Access、通道、请求映射、循环模式这些词头就大了。我先把 DMA 的本质用一句话说清楚DMA 是一条让外设和内存之间直接搬运数据的专用通道搬运过程中 CPU 不需要逐字节参与只需要在开始和结束时被通知一声。打个比方你开了一家小餐馆CPU 就是老板。传统模式下每一桌客人点菜老板都要亲自去后厨下单、端菜、结账客人一多老板就忙不过来。DMA 相当于你请了一个专门的传菜员客人点完菜之后老板只需要告诉传菜员“把这几桌的菜从后厨端到对应桌子”传菜员自己一趟一趟跑老板可以继续干别的事等传菜员全部端完了再跟老板说一声“搞定了”。这个比喻基本能覆盖 DMA 的核心工作方式接下来我会从原理到应用把 DMA 的完整链路拆开讲清楚。1.1 DMA 为什么需要存在CPU 的效率困境先看一个很常见的场景。你用 STM32 的串口接收一批数据假设一次收 1KB。如果用传统的中断接收方式每收到一个字节串口外设就会触发一次中断CPU 要停下来保存现场、跳到中断服务函数、把数据从数据寄存器搬到内存数组、再恢复现场。1KB 数据就是 1024 次中断CPU 大量时间都耗在进出中断的过程中。实际上串口波特率还不算高如果是 ADC 连续采样、SD 卡读写、以太网接收这种高速场景CPU 被占用的比例会非常可怕。算一笔简单的账假设系统主频 168MHz一次完整的中断进出需要大约 40 个时钟周期这已经是比较乐观的估计了那么 1024 次中断光中断开销就是 40960 个周期这还不算中断服务函数里实际搬数据的时间。如果把 CPU 时间去干 PID 算法、显示刷新、按键扫描效率会高得多。DMA 就是为了解决这个效率困境而生的。它把“搬运数据”这个重复劳动从 CPU 手里接管过来CPU 只需要做两件事一是配置 DMA 传输的参数二是等 DMA 传输完成后处理数据。1.2 DMA 的核心组成从硬件视角看传输链路要真正理解 DMA不能只看软件层面的函数调用得知道硬件上都有哪些部件在协作。一个完整的 DMA 传输链路通常包含这几部分DMA 控制器这是 DMA 的“大脑”负责管理所有 DMA 通道的传输请求、优先级仲裁、传输状态。在 STM32 上它一般是独立的外设挂在总线矩阵上。DMA 通道每条通道对应一个或多个外设请求源可以理解为一条独立的“传送带”。不同通道可以配置不同的传输方向、数据宽度、优先级。外设请求源串口、ADC、定时器、I2C、SPI 这些外设在特定事件发生时会向 DMA 控制器发出传输请求。比如串口收到一个字节就会产生一个“接收数据请求”。存储器地址和外设地址DMA 搬运数据的两个端点。一个通常是外设的数据寄存器地址比如 USART_DR另一个是内存地址比如数组的首地址。传输计数器记录还需要传输多少个数据单元每传完一个就减一减到零就表示传输完成。总线仲裁器当多个 DMA 通道同时请求传输时由仲裁器决定谁先谁后。这里要特别强调一个点很多人写代码的时候只关心调用了哪个 DMA 函数却忽略了外设侧的配置。实际上 DMA 能不能正常工作一半取决于 DMA 控制器的配置另一半取决于外设是否开启了 DMA 请求功能。比如你要用串口 DMA 接收必须在串口初始化时使能 USART 的接收 DMA 请求光配置 DMA 控制器是没用的。2. DMA 的工作流程从触发到完成的完整生命周期DMA 的工作流程看起来只有“配置-启动-完成”三步但每一步里面都有不少细节。我按照实际开发中会经历的完整流程来拆解。2.1 DMA 工作流程的六个阶段一个标准的一次性 DMA 传输非循环模式完整生命周期是这样的外设事件触发请求外设产生了某个事件比如串口收到了一字节数据、ADC 完成了一次转换、定时器更新事件发生。这个事件会转化为 DMA 请求信号发送给 DMA 控制器的对应通道。DMA 控制器仲裁如果同时有多个通道请求传输DMA 控制器会根据通道优先级进行仲裁。优先级高的先响应同级的话通道号小的优先不同芯片实现略有差异。源地址读取DMA 控制器从源地址读取一个数据单元。源地址可能是外设数据寄存器也可能是内存数组。目的地址写入DMA 控制器把读到的数据写入目的地址。目的地址可能是内存数组也可能是外设数据寄存器。地址更新与计数递减根据配置DMA 会自动决定源地址和目的地址是递增、递减还是固定不变同时传输计数器减一。传输完成判断如果计数器减到零DMA 产生传输完成中断如果使能了中断并自动禁用该通道。如果是循环模式计数器重载继续下一轮传输。从软件角度看这个流程只需要三个步骤初始化 DMA 通道参数、使能外设的 DMA 请求、启动 DMA 传输。后续的数据搬运全部由硬件完成。2.2 DMA 传输方向为什么说“方向”是个坑DMA 支持三种传输方向但在不同芯片、不同外设上这三种方向的配置方式差异很大这也是新手最容易踩坑的地方。外设到内存典型场景是串口接收、ADC 采样、I2C 接收。外设数据寄存器是源数组是目的。内存到外设典型场景是串口发送、DAC 波形输出、SPI 发送。数组是源外设数据寄存器是目的。内存到内存典型场景是内存拷贝。两个内存地址互传常用于在 RAM 内部搬数据。这里有个大坑内存到内存的 DMA 传输不是所有外设请求源都不需要。在 STM32 上内存到内存模式是软件触发的不需要外设请求配置好之后直接启动 DMA 通道就行。但在某些芯片上内存到内存模式需要选择一个外设请求源来触发如果你没搞清楚就照抄别人的代码很可能会发现 DMA 根本启动不了。还有一个更隐蔽的问题外设到内存方向外设地址通常是固定不变的因为数据寄存器地址就一个但内存地址需要自动递增。而在内存到外设方向内存地址递增外设地址固定。如果你的地址递增模式配置反了数据会全部写到同一个位置最后一次写入覆盖前面的数据。2.3 DMA 传输模式单次、循环、乒乓与离散传输DMA 的传输模式直接决定了你的代码架构我逐个说一下。单次传输模式Normal Mode传输计数器从初始值减到零之后DMA 通道自动禁用需要软件重新设置计数器并启动才能再次传输。这种模式适合一次性的数据搬移比如开机把一段数据从 Flash 拷到 RAM。循环传输模式Circular Mode传输计数器减到零后自动重载初始值DMA 通道始终处于使能状态。这种模式特别适合 ADC 连续采样、串口不定长接收。数据会不断写入环形缓冲区你可以通过 DMA 的中断回调或者轮询 DMA 当前计数值来判断新数据来了多少。乒乓传输模式Ping-Pong Mode这是循环模式的一种变体使用两块缓冲区交替接收数据。当 DMA 写满缓冲区 A 时自动切换到缓冲区 B 写入同时通知 CPU 处理缓冲区 A 的数据写满 B 后又切回 A。这样 CPU 处理数据的时间和 DMA 采集数据的时间完全重叠不会出现数据覆盖的问题。在高速 ADC 采集、音频处理场景中非常实用。离散式传输 / Scatter-GatherSG模式这是一种更高级的 DMA 模式。标准 DMA 传输要求源地址和目的地址分别是连续的内存块而 Scatter-Gather 模式允许你定义一组“传输描述符”每个描述符指定一个小的传输任务源地址、目的地址、传输长度DMA 自动按顺序执行这一组描述符。这样做的好处是不需要先做内存拷贝把分散的数据集中到连续缓冲区直接按描述符搬运即可。Linux 下很多驱动程序就是依赖硬件的 Scatter-Gather 能力来减少内存拷贝开销的。在实际项目里串口 DMA 接收通常会配合空闲中断和环形缓冲区使用。数据不是按固定长度到达的DMA 循环模式把数据不停写入缓冲区串口空闲中断告诉你“这一帧数据接收完了”你再从缓冲区里把这一帧数据提取出来处理。3. DMA 的传输模式与关键参数配置实战这一部分我以实际开发中最常见的场景为例带着大家一步步配置 DMA。选型上以 STM32 为主因为用的人最多但原理是通用的其他芯片完全可以对照理解。3.1 传输的参数到底该怎么定数据宽度、地址递增与突发传输配置 DMA 时有几个参数是必须明确的我先逐个讲清楚。数据宽度Data WidthDMA 每次搬运一个数据单元的大小可选字节8 bit、半字16 bit、字32 bit。这个参数必须和源/目的的数据宽度匹配。比如 ADC 的 12 位采样结果存储在 16 位寄存器里那数据宽度就选半字。串口发送的数据是字节流就选字节。如果你把数据宽度配错了搬出来的数据要么被截断要么多了垃圾数据。这里有一个需要特别注意的点外设数据宽度和内存数据宽度不一致时能否转换答案是大部分 DMA 控制器支持外设宽度与内存宽度不同。比如外设是 8 位的寄存器内存是 32 位的数组DMA 可以把 4 个字节拼成一个字写入内存。但这种模式使用时要非常小心不同芯片的行为不完全一致建议没有特殊需求就保持一致。地址递增Address Increment控制 DMA 每次传输后源地址和目的地址是否自动加一。原则很简单如果数据是连续存储的就递增如果源或目的固定在一个寄存器上就不递增。最常见的是外设到内存方向外设寄存器固定内存地址递增。突发传输Burst TransferDMA 每次仲裁获得总线后连续传输多少个数据单元再释放总线。支持 1、4、8、16 等不同突发长度。突发传输可以提高总线利用率因为减少了仲裁次数。但要注意突发传输通常要求源/目的地址按突发长度对齐如果你的缓冲区地址没有对齐可能触发 DMA 传输错误DMA Transfer Error。优先级Priority同一 DMA 控制器下多个通道同时请求时的响应顺序。合理设置优先级很重要。比如 ADC 连续采样用于电流环控制那这个通道就应该给最高优先级否则在突发高负载下采样间隔抖动会影响控制性能。3.2 一次完整的串口 DMA 收发配置过程我以 STM32 HAL 库为例演示一个完整的串口 DMA 收发配置过程。这个例子已经过实际验证大家可以作为参考。第一步在 CubeMX 里启用串口的 DMA 请求。找到 USART 的 DMASettings添加 TX 和 RX 两个 DMA 通道。TX 方向是 MemoryToPeripheralRX 方向是 PeripheralToMemory。两个通道的模式都选 Normal优先级根据需求设置。第二步在代码初始化中启动 DMA 接收uint8_t rx_buffer[256]; uint8_t tx_buffer[128]; // 启动 DMA 接收接收 256 字节后触发完成回调 HAL_UART_Receive_DMA(huart1, rx_buffer, 256);第三步发送数据时也走 DMAHAL_UART_Transmit_DMA(huart1, tx_buffer, len);这样配置完成后CPU 不再被串口中断打扰。接收端收满 256 字节后HAL 库会调用HAL_UART_RxCpltCallback你在这个回调里处理数据即可。但实际项目中很少有恰好接收固定长度数据的场景。更实用的是结合串口空闲中断来做不定长接收。原理是DMA 循环接收写缓冲区串口空闲中断表示总线上一段时间没有新数据即一帧数据结束了此时可以从缓冲区提取数据。CubeMX 开启串口空闲中断的方法是在 NVIC 设置中使能 USART global interrupt然后在代码中先调用HAL_UART_Receive_DMA再调用__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE)开启空闲中断。在串口中断回调里检测空闲标志void HAL_UART_IRQHandler(UART_HandleTypeDef *huart) { // 其他处理... if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart); // 计算当前 DMA 接收了多少新数据进行处理 } }这里有个细节空闲中断不是 HAL 库默认帮你处理好的你需要在串口中断服务函数里自己判断和清除标志位。具体做法一般是在HAL_UART_IRQHandler之后追加自己的判断逻辑。3.3 基于 DMA 计数值计算接收数据长度在循环接收模式下怎么知道 DMA 已经往缓冲区里写了多少新数据这需要读取 DMA 控制器的当前计数值。HAL 库提供了获取剩余未传输计数的函数。我用一个实际场景说明DMA 缓冲区大小 256 字节循环模式已经接收了 100 字节此时 DMA 的计数器值应该是 256 - 100 156。等到接收了 200 字节计数器值是 256 - 200 56。注意如果已经接收了 300 字节计数器会从 256 重新开始递减当前值是 256 - (300 - 256) 212。所以在代码中计算当前已接收数据量的公式是uint16_t remain __HAL_DMA_GET_COUNTER(hdma_usart1_rx); uint16_t received rx_buffer_size - remain;但要正确处理环形缓冲区的边界不能让新旧数据互相覆盖。一般做法是维护一个 last_pos 变量记录上一次处理的位置根据最新 received 和 last_pos 之间的关系计算新数据长度然后更新 last_pos。AC 实际项目中如果处理不及时导致 DMA 缓冲区写满新的数据会覆盖旧数据因此缓冲区大小和处理速度必须匹配。这种问题在高速连续传输场景下尤其突出。3.4 DMA 的中断管理完成中断与错误中断DMA 传输完成会产生完成中断传输错误会产生错误中断。在 HAL 库中完成中断对应HAL_DMA_TC_Callback或外设的 RxCplt 回调错误中断对应HAL_DMA_ErrorCallback。很多初学者只关注完成中断忽略了错误中断。实际使用中错误中断常常能帮你快速定位问题。比如地址未对齐导致的总线错误、非法地址访问、传输被异常中止等都会触发错误中断。建议在开发阶段把错误回调函数打印出当前的 DMA 状态寄存器和错误标志定位问题会快得多。另外要特别注意中断服务函数的耗时。DMA 完成中断虽然比逐字节中断频率低但在高采样率下触发频率依然可能很高。比如 1MHz 采样率下每次采样触发一次完成回调那主循环就别想干别的了。这种情况下考虑启用乒乓模式或者用循环模式批量处理数据。4. 典型应用场景拆解从 STM32 到高性能处理器的实际用法DMA 的应用范围非常广从单片机到应用处理器从简单的串口收发到复杂的多通道采集系统。我挑几个典型场景细致拆解。4.1 串口 DMA最基础也最常用的场景串口 DMA 是目前嵌入式开发中使用最多的 DMA 场景核心价值在于解决两个问题一是发送长数据时阻塞 CPU二是接收频繁数据时中断太密。串口 DMA 发送直接调用HAL_UART_Transmit_DMA即可。需要特别注意DMA 发送是异步的如果你发完立即改变缓冲区内容DMA 可能还没搬完。所以要么等传输完成回调后释放缓冲区要么用互斥机制保护。另外连续调用两次 DMA 发送时需要等上一次传输完成否则会返回 HAL_BUSY。有朋友问“串口 DMA 发送需要等待上一轮数据发送完吗”我的答案是如果发送内容无关紧要、可以丢弃那可以不等待直接重新配置缓冲区发送。但不能直接复用同一个缓冲区否则数据会被新内容覆盖出现乱码。更稳妥的做法是维护一个发送队列把待发送的数据丢进队列DMA 发送完成后再从队列取下一段。串口 DMA 接收固定长度和不定长两种。固定长度用HAL_UART_Receive_DMA收满触发回调不定长则配合空闲中断前面已经讲过原理。这里再补充一个常见问题接收过程中如果发生了错误比如帧错误、噪声错误DMA 会停止接收。HAL 库在错误回调里会调用HAL_UART_DMAStop之后你需要重新启动 DMA 接收。很多项目出现“串口收了一会儿就再也不收了”的问题就是因为没有处理这个错误恢复流程。4.2 ADC 多通道采集 DMA数据如何保持通道对应关系ADC 多通道使用 DMA 是另一个高频场景。用 STM32CubeMX 配置 ADC 多通道 DMA 采集时最常见的问题是通道数据顺序乱了。数据乱掉的本质原因是ADC 的“注入通道”和“规则通道”转换顺序不同多通道规则转换时是轮流转换各个通道DMA 按照转换完成顺序把结果写入缓冲区。如果你的通道扫描顺序和 DMA 缓冲区索引的对应关系没弄清楚拿到数据后就会张冠李戴。举个例子你配置了 ADC 通道 0、1、2、3扫描顺序就是 0→1→2→3。DMA 写入缓冲区的顺序就是第一次转换结果对应通道 0第二次对应通道 1以此类推。这个顺序是固定的你要做的就是把缓冲区索引和通道号对应起来。但问题往往出在“时序”上。如果 ADC 各通道的采样时间设置不当多通道扫描一次的总时间可能超过你的预期。DMA 在循环模式下的写入是和转换同步进行的只要 DMA 缓冲区大小恰好等于通道数就不会乱。实际使用中另一个大坑是ADC 数据寄存器的数据宽度与 DMA 配置不匹配。STM32 很多 ADC 的转换结果是 12 位或 16 位存储在 32 位的数据寄存器里。如果你在 CubeMX 里配置 DMA 数据宽度为 Byte问题就来了每次 DMA 传输只搬低 8 位数据自然不对。正确做法是根据具体芯片来选择STM32F4 一般选择 Half Word 或 Word这个细节如果不对采集到的数据完全没法用。以 GD32E230 为例网上很多人反映 ADC DMA 数据紊乱我排查过类似问题常见原因包括DMA 通道选择错误GD32 的 DMA 请求映射和 STM32 不完全一样、ADC 校准未完成就启动转换、DMA 传输完成中断标志未及时清除。排查这类问题建议先关掉 DMA 中断只观察缓冲区数据再逐步开启功能缩小问题范围。4.3 PWM DMA用 DMA 生成任意波形PWM DMA 的核心思路是定时器利用 DMA 来更新比较寄存器从而在 PWM 输出引脚上产生一个任意形状的波形这个过程中 CPU 不需要每周期都去操作寄存器。实现方法大概是定时器产生更新事件触发 DMA 把内存里的波形表搬到定时器的比较寄存器。每个 PWM 周期DMA 自动把下一个波形值写入寄存器输出引脚就跟着波形表变化。通过调整波形表的内容你可以输出正弦波、三角波、任意自定义波形不需要软件实时计算。这个方案适合 LED 呼吸灯效果、音频播放通过 PWM 输出模拟音频、步进电机的复杂控制波形等场景。需要注意的点波形表的更新频率决定了输出波形的频率和精度。如果定时器频率是 1MHz波形表有 100 个点那输出波形频率就是 10kHz。波形表放在内存里DMA 配置为循环模式每次定时器更新事件到来就搬一个数。内存到外设方向外设地址固定为比较寄存器内存地址递增。还有一个技巧如果需要更高分辨率的输出可以采用 PWM 硬件定时器 DMA 的“有限状态机”思路让 DMA 同时搬运多路波形数据到多个通道的比较寄存器实现多路波形同步输出。4.4 I2C、SPI、UFS 等高速外设的 DMA 传输串口之外I2C、SPI 在高速通信中同样离不开 DMA。SPI DMASPI 是主从同步通信主设备提供时钟同时收发数据。使用 DMA 可以显著减少 CPU 干预。常见场景是 SPI Flash 读写、LCD 屏幕刷新。大屏刷屏时一帧图片数据量可到几百 KB用传统方式逐字节发送会卡到怀疑人生DMA 发送则非常轻松。SPI DMA 要注意的细节一是 SPI 的收发通常是并行的即读操作也需要发送数据来产生时钟。DMA 读的时候需要同时配置发送 DMA 和接收 DMA发送端发送一个无意义字节比如 0x00 或 0xFF来产生时钟接收端接收真正的数据。二是 SPI 的 NSS 片选控制通常还是需要 CPU 参与DMA 传输前后要手动拉低拉高片选。I2C DMAI2C 传输有地址阶段、控制阶段、数据阶段DMA 通常只负责数据阶段地址和控制部分仍需 CPU 配置。因此 I2C DMA 的逻辑比 SPI DMA 复杂。实际使用中I2C 速率最高也就几 Mbps除非数据量很大否则 DMA 的价值相对没那么显著。但如果你的 I2C 挂了多个传感器需要连续读取大量数据DMA 还是能省不少 CPU 时间。UFS DMAUFSUniversal Flash Storage是手机等领域常用的存储标准它内部自带 DMA 引擎用于在存储芯片内部搬运数据。这和我们在 MCU 上说的 DMA 略有区别但核心思想一致减少 CPU 介入提高数据传输效率。UFS 的多队列、命令处理等机制其实也是围绕 DMA 传输来设计的。4.5 AXI 总线下的 DMA 传输高性能处理的必经之路在更高性能的嵌入式处理器比如 Xilinx Zynq、RK3588、树莓派 CM4 这类 ARM FPGA 或 ARM SoC 平台上DMA 通常会挂载在 AXI 总线上比如 AXI DMA、AXI VDMA。AXI DMA 与 MCU 上的 DMA 核心区别在于AXI DMA 支持更大的数据宽度64 位甚至 128 位、更高的传输带宽并且通常支持 Scatter-Gather 模式。FPGA 逻辑可以直接通过 AXI DMA 与 ARM 核共享大块内存数据的流向可以是 FPGA 内部逻辑 → AXI DMA → DDR或者反过来。在使用这类平台时常会遇到类似“RK3588 以太网报 failed to reset the DMA”这样的错误。这种问题往往不是 DMA 本身损坏而是驱动初始化顺序、时钟未开启或 DMA 控制器处于忙状态未被复位导致。排查思路一般是确认 DMA 控制器的时钟和复位信号是否正常、驱动是否与硬件版本匹配、DMA 描述符是否存在异常。注意不要上来就怀疑硬件坏了先看日志里的上下文。另外一个关键差异MCU 上的 DMA 一般由外设直接触发而 SoC 上的 DMA 往往是通过描述符驱动的软件准备好描述符、写 DMA 控制器的描述符地址、然后使能 DMA硬件自动从内存中加载描述符并执行因此描述符的地址对齐要求、内存分配方式一致/非一致非常重要。分配不一致内存时注意 Cache 一致性问题否则 DMA 读到的数据可能是缓存里的旧数据。4.6 分布式 DMA 与多核场景“分布式 DMA”这个词在工业和网络领域也经常出现通常指的是多个 DMA 引擎分布在不同的总线段或不同的处理器核心附近各自负责本地的数据传输降低跨总线传输的竞争。在 AMP非对称多处理或者 SMP对称多处理架构下每个核可能有自己的 DMA 资源。分布式 DMA 的好处是多个 DMA 引擎可以并行搬运不同方向的数据不会都挤在同一条总线上。比如一个核的串口 DMA 不影响另一个核的 SD 卡 DMA数据传输的并发度更高。对我们开发者来说分布式 DMA 意味着在用多核芯片时需要特别留意 DMA 请求归属和中断路由的配置确认是哪个核负责处理这个 DMA 的中断和错误。5. 常见报错与问题排查我在实际项目中踩过的坑这一节把我在各类 DMA 调试中遇到的典型问题汇总一下这些问题在论坛上也经常被讨论做个系统性总结。5.1 问题速查表我把常见的 DMA 故障按“现象-可能原因-排查方法”整理成表格方便大家快速定位。现象可能原因排查方法DMA 传输不启动外设 DMA 请求未使能检查外设寄存器是否开启对应 DMA 请求DMA 通道未正确映射到外设对照芯片参考手册检查 DMA 通道映射表内存地址未对齐按数据宽度对齐缓冲区地址数据全部相同地址递增未配置检查内存/外设地址递增开关数据宽度配置错误检查源和目的的数据宽度数据错位/紊乱ADC 通道顺序和缓冲区索引不匹配检查 ADC 扫描顺序与 DMA 缓冲区对应关系DMA 优先级配置不当导致数据丢失提高该通道优先级接收一段时间后停止DMA 错误中断未处理使能错误中断并打印状态寄存器外设错误如帧错误、噪声错误处理表达错误标志并重新启动 DMA指定长度接收不触发回调传输计数器设置错误检查传给 DMA 的长度值是否正确DMA 中断未使能检查 NVIC 和 DMA 中断配置串口发送乱码DMA 缓冲区被改写等待上一次发送完成再复用缓冲区未等待 TC 标志使用事件标志或完成回调DMA 总线错误地址访问无效区域检查地址是否在合法内存范围内Cache 一致性问题使用 Cache 管理函数对缓冲区执行 Clean/Invalidate系统性能反而下降DMA 优先级设置过高抢占 CPU合理降低优先级或使用突发传输5.2 易错点与排查技巧这里分享几个经验性的排查技巧它们在实际调试中帮过我大忙。技巧一先排查外设侧再排查 DMA 侧。我见过太多人在 DMA 配置上反复折腾最后发现是外设的中断标志没清除。DMA 请求到不了 DMA 控制器配置再好也没用。调试 DMA 问题时先用调试器读外设状态寄存器确认外设确实产生了 DMA 请求。技巧二打印 DMA 状态寄存器。大部分芯片的 DMA 控制器都有状态寄存器里面包含传输完成标志、错误标志等。调试时把状态寄存器打印出来看能快速判断 DMA 到底卡在哪个环节。HAL 库可以通过__HAL_DMA_GET_FLAG读取这些标志。技巧三先关闭 DMA 中断用轮询方式验证基本传输功能。很多 DMA 传输失败的根因是中断处理问题而不是 DMA 本身。你可以暂时关闭中断用轮询标志位的方式去验证数据是否搬运正确。数据对了再开中断这样就把问题排查范围缩小了一半。技巧四用固定的简单数据测试。调试 DMA 发送时可以先发送一个简单的数组比如 1、2、3、4...255然后查看接收端的数据。如果数据完全一致说明传输链路没问题如果有规律性错乱大概率是地址递增或数据宽度的问题。技巧五配置 DMA 接收时记得启用传输完成中断和错误中断。开发阶段这两个中断都很重要尤其是在调试初期。上线后再根据功耗需求调整。5.3 Cache 一致性问题在带有 Cache 的高性能处理器上使用 DMA一个绕不开的问题是 Cache 一致性。DMA 直接读写内存而 CPU 读写内存时可能经过 Cache 缓存。两者看到的数据可能不一致导致 DMA 读到的数据是过期的或者 CPU 读到的 DMA 数据是缓存里的旧内容。解决方法很简单在 DMA 写内存之前CPU 需要对缓冲区做 Cache Clean确保缓冲区数据回写到物理内存。在 DMA 写完之后、CPU 读之前需要做 Cache Invalidate使 CPU 缓存失效强制从内存重新读取。在某些 SoC 上你还可以在设备树或驱动里将 DMA 缓冲区标记为一致性内存coherent memory驱动框架会自动帮你处理。这里有一个实际的经历我在一个 Zynq 平台上调试以太网 DMA 时发现网络包内容偶尔是错的排查了半天最后发现是 DMA 接收缓冲区的 Cache 没有做 Invalidate 操作。加上之后问题就消失了。这个坑希望大家少踩。6. 工具选型与配置流程从标准化配置到快速验证DMA 的配置方式不同平台差别很大。在 MCU 上直接寄存器操作不够直观最常用的是 HAL 库或者 CubeMX 图形化配置在 SoC 和 Linux 环境下则是通过 DTS设备树配置在驱动的 DMA 引擎框架中。很多朋友问我DMA 配置是用 CubeMX 自动生成好还是手写代码好我的观点是开发阶段建议用 CubeMX 生成基础配置这样外设请求映射、时钟、中断这些容易搞错的地方不会出错。但是真正做产品时你可能需要手调一些细节比如优先级、缓冲区地址对齐、错误处理逻辑这些是 CubeMX 生成代码无法完全覆盖的。6.1 CubeMX 配置 DMA 的标准步骤以 STM32F407 ADC 多通道 DMA 为例标准配置步骤在 Analog 分类下打开 ADC1开启 Scan Conversion Mode选择通道数。在 ADC1 的 DMA Settings 里点击 Add选择 ADC1 作为 DMA 请求源。DMA 模式选 Circular数据宽度选 Half Word12 位 ADC 结果在 16 位寄存器中。内存地址递增开启外设地址不变。在 NVIC 中使能 DMA 中断。生成代码后在 main.c 中启动 ADC DMA 采集HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buffer, 4);这里有一点要特别提醒HAL_ADC_Start_DMA的第一个参数是 ADC 句柄第二个参数是存放结果的数组地址第三个参数是传输个数不是字节数。设置成 4 意味着每次扫描完成 4 个通道后产生一次 DMA 完成中断。如果你的缓冲区是 uint16_t 类型的数组它会在每次转换完成后把当前 4 个通道的数据都存进数组。6.2 UART DMA 在 CubeMX 中的配置要点串口 DMA 在 CubeMX 中的配置也不复杂重点在于区分发送和接收的 DMA 通道。在 USART 的 DMA Settings 中点击 Add选择 USART_TX 和 USART_RX 分别添加。TX 的 DMA 模式选择 NormalRX 的 DMA 模式建议选择 Circular如果做不定长接收的话。在 NVIC 中使能 USART 全局中断和 DMA 中断。生成代码后主程序里先调用HAL_UART_Receive_DMA(huart1, rx_buffer, buffer_size)开始接收。如果你做的是“DMA 串口发送需要等待上一轮数据发送完吗”这类问题的验证可以在代码中加一个简单的状态标志在HAL_UART_TxCpltCallback中置位发送前检查该标志或者直接用一个互斥量来防重入。6.3 Linux 设备树中的 DMA 配置在 Linux 环境下DMA 配置一般通过设备树描述。以 Zynq 平台的 AXI DMA 为例一个简单的设备树节点通常包含下面这些信息axi_dma_0: dma40400000 { compatible xlnx,axi-dma-1.00.a; reg 0x40400000 0x10000; dma-channels 1; #dma-cells 1; dma-channel40400000 { interrupts 0 29 4; xlnx,datawidth 0x40; }; };驱动通过标准的 DMA engine API 来请求 DMA 通道、提交传输描述符、等待传输完成。在这个框架里我们一般不需要关心底层 DTS 的具体格式只要保证设备树能正确描述 DMA 控制器的寄存器地址、中断号、数据宽度即可。如果你在调试以太网驱动时看到类似 “failed to reset the DMA” 的报错优先怀疑设备树或驱动中的 DMA 通道资源配置问题尤其是中断号和寄存器地址是否正确。7. 补充热词引申的扩展场景前面大量篇幅都在讲 MCU 场景但 DMA 的应用远不止这些。结合一些搜索热词我再补充几个大家容易关心的方向。7.1 无 DMA 时的替代方案与取舍如果你的芯片没有 DMA或者 DMA 资源被其他外设占用完了也不是完全没有办法。最简单的替代方案是“逐字节轮询中断”直白但低效。如果数据量不大、速率不高这种方式完全够用。比如串口波特率 9600每秒不到 1KB 数据中断接收毫无压力。但如果是高速 ADC 连续采样那就必须想办法腾出 DMA 资源。还有一种方案是用定时器中断配合总线读取。比如定时器每 10 微秒触发一次中断在中断里读取 ADC 转换结果。这种方式能实现和 DMA 相似的功能但 CPU 开销明显更大。选型阶段就考虑 DMA 资源比较稳妥。很多 32 位 MCU 的 DMA 通道数是有限的比如 8 或 16 条通道如果项目里串口、ADC、SPI、定时器都要用 DMA就要先规划好通道分配避免做到最后发现 DMA 不够用。7.2 双核与多核平台上的 DMA 分配在多核平台上DMA 资源的分配要遵循一个原则数据产生和消费尽可能在同一核附近避免跨核跨总线传输。比如你用双核芯片一个核负责采集传感器数据并使用 AI 算法计算那就把 ADC DMA 和相关数据处理都安排在这个核上另一个核负责通信和外设交互用另一个 DMA 通道。这样能最大限度地发挥多核并行优势。7.3 从 DMA 到 DMA Proxy在分布式系统里还有 DMA Proxy 这个概念。通常指的是一个模块或服务代理背板/总线上其他设备的 DMA 传输请求。它的存在意义在于不是所有设备都能访问系统主内存有些协处理器或 FPGA 的地址空间受限需要通过一个 DMA Proxy 来代表这些设备完成内存访问。这对我们普通应用开发者来说相对少见但在工控、通信设备、定制化硬件系统中可能会遇到。理解思路即可DMA Proxy 是连接受限设备和系统内存之间的桥梁。7.4 从 DMA 到数据流处理的应用本质上DMA 是一种典型的“以空间换时间”的架构。它牺牲了 DMA 控制器的硬件资源换来了 CPU 的大量时间。这个思想应用在实时操作系统、音视频采集、高速数据采集等场景都能带来立竿见影的效果。举个例子一个语音采集系统麦克风通过 I2S 接口接入 MCU。I2S 把音频数据连续不断地送进来如果用中断方式接收每个采样点都要打断 CPU 一次很可能导致音频数据卡顿。配合 DMA 加上双缓冲乒乓模式一个缓冲区在被 DMA 填充的同时另一个缓冲区的音频数据可以被音频算法处理两者完全并行系统负载和实时性都得到保障。这类“DMA 乒乓缓冲 处理流水线”的架构在音频、视频、通信基带等领域非常通用我个人强烈建议做嵌入式的朋友掌握这个套路。8. 写在最后DMA 调试的第一个动作与一次排查实录最后分享一个我最近实际调试 DMA 的案例希望能帮你少走弯路。有一次我在调试一块板子串口 DMA 接收数据时上位机发 100 字节有时候能收到 90 个有时候收到 100 个有时候收到 102 个完全没有规律。一开始我怀疑是 DMA 配置有问题反复检查优先级、地址、宽度都没发现异常。后来我打开了串口错误中断的状态标志发现 UART 的状态寄存器里置了 OROverrun Error标志。这意味着串口接收寄存器在 DMA 还没来得及把数据搬走时又收到了新数据发生了溢出。根本原因不是 DMA 配置而是 DMA 请求优先级不够高在高负载情况下没能及时响应串口请求或者串口接收中断和 DMA 之间产生了竞争。我把 DMA 优先级从 Low 提到 High并且确认串口接收中断优先级低于 DMA 的传输完成中断后数据就稳定了。事后回头看如果一开始就检查状态寄存器和错误标志这个问题的定位时间能缩短一半以上。我个人的习惯是DMA 调试出现异常第一个动作永远是读状态寄存器而不是改代码。状态寄存器最能直接反映硬件层面的问题先看硬件再看软件定位效率会高很多。DMA 是个很基础但非常重要的外设值得你在实践中反复接触。踩过几个坑之后你会发现它是一个非常可靠的工具只要配置正确、缓冲区管理合理性能提升非常可观。希望这篇文章能帮你在 DMA 的路上少走弯路。