2、DMA基础概念:什么是DMA、DMA与CPU中断的区别、DMA传输模式(单次/循环/链表)、DMA性能优势分析。

好,咱们正式开始聊DMA。说实话,很多做嵌入式音频的朋友,一开始都绕不开这个坎。我当年刚接触音频项目时,也被DMA搞得一头雾水。但搞懂了它,你的系统性能才能真正释放出来。

2.1 什么是DMA?

DMA,全称Direct Memory Access,直接存储器访问。说白了,就是硬件层面的「数据搬运工」。

你想想看,CPU本来是个大忙人,要算数学、跑逻辑、处理中断。结果呢?音频数据来了,CPU得亲自去I2S接口读一个字节,再写到内存里。读一个,写一个,循环往复。这不就跟让CEO亲自去搬仓库的箱子一样浪费吗?

DMA的作用就是:让CPU从数据搬运中解放出来。DMA控制器是一个独立的外设,它可以在不占用CPU的情况下,自动完成外设和内存之间的数据传输。

核心理解: DMA = 硬件级的memcpy + 自动触发机制。它知道从哪里读、写到哪里、传多少字节,全程不需要CPU插手。

我在项目中遇到过这样的情况:一个48kHz、16位立体声的音频流,每秒要传输 48k × 2 × 2 = 192k 字节。如果用CPU逐字节搬运,光中断处理就能吃掉30%以上的CPU性能。用了DMA后,CPU占用率直接降到5%以下。差距就是这么明显。

2.2 DMA与CPU中断的区别

很多新手会问:「DMA和中断不都是用来处理数据传输的吗?」嗯,这个问题问得好。它们确实都跟数据传输有关,但角色完全不同。

对比项 DMA CPU中断
谁干活 DMA控制器(硬件) CPU(软件)
数据搬运 自动完成,不占CPU 每次中断都要CPU亲自搬
触发方式 外设请求或软件触发 外设事件或定时器
开销 几乎为零(硬件自动) 上下文切换 + 压栈出栈
适用场景 大数据块、周期性传输 事件通知、异常处理

我个人的理解是这样的:中断是「通知机制」,DMA是「执行机制」。中断告诉CPU「数据来了,你快来处理」;而DMA直接就把数据搬好了,然后可能发一个中断告诉CPU「活干完了,你来用吧」。

避坑指南: 我曾经在一个音频项目中,试图只用中断来处理I2S数据。结果音频一开,系统就卡死。后来发现是中断频率太高(48kHz),CPU根本来不及做其他事。换成DMA + 低频中断(比如每半帧中断一次),问题立刻解决。

所以,正确的做法是:DMA负责搬砖,CPU负责动脑。DMA把数据搬完,发一个中断告诉CPU「数据准备好了」,CPU只需要做一次处理,而不是每来一个字节就中断一次。

2.3 DMA传输模式

DMA不是只能一种方式干活。根据实际需求,它有三种主流模式。我一个个说。

2.3.1 单次传输模式

这是最基础的DMA模式。配置好源地址、目的地址、传输长度,启动一次,传完就停。

// 伪代码示例:单次传输配置
DMA_InitTypeDef dma_init;
dma_init.SourceAddress = (uint32_t)&I2S_DR;   // 从I2S数据寄存器读
dma_init.DestAddress   = (uint32_t)audio_buf; // 写到内存缓冲区
dma_init.TransferSize  = 1024;                // 传输1024个字节
dma_init.Mode          = DMA_MODE_SINGLE;     // 单次模式
DMA_Init(&dma_init);
DMA_Start();  // 启动,传完自动停止

这种模式适合一次性数据传输,比如把一段音频数据从Flash搬到内存。传完就结束,干净利落。

2.3.2 循环传输模式

音频播放最常用的模式。DMA传完一轮数据后,自动回到起始地址重新开始,周而复始。

// 伪代码示例:循环传输配置
dma_init.Mode = DMA_MODE_CIRCULAR;  // 循环模式
dma_init.BufferSize = 512;          // 缓冲区大小
DMA_Init(&dma_init);
DMA_Start();  // 启动后永不停止,直到你手动停止

为什么音频要用循环模式?因为音频是连续流。你想想看,如果每次传完都要重新配置DMA,中间必然产生间隙,音频就会「咔咔」断音。循环模式保证了数据流的连续性。

注意: 循环模式下,一定要处理好「半传输中断」和「传输完成中断」。我习惯用双缓冲(ping-pong buffer)配合循环DMA,一个缓冲区在播放,另一个在填充数据,这样永远不会出现数据断流。

2.3.3 链表传输模式

这个模式高级一些,也叫「散聚传输」或「描述符链」。DMA可以按照一个链表结构,依次执行多个不同的传输任务。

// 伪代码示例:链表传输配置
DMA_LinkedListNode node1, node2, node3;
node1.SourceAddr = src1;  node1.DestAddr = dst1;  node1.Size = 256;
node2.SourceAddr = src2;  node2.DestAddr = dst2;  node2.Size = 128;
node3.SourceAddr = src3;  node3.DestAddr = dst3;  node3.Size = 512;

// 链接节点
node1.Next = &node2;
node2.Next = &node3;
node3.Next = NULL;  // 链表结束

DMA_LinkedListInit(&node1);  // 从第一个节点开始执行
DMA_Start();

这种模式适合复杂的数据处理场景。比如音频处理中,你可能需要从三个不同的内存区域取数据,拼成一个完整的音频帧。用链表模式,DMA可以自动完成这个拼接过程,CPU完全不用管。

我记得在一个多通道音频项目中,需要同时采集4个麦克风的数据。用链表DMA,每个通道配置一个节点,DMA自动轮询采集,效率极高。

2.4 DMA性能优势分析

说了这么多,咱们用数据说话。DMA到底能带来多少性能提升?

场景 无DMA(纯中断) 有DMA 性能提升
48kHz立体声播放 CPU占用率约35% CPU占用率约3% 10倍以上
SD卡音频文件读取 阻塞式读取,卡顿明显 后台DMA传输,流畅播放 体验质变
多通道音频采集 中断频繁,容易丢数据 DMA自动轮询,零丢失 可靠性大幅提升

性能优势主要体现在三个方面:

  • CPU利用率提升: 这是最直接的。CPU从数据搬运工变成了任务管理者,可以把算力用在音频算法、UI响应等更有价值的地方。
  • 数据传输速率更高: DMA是硬件直接传输,没有指令取指、解码、执行的开销。在高速音频场景(比如192kHz采样率)下,DMA几乎是唯一的选择。
  • 系统响应更及时: 因为CPU不再被频繁中断打断,其他任务(比如按键检测、屏幕刷新)可以更流畅地运行。

总结一句话: DMA不是锦上添花,而是嵌入式音频系统的刚需。没有DMA,你很难做出高质量、低延迟的音频产品。

好了,DMA的基础概念就聊到这里。下一章咱们会深入具体的DMA配置和寄存器操作,到时候我会拿一个实际的音频项目来拆解。你准备好了吗?