DMA传输模式:单次、突发、循环与链表模式

这一节我们聊聊DMA的传输模式。说实话,很多工程师对DMA的理解停留在“能传数据”的层面,但不同模式的性能差异和适用场景,才是真正拉开差距的地方。我个人习惯把这四种模式看作一个成熟度进阶——从“能用”到“用得好”,中间差的就是对这些模式的理解。

核心认知:DMA传输模式决定了你的数据搬运效率、CPU干预频率和系统吞吐上限。选错了模式,总线带宽再高也白搭。

3.1 单次传输(Single Transfer)

最简单的模式。每次DMA请求只传输一个数据单元(一个字或一个字节),然后释放总线。下次传输需要重新触发。

我在项目中遇到过,某块低速传感器采集卡就是用单次模式。为什么?因为数据量小,传感器自己的采样率也低,单次传输足够用了。反而用突发模式的话,总线抢占太频繁,还会影响其他实时任务。

  • 特点:传输粒度小,总线占用时间极短
  • 中断频率:每传一个数据单元触发一次中断,中断延迟敏感
  • 适用场景:低频外设、控制类应用、数据量小的采集

警告:别在高速外设(如千兆网卡、高速ADC)上用单次模式。我曾经见过有人这么干——中断风暴直接导致系统响应超时,整个协议栈都崩了。

3.2 突发传输(Burst Transfer)

突发模式就是一次DMA请求,连续传输多个数据单元。突发长度(Burst Length)通常可配置,常见的有4、8、16、32等。

你想想看,为什么突发模式更高效?因为每次DMA请求只需要一次总线仲裁,后续数据都在同一个总线事务里完成。说白了,省掉了反复申请总线的开销。

突发长度 总线效率 中断次数 典型场景
4 中等 较高 SPI Flash读取
8 良好 中等 低速网络接口
16 高 较低 高速ADC、摄像头
32 极高 低 DDR内存搬运

这里有个坑:突发长度不是越大越好。总线会被长时间占用,其他主设备(比如CPU本身)就得等着。我之前在做视频采集卡时,把突发长度设到了64,结果CPU的Cache refill等着总线空闲,帧处理延迟直接翻倍。

经验之谈:突发长度的选取要结合总线带宽和系统实时性要求。我的建议是,从8开始试,逐步增加,监控总线的等待周期。

3.3 循环模式(Circular Mode / Auto-Reload)

循环模式,也叫自动重载模式。DMA传输完成后,地址和计数器自动复位到初始值,又开始下一次传输。这样就不用每次重新配置DMA。

我记得做音频采集时用到这个模式。音频数据流是连续的,ADC采样率固定,用循环模式配合乒乓缓冲,完美解决了数据丢帧问题。

  • 优点:无需CPU反复操作DMA寄存器,减少软件开销
  • 中断策略:通常在半传输和完全传输时各触发一次中断,用于交换缓冲
  • 痛点:如果中断处理不及时,DMA会覆盖未处理的数据——这就是所谓的“数据覆盖”问题

循环模式的一个关键参数是缓冲区大小。设太小,中断太频繁;设太大,响应延迟增加。我个人的习惯:在音频场景下,缓冲区设为1ms的数据量是个不错的起点。

3.4 链表模式(Linked List / Descriptor Mode)

这是DMA最灵活、也最有难度的模式。每个DMA传输任务由“描述符”(Descriptor)定义,多个描述符通过链表串起来。DMA控制器自动按链表顺序执行传输,完成后自动跳到下一个描述符。

讲真,链表模式在高端嵌入式芯片(比如TI的C6678、NXP的i.MX系列)里用得很多。它特别适合处理不规则的数据结构或复杂传输序列。

// 一个典型的DMA描述符结构(伪代码)
struct dma_descriptor {
    void    *src_addr;      // 源地址
    void    *dst_addr;      // 目的地址
    uint32_t length;        // 传输长度
    uint32_t flags;         // 控制标志(如中断使能)
    struct   dma_descriptor *next; // 指向下一个描述符
};

链表模式有几个核心优势:

  • 零CPU干预:配置好链表后,CPU可以去做别的,DMA自己跑完整条链路
  • 灵活传输:每个描述符可以有不同的源、目的和长度,甚至可以在不同外设之间切换
  • 链路中断管理:可以在关键描述符上设置中断,实现“一个中断完成多个传输”的效果

我曾经在网络协议处理中用到链表模式。数据包头部和数据部分存储在内存的不同位置,每个描述符指向一部分,DMA自动完成“组装+搬运”。这样CPU就不用做内存拷贝,吞吐率直接翻了一倍不止。

DMA四种传输模式对比 单次传输 1次请求 = 1个数据 中断频率:极高 总线压力:低 适合:低速/控制 突发传输 1次请求 = N个数据 中断频率:中等 总线压力:较高 适合:中高速外设 循环模式 自动重载 + 循环 中断频率:可控 数据流:连续 适合:音频/视频 链表模式 描述符链表驱动 中断频率:低 灵活性:极高 适合:网络/复杂任务 中断次数对比(传输等量数据) 单次:>> 突发:MMM 循环:MM 链表:M 吞吐率趋势(从左到右) 单次 → 突发 → 循环 → 链表,吞吐率递增,CPU干预递减

3.5 如何选择传输模式?

这个问题没有标准答案,但我可以通过几个维度来帮你做判断:

  1. 数据量:单次传输适合小数据(几个字节),突发传输适合中等数据块(几十到几百字节),链表模式适合大规模、不规则传输
  2. 实时性要求:如果中断延迟敏感(比如控制环路),尽可能降低中断频率——用突发或链表模式
  3. 硬件支持:不是所有芯片都支持链表模式。在资源受限的MCU上,循环模式可能是最好的选择
  4. 开发复杂度:链表模式配置起来最麻烦,需要有足够的时间和验证手段

我的建议:如果你刚开始接触DMA优化,先用突发模式调通基本功能。等对硬件行为有了手感,再挑战链表模式。别一口气想着最优解,先跑起来再说。

嗯,模式的选择是性能优化的第一步。实际工作中,我经常看到有人用单次模式跑高速外设,结果CPU被中断淹死。也有些场景,明明链表模式能解决,却因为怕麻烦硬扛着高中断频率。我觉得,理解这些模式的差异和代价,是成为DMA高手的必修课。

还有一个细节值得注意:无论是哪种模式,都需要关注数据对齐和描述符的内存布局。举个例子,如果描述符跨了Cache行边界,DMA控制器可能会多读一次内存,性能直接损耗。我踩过这个坑,改完描述符对齐后,吞吐率提升了将近15%。所以,配置DMA时别光盯着模式,底层的内存访问方式也很重要。

避坑指南:我曾经在一次项目中,把链表模式的描述符放在了DDR的Cacheable区域。每次DMA从描述符链表里取下一项时,Cache line刷回内存的操作搞乱了时序。解决办法很简单——把描述符放到Non-Cacheable内存区域,或者做好Cache刷新。这种问题很难复现,排查起来极其痛苦。