ARTICLE DETAIL

资讯详情

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

DMA工作原理与实战:从数据搬运到串口接收、ADC采样

DMA工作原理与实战:从数据搬运到串口接收、ADC采样 搞嵌入式这么多年DMA 这个话题我写过代码、调过 bug、也被它坑过无数次。很多朋友一提到 DMA 就觉得是个“高级功能”总觉得配置复杂、时序难懂、出了问题也不知道从哪里查。实际上 DMA 的本质特别朴素它是一个不需要 CPU 参与的数据搬运工。你告诉它“从哪搬、搬到哪、搬多少”它自己就能把活干了干完再喊你一声。就这么简单。这篇文章我想把 DMA 从工作流程、传输模式到典型应用场景完整串一遍。里面会穿插大量我在 STM32、PY32、串口不定长接收、ADC 连续采样这些实战中踩过的坑和验证过的套路。不管你是在调一个串口接收卡住的问题还是在纠结 DMA 发送要不要等上一轮完成或者想搞清楚 UFS DMA、分布式 DMA 这些偏门概念到底是怎么回事这篇文章都能给你一个相对完整的答案。1. DMA 是什么先从一次“手动搬砖”说起1.1 没有 DMA 的时候CPU 有多累想象一个场景串口每收到一个字节CPU 就要停下来去读数据寄存器然后把数据写到内存数组里。波特率不高的时候还好如果是 921600 的波特率大约每 10 微秒就有一个字节进来CPU 大部分时间都在“读寄存器、写内存、读寄存器、写内存”这个循环里。更麻烦的是如果这段代码优先级不够高来得太快的数据还可能被漏掉直接导致丢帧。中断方式稍微好一点本质上也是每来一个字节打断一次 CPU。数据量小看不出问题可一旦数据量上来比如一秒钟几万个字节的传感器数据流中断就会像连续敲门一样CPU 根本没法干正事。轮询更不用说了纯属浪费。1.2 DMA 就是那个“专职快递员”DMA 的全称是 Direct Memory Access中文叫直接存储器访问。它的核心价值就是让数据在外设和内存之间直接搬运整个过程不需要 CPU 逐字节干预。你可以把它理解成一个专职快递员CPU 只需要把发货地址、收货地址、货品数量告诉它然后就回去忙自己的事情了。快递员搬完货按一下门铃告诉 CPU“货搬完了”整个过程就结束了。这个“按门铃”的动作在硬件上就是 DMA 传输完成中断。CPU 收到中断后只需要处理搬运完成后的数据就行。比如串口接收DMA 把一整包数据搬进内存后CPU 再一次性处理效率完全不一样。1.3 DMA 控制器本身也是一个“小处理器”很多人忽略了一个关键点DMA 控制器本身就是一个独立的小处理器它有自己的寄存器、自己的状态机甚至有的芯片 DMA 控制器还不止一个通道。比如 STM32F1 有两个 DMA 控制器DMA1 和 DMA2每个控制器下有多个通道不同通道可以映射到不同的外设请求。这意味着 DMA 的搬运能力和 CPU 是并行的CPU 在跑主逻辑DMA 在搬数据两者互不干扰。这也是为什么 UFS DMA、SDIO DMA 这些高速存储场景里离不开 DMA。UFS 的理论带宽能到几千 MB/s靠 CPU 逐字节搬运根本不可能必须用专门的 DMA 引擎把数据从闪存控制器搬到内存再交给 CPU 做高层处理。2. DMA 工作流程拆解一次完整搬运是怎么发生的2.1 请求、响应、传输、结束四个动作一台戏一次 DMA 传输从硬件角度看大概经历这么几个阶段外设请求外设比如串口、ADC、定时器准备好数据后会拉高一根请求线告诉 DMA 控制器“我有数据要搬”。DMA 响应DMA 控制器收到请求后如果通道空闲就会接管总线向总线仲裁器发起访问请求。这个总线仲裁很重要因为 CPU 也要用总线如果两者同时访问内存就得有优先级仲裁机制。数据传输DMA 从源地址读取数据写到目的地址。对于串口接收源地址是串口的数据寄存器目的地址是内存数组。对于内存到内存拷贝源和目的都是内存地址。传输结束当传输计数器减到 0DMA 会置一个传输完成标志位如果使能了中断就会向 CPU 发出中断请求。如果配置的是循环模式计数器会重新加载继续下一轮搬运。2.2 关键参数不是随便填的配置 DMA 的时候通常会碰见这些参数源地址和目的地址要注意地址是固定的还是递增的。比如串口接收源地址串口 DR 寄存器是固定的目的地址是递增的数据要从寄存器依次写入内存数组。比如内存到内存拷贝源和目的都需要递增。传输宽度字节、半字、字。串口数据寄存器一般是 8 位ADC 转换结果如果是 12 位一般用半字16 位来搬运。这个一定要匹配好否则数据会错位。传输计数一次 DMA 传输多少个数据单元。串口接收里就是期望接收多少个字节ADC 连续采样里就是采样次数。突发长度这是很多新手困惑的地方。突发模式Burst指 DMA 一次申请总线后连续搬运多个数据而不是搬一个就释放总线。突发长度越大总线占用时间越长但搬运效率越高。不过要注意外设那边的 FIFO 深度得配合得上否则数据会溢出。2.3 CPU 在传输过程中做了什么很多人以为 DMA 传输过程中 CPU 完全不管这也是个误解。CPU 在 DMA 传输期间还是能执行指令的只是如果 CPU 恰好也要访问总线就得和 DMA 竞争。对于大多数场景这种竞争影响不大但在一些实时性要求很高的任务里DMA 的突发传输可能会导致 CPU 的某次内存访问被延迟进而影响实时响应。这就是为什么我在很多实时控制项目里DMA 传输的数据量会合理控制不会一上来就配置 64KB 的大块搬运。经验值是多用几个小块的 DMA 搬运配合中断逐块处理对实时性的影响会小很多。2.4 中断回调里到底能干什么DMA 传输完成中断触发后你可以在回调里处理数据但要注意几个禁忌。首先不要在里面做耗时的操作比如 printf、延时、大数据处理这些都放到主循环里做。其次如果需要重新启动下一轮 DMA 接收建议先看一下 DMA 的状态寄存器确保上一轮确实结束了再改配置。我见过很多人在 DMA 传输完成中断里直接调用 HAL_UART_Receive_DMA 来开启下一轮接收然后发现数据错乱。原因很简单第一次接收还没完全停止你又开启了新的接收两个操作相互干扰了。正确的做法是先调用 HAL_UART_DMAStop 停掉当前的 DMA再重新开启。3. 传输模式怎么选单次、突发、循环与连续请求3.1 单次传输 vs 突发传输DMA 的单次传输模式CPU 每次响应外设请求只搬运一个数据单元。这种方式响应快、总线占用时间短但是效率低因为每次搬运都需要重新申请总线、仲裁、释放。突发传输则是一次响应连续搬运多个数据。比如配置突发长度为 4一次响应就搬 4 个数据单元。这种方式效率高适合大数据块搬运比如从 UFS 读一段数据到内存、从 ADC 连续采样缓存到内存。选择哪种模式主要看数据量和实时性要求。数据量小、实时性要求高的用单次数据量大、实时性要求相对宽松的用突发。3.2 循环模式与 DMA continuous requests循环模式在 ADC 连续采样、串口不定长接收这些场景里非常常用。配置成循环模式后DMA 搬完一轮数据计数器自动重载继续下一轮搬运整个过程不需要 CPU 干预。DMA continuous requests这个概念主要和循环模式有关。它指的是 DMA 在传输完成后是否继续响应外设的请求。如果开启了 continuous requestsDMA 会在完成一轮后立刻准备接收下一轮外设请求相当于保持“随时待命”的状态。如果不开DMA 完成一轮后就停了必须软件重新触发才能开启下一轮。以 STM32 的 HAL 库为例HAL_ADC_Start_DMA 开启后如果配置了循环模式DMA 会一直在后台搬运 ADC 转换结果。你只需要在传输完成中断里读数据甚至可以直接从内存数组里读最新的数据彻底实现“后台采集、前台使用”。3.3 直接模式 vs FIFO 模式直接模式就是 DMA 收到一个数据就搬运一个数据没有中间缓存。FIFO 模式下DMA 先把数据存到内部的 FIFO 里攒够了再一次性搬运出去。FIFO 模式的好处是能减少总线访问次数还能在不同数据宽度之间转换。比如源数据是 8 位的目的数据是 32 位的通过 FIFO 做数据宽度转换就非常方便。缺点是会引入延迟不适合对实时性要求特别高的场合。实际项目中串口 DMA 接收我一般用直接模式因为串口数据是一字节一字节来的FIFO 反而容易造成接收延迟。而 ADC 连续采样如果采样率很高、数据量很大用 FIFO 模式可以显著降低总线占用率。4. 串口 DMA 实战不定长接收与发送等待4.1 不定长接收空闲中断才是主角串口 DMA 接收不定长数据是很多人的入门必修课也是最容易出问题的地方。核心配置就一句话DMA 负责把数据搬进缓冲区串口的空闲中断负责告诉你“一帧数据收完了”。所谓空闲中断就是串口在一段时间内没有收到新数据时会触发的中断。配合 DMA 循环接收可以做到DMA 一直在后台收数据收到哪算哪一旦串口空闲了就进入空闲中断通过比较 DMA 当前计数器和初始计数器的差值就知道这一帧收了多长。具体配置上用 STM32 HAL 库的话开启串口 DMA 接收之后再使能串口的 IDLE 中断。在中断回调里先读取串口的 SR 寄存器确认是空闲中断然后读取 DMA 剩余的未传输个数通过计算得出本次接收到的数据长度。4.2 DMA 发送到底要不要等上一轮完成这个问题我专门拿出来说因为太典型了。DMA 发送和 DMA 接收不一样接收是外设往内存搬数据发送是内存往外设搬数据。发送的时候数据从内存搬到串口的发送数据寄存器再由串口硬件逐字节发出去。两者之间是有一个“搬运完成”和“实际发送完成”的差异的。DMA 搬运完成不代表串口已经把数据发出去了。这是最大的坑。如果你在 DMA 发送完成中断里立刻修改发送缓冲区的内容或者关闭 DMA很可能导致数据发了一半被截断。因为 DMA 只是把数据搬到了串口的发送移位寄存器里移位寄存器里的数据还没完全发出去。我验证过的可靠做法是在 DMA 发送完成中断里再检查一下串口的 TCTransmission Complete标志位。等 TC 置位了才表示这一帧数据真正发送完毕。如果 DMA 发送完成后你还要发下一帧务必等 TC 清掉后再开始下一轮发送否则可能出现帧与帧之间粘连。4.3 PY32F003 串口 DMA 接收的一个实际例子我也在 PY32F003 这类小资源单片机上做过串口 DMA 接收。PY32F003 的 DMA 资源比较紧张只有一个 DMA 控制器通道数量也有限所以配置的时候要精打细算。我的做法是开启串口 DMA 循环接收缓冲区大小设成 256 字节。DMA 把收到的数据循环写入缓冲区同时使能串口空闲中断。每收到一帧完整数据以空闲中断为标志我就计算数据长度然后做协议解析。一个需要注意的细节是在 PY32 上启用 DMA 时钟的时机很重要。如果初始化顺序不对比如先配置 DMA 通道再开启外设时钟会导致 DMA 配置丢失表现就是数据永远进不了缓冲区。我的习惯是先开启 DMA 和串口的时钟再初始化 DMA 通道最后初始化串口。5. DMA 的更多典型应用场景5.1 ADC 多通道连续采样让 DMA 帮你攒数据STM32 HAL 库的 ADC 单通道 DMA 多次采样是我用得最多的场景之一。比如需要采集一个模拟传感器的波形采样率 10kHz采 1000 个点。如果没有 DMA你得在定时器中断里一个个读 ADC 转换结果有了 DMA 后初始化一次硬件就会自动把 1000 个采样值搬进内存数组搬完了再通知你处理。关键参数有这几个ADC 扫描模式、连续转换模式、DMA 循环模式。多通道采样时还要注意ADC 的通道顺序要和 DMA 缓冲区里的数据顺序对应上。我踩过的一个坑是配置了多通道扫描结果 DMA 缓冲区里数据顺序和通道配置顺序不一致分析数据时老是错位。后来我养成了一个习惯在初始化的时候把通道顺序映射表写清楚调试时先在缓冲区里打印前几个值验证顺序没问题再继续。5.2 UFS DMA高速存储背后的搬运引擎UFS 是高端手机和 SSD 里常用的闪存接口标准它的数据吞吐量非常高。UFS 控制器内部就集成了 DMA 引擎负责把闪存里读出的数据搬到系统内存或者把内存里的数据写到闪存。UFS DMA 的工作方式和 MCU 里的 DMA 本质一样都是把源地址的数据搬到目的地址但是它的传输宽度更大、突发长度更长、通道数更多。而且 UFS DMA 往往需要支持多队列也就是同时有多块数据要搬运DMA 引擎需要按优先级依次处理。很多人只关注 UFS 的协议层忽略了 UFS DMA 的性能调优。比如 DMA 的 burst 长度设置直接决定了 UFS 的实测读写带宽。如果 burst 长度设得太小DMA 频繁申请总线带宽上不去设得太大FIFO 又可能不够用。这个需要根据具体的控制器和内存带宽去调。5.3 DMA 测速软件怎么看带宽DMA 测速软件的原理其实就是让 DMA 持续搬运大数据块然后统计单位时间内搬运了多少数据算出实际带宽。这类工具在评估存储设备性能、验证 DMA 配置是否最优时很有用。比如用 PC 上的磁盘测速软件本质上也是在触发存储控制器的 DMA 引擎搬运数据通过测量搬运时间来计算速率。嵌入式上类似的做法是配置 DMA 循环搬运一块已知大小的缓冲区同时开启一个定时器搬运一段时间后统计搬运次数就能算出实际带宽。一个人容易忽略的问题是DMA 的理论带宽和实际带宽之间是有差距的。因为总线仲裁、FIFO 等待、外设响应延迟都会消耗时间。所以测出来的实际带宽如果只有理论带宽的 60%70%不一定是配置有问题可能只是总线上其他设备也在占用带宽。这时候可以通过调整 burst 长度、通道优先级来优化。5.4 分布式 DMA把搬运扩展到多节点分布式 DMA 是我早期接触服务器开发时才遇到的。普通 DMA 只能在一个处理器系统内部搬运数据而分布式 DMA 允许跨节点进行数据搬运每个节点都有本地的 DMA 引擎通过高速网络比如 RDMA 网络或者 NVMe over Fabric协同工作。简单理解普通 DMA 是“同一间屋子里的快递员”分布式 DMA 是“跨城市快递网络”每个城市有自己的快递分拣中心。这种架构的好处是大大减少了远程数据访问的延迟因为不需要把数据从远端搬到本地再处理而是直接在远端处理完再把结果传回来。分布式 DMA 在分布式存储、AI 训练集群里应用很广。比如在 GPU 集群里每个 GPU 节点通过 RDMA 访问远端内存实际上底层就依赖分布式 DMA 的搬运能力。理解普通 DMA 的原理后再去看分布式 DMA 就不难了核心思想都是“减少 CPU 参与、让数据自己流动”。6. 常见问题与排查技巧实录6.1 疑难杂症速查表下面这份速查表是我这些年调 DMA 遇到过的典型问题整理成表格方便大家快速定位。症状可能原因排查方向数据错位、顺序不对数据宽度配置错误或多通道顺序映射不一致核对源、目的地址的数据宽度检查多通道配置顺序只收到第一帧后续收不到DMA 传输完成中断里没有重新开启接收在完成中断里先停 DMA再开启新一轮接收数据能收但丢最后一个字节没有等待串口 TC 标志位DMA 发送完成后检查 TC 状态乱码、偶尔多几个字节波特率误差大或 FIFO 配置不合理检查时钟配置改用直接模式测试ADC 值全都是同一个数DMA 搬运方向和 ADC 数据寄存器地址配置错误检查 DMA 通道映射和外设地址是否可以访问DMA 搬运超时或卡死总线死锁或 DMA 通道被其他外设占用检查 DMA 通道映射表确认没有冲突低功耗唤醒后 DMA 不工作DMA 时钟被关闭后未重新初始化在低功耗恢复流程里重新配置 DMA6.2 几个值得记住的经验第一件DMA 调试时先把中断关了用标志位轮询来验证配置。如果轮询模式下数据搬运都正常再开中断如果轮询模式就不对别浪费时间去查中断逻辑肯定是 DMA 本身的配置问题。第二件DMA 缓冲区尽量定义成全局变量或者静态变量。很多人把 DMA 缓冲区定义成局部变量结果数据直接错乱。因为局部变量在栈上地址可能随着函数调用变化DMA 控制器访问的是固定地址一旦栈被回收数据就写到别的地方去了。第三件用逻辑分析仪或者串口打印关键节点的标志位。在我不知道数据是在哪一步丢的时候我喜欢在传输开始前、DMA 完成中断里、主循环处理时分别打印标志位通过观察标志位出现的顺序能快速定位问题在哪一层。第四件DMA 完成中断里不要做的事。不要在中断里做耗时操作不要调用 printf不要在中断里等待另一个中断或外设事件。这些都是血的教训即使强如 DMA也扛不住 CPU 在中断里磨蹭太久。第五件配置 DMA 前先把所有相关外设时钟打开。很多“DMA 不工作”的问题排查到最后发现是 DMA 控制器本身的时钟没使能。特别是在某些低功耗芯片上DMA 时钟默认是关闭的忘了开就直接没反应。7. 项目源码里那些值得抄的小技巧7.1 环形缓冲区与 DMA 的结合串口 DMA 不定长接收时我建议把 DMA 循环接收和环形缓冲区结合起来。DMA 直接写入一个固定大小的数组然后通过维护读索引和写索引实现数据流的连续处理。具体做法是DMA 循环接收时每触发一次空闲中断就更新写索引主循环里处理数据时维护一个读索引。当读索引追上写索引时说明暂时没有新数据可以等待。这种方式比每收到一帧就拷贝一次缓冲区更省内存也适合高频数据流。要注意的是环形缓冲区的读写索引操作要做好临界保护。因为写索引是在中断里更新的而读索引在主循环里更新如果主循环正在读数据时中断又更新了写索引可能逻辑上会出差错。我的做法是在主循环里先关中断读取写索引再开中断然后再处理数据。7.2 DMA 完成回调里的状态机设计DMA 完成中断适合做什么我的答案是触发状态机转换而不是直接处理数据。比如一个数据采集系统DMA 搬完一批 ADC 数据后应该在中断里设置一个标志位通知主循环“数据已就绪可以处理了”。主循环检测到标志位后再把状态机从“等待数据”切换到“分析数据”。这种做法的好处是DMA 中断处理极快不会影响后续传输主循环有充足的时间处理复杂逻辑不会被中断挤占。更重要的是状态机的描述让代码逻辑非常清晰一个阶段一个阶段地推进不容易乱。7.3 用 DMA 实现内存拷贝DMA 不光能搬外设数据还能做内存到内存的拷贝。在需要频繁拷贝大数据块的场景下用 DMA 比用 memcpy 更合适。STM32 的 DMA 里内存到内存模式需要手动触发一次传输而不是由外设触发。配置成内存到内存模式后DMA 从源地址读取数据写到目的地址。我实测过对于 1KB 以上的数据块DMA 拷贝的效率比 CPU 逐字节拷贝高不少因为 CPU 可以在这段时间里做其他事情。不过要注意内存到内存拷贝时 CPU 还是要参与发起的所以它只能和 CPU 的执行并行不能完全替代 CPU 的工作。7.4 优先级与仲裁怎么配DMA 通道的优先级配置直接决定了多个 DMA 请求同时到来时的处理顺序。实际项目中我一般把实时性要求最高的外设设置成最高优先级。比如一个系统里既有串口接收、又有 ADC 采样还有定时器触发的 DMA 搬运那么优先保证串口接收不丢数据因为 ADC 采样数据丢几个点还能靠算法补偿串口丢一个字节可能整帧就废了。优先级也不是越高越好。如果所有通道都是最高优先级仲裁就失去了意义。建议合理分配关键通道用高优先级次要通道用低优先级。另外还要注意 DMA 控制器本身的总线优先级。在 STM32 上DMA1 和 DMA2 共用总线如果 DMA2 的数据量很大会挤占 DMA1 的总线访问时间导致 DMA1 的外设数据来不及搬运。7.5 数据宽度不匹配时的处理有时候 DMA 的源数据宽度和目的数据宽度不一样。比如 ADC 转换结果是 16 位的但你要把数据发送到某个 8 位的外设接口上。这种情况下要么在软件里做数据转换要么利用 DMA 的 FIFO 功能。STM32 的 DMA 在开启 FIFO 模式后可以把数据宽度从 16 位转换成 8 位DMA 会自动处理数据拆分。但要注意FIFO 模式下会有额外的延迟而且必须配置正确的 FIFO 阈值。如果发现转换后的数据顺序不对重点检查 FIFO 的阈值设置和数据宽度匹配关系。7.6 低功耗场景下的 DMA 处理策略低功耗嵌入式设备里DMA 是个双刃剑。一方面它能在 CPU 睡着的状态下搬运数据比如让 MCU 进入睡眠模式DMA 继续通过串口接收数据等收到一定量数据后靠 DMA 中断唤醒 CPU。另一方面一旦低功耗模式把 DMA 时钟关了就可能出现数据丢失。我的建议是需要在低功耗模式下保持通信的场景优先选择支持 DMA 唤醒的芯片或者使用外设自带的 FIFO 缓存数据。如果芯片不支持 DMA 在低功耗模式下工作那就要设计好唤醒策略比如先靠中断唤醒 CPU再开启 DMA 批量搬运数据而不是让 DMA 一直在低功耗下运行。8. 写在最后的几句心里话做嵌入式这些年我最大的感受是DMA 不难难的是理解数据流在整个系统里是怎么走的。DMA 的每个参数背后都对应着一次总线的申请、一次存储器的访问、一次外设的状态切换。只有把“数据从哪里来、到哪里去、中间经过谁”这条链路想清楚DMA 才能真正成为你的利器而不是一个让你加班到深夜的调试对象。我自己踩过最深的一个坑就是早期做串口 DMA 发送的时候想当然地以为 DMA 传输完成就等于数据发出去了结果在高速波特率下反复丢最后一两个字节。后来老老实实查参考手册才发现“搬运完成”和“发送完成”是两回事。从那之后我再也不敢凭直觉写 DMA 代码了每个细节都必须从芯片手册里找到依据。如果你现在正在为 DMA 的某个问题头疼我的建议是先关掉中断用轮询的方式验证 DMA 配置是否正常。然后逐条核对源地址、目的地址、数据宽度、传输计数这四个关键参数。最后再开中断确认中断回调里的操作足够轻量。这套方法虽然朴素但每一条都来自真实的调试经历照着做绝大多数问题都能自己定位到根因。
返回列表