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配置和寄存器操作,到时候我会拿一个实际的音频项目来拆解。你准备好了吗?