DMA 性能瓶颈分析:总线仲裁、内存对齐、Cache 与描述符链表

DMA 用起来很简单,对吧?配置好源地址、目标地址、长度,一启动就跑。但你真的把它跑满了吗?

我见过不少项目,DMA 传输速度远低于理论值,CPU 还被莫名其妙拖慢。问题出在哪?说白了,就是几个常见的性能瓶颈。这一章,咱们就把它们一个个揪出来。

总线仲裁延迟:谁在抢路权?

STM32 内部不止一个总线主控。CPU、DMA、以太网、USB 都要通过总线矩阵访问 SRAM、Flash 或外设。DMA 传输时,如果 CPU 也在频繁读写内存,总线仲裁器就得来回切换授权。

核心问题:每次总线切换都有固定开销(几个时钟周期)。高频切换下,DMA 的实际带宽会明显下降。

我在一个音频采集项目里遇到过:DMA 从 ADC 搬运数据到内存,CPU 同时在做 FFT 运算。结果 DMA 偶尔丢数据,查了半天才发现是总线冲突导致传输延迟,ADC 的 FIFO 溢出了。

如何缓解?

  • 提高 DMA 优先级:在 DMA 控制器里设置通道优先级高于 CPU。但这会影响 CPU 响应,要权衡。
  • 分散 CPU 访问:让 CPU 在 DMA 传输间隙访问内存。比如用双缓冲,DMA 写一块,CPU 读另一块。
  • 使用专用总线:有些 STM32 系列(如 H7)有 AXI 总线矩阵,DMA 和 CPU 可以走不同路径,减少冲突。

我的习惯:在 CubeMX 里配置 DMA 时,我会把高带宽、实时性要求高的通道(如 ADC、I2S)优先级设高,低带宽的(如 UART 发送)设低。别所有通道都抢最高优先级,反而容易出问题。

内存对齐问题:一个字还是四个字?

DMA 传输时,源地址和目标地址的对齐方式直接影响效率。ARM Cortex-M 内核访问 32 位数据时,如果地址是 4 字节对齐,一次就能读完。如果不对齐,硬件要拆成多次访问,性能直接打折扣。

对齐方式 DMA 传输效率 典型场景
4 字节对齐 最高,单次传输 32 位 内存到内存、ADC 到 SRAM
2 字节对齐 中等,需拆分为两次 16 位 部分外设 FIFO 地址
未对齐 最低,硬件自动拆分,可能触发异常 协议解析、数据拼接

我曾经踩过的坑:用 DMA 从 UART 接收不定长数据,把接收缓冲区定义在结构体里,结果结构体成员没做 __attribute__((aligned(4)))。DMA 传输时偶尔卡死,调试了整整两天才发现是对齐问题。从那以后,我所有 DMA 缓冲区都强制 4 字节对齐。

代码里怎么保证?很简单:

// 强制 4 字节对齐
uint8_t dma_rx_buffer[256] __attribute__((aligned(4)));

// 或者用联合体
typedef union {
    uint8_t  bytes[256];
    uint32_t words[64];
} dma_buffer_t;

Cache 一致性:你看到的数据是假的

如果你用 STM32 H7 或 MP1 系列,内部有 Cache。DMA 直接访问内存,而 CPU 可能通过 Cache 读写。这就出问题了:DMA 写入了新数据,但 CPU 读的是 Cache 里的旧数据。反过来,CPU 写了数据到 Cache,DMA 去内存读到的还是旧数据。

这就是 Cache 一致性问题。说白了,CPU 和 DMA 看到的内存内容不一样。

典型表现:DMA 传输完成后,CPU 读取缓冲区发现数据没变,或者数据错乱。重新读一次又对了。这种 bug 最难复现,也最难查。

解决方案

  • 使用非 Cache 区域:在 MPU 里把 DMA 缓冲区所在的地址区域配置为 Non-Cacheable。这样 CPU 每次都直接读内存,不会缓存。
  • 手动维护 Cache:DMA 传输前,CPU 调用 SCB_CleanDCache() 把脏数据写回内存。传输完成后,调用 SCB_InvalidateDCache() 让 Cache 失效,下次读取从内存重新加载。
// DMA 传输前:清理 Cache,确保内存数据最新
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, size);

// 启动 DMA 传输
HAL_DMA_Start_IT(...);

// DMA 传输完成回调:使 Cache 失效,读取最新数据
void DMA_Complete_Callback(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, size);
    // 现在可以安全读取 rx_buffer 了
}

我建议:如果项目里用了 Cache,优先把 DMA 缓冲区放在 DTCM 区域(如果芯片支持)。DTCM 默认不经过 Cache,天然一致。省心很多。

描述符链表的开销:灵活性的代价

STM32 的高端系列(如 H7、G4)支持 DMA 描述符链表模式。你可以把多个传输任务串成链表,DMA 自动执行完一个再执行下一个。听起来很完美,对吧?

但描述符链表不是免费的。每次链表节点跳转,DMA 控制器都要从内存读取下一个描述符。这个读取操作本身就要占用总线带宽。如果描述符在慢速内存(如外部 SDRAM),开销更大。

模式 灵活性 额外开销 适用场景
普通模式 低,一次配置一次传输 无 简单、固定长度的传输
循环模式 中,自动重复 低 ADC 连续采样、音频流
描述符链表 高,可动态改变传输链 每次跳转约 10-20 个时钟周期 复杂协议、多缓冲、乒乓操作

我曾经遇到的情况:在一个高速数据采集项目里,用描述符链表做乒乓缓冲。结果发现每次链表跳转时,DMA 会短暂停顿,导致采样率上不去。后来把描述符放到 SRAM1(速度更快),并且减少链表节点数量,才解决问题。

优化建议

  • 描述符放快速内存:尽量放在 SRAM1 或 DTCM,别放外部 SDRAM。
  • 减少链表长度:能用 2 个节点做乒乓,就别用 8 个。节点越多,跳转开销越大。
  • 预取描述符:有些 STM32 系列支持描述符预取,可以提前读取下一个节点,减少等待。
DMA 性能瓶颈全景图 总线仲裁延迟 多主控抢路权 内存对齐问题 非对齐拆分访问 Cache 一致性 CPU 与 DMA 数据不同步 描述符链表开销 节点跳转消耗带宽 DMA 实际带宽下降 / 传输延迟增加 / 数据错误 严重时导致 FIFO 溢出、采样率不达标、系统卡顿 优化措施 • 合理设置 DMA 通道优先级 • 缓冲区强制 4 字节对齐 • 使用非 Cache 区域或手动维护 Cache • 描述符放快速内存,减少链表节点数 • 使用双缓冲避免总线冲突 • 利用 AXI 总线矩阵分散访问 • 优先使用 DTCM 区域 • 启用描述符预取功能

这四个瓶颈,你遇到任何一个,DMA 性能都上不去。更麻烦的是,它们经常同时出现。比如总线冲突加上 Cache 不一致,查起来特别头疼。

我的经验是:先定位,再优化。别一上来就改代码。用逻辑分析仪抓总线波形,或者用 STM32 内置的调试计数器,看看 DMA 实际传输速率和理论值差多少。找到瓶颈在哪,再对症下药。

一句话总结:DMA 性能瓶颈不是单个问题,是总线、内存、Cache、描述符四个维度的综合结果。每个维度优化一点,整体性能就能上一个台阶。