4. 音频DMA与内存管理:MTK音频DMA控制器、环形缓冲区管理、Scatter-Gather传输机制
好,咱们今天聊点硬核的——音频DMA和内存管理。
说实话,音频驱动里最容易出bug的地方,往往不是音频算法本身,而是数据搬运这块。你想想看,音频数据流就像一条水管,DMA就是那个水泵。水泵坏了,或者水管堵了,声音肯定出问题。我在MTK平台上调试过不下几十个音频问题,至少有一半跟DMA配置或内存管理有关。
4.1 MTK音频DMA控制器概览
MTK的音频DMA控制器,说白了就是一块专门负责搬运音频数据的硬件模块。它不需要CPU插手,自己就能把数据从内存搬到I2S接口,或者反过来。
我个人习惯把MTK音频DMA分为两类:
- Normal DMA:传统的DMA方式,一次传输一块连续内存
- Scatter-Gather DMA:支持非连续内存块传输,更灵活
嗯,这里要注意,MTK的音频DMA控制器通常有多个通道。比如MT6765平台,音频DMA通道有8个,每个通道可以独立配置。我遇到过一个问题,就是两个音频流同时播放时,通道分配冲突了,结果一个没声音。后来我加了个通道管理机制,才彻底解决。
核心寄存器:音频DMA控制器的关键寄存器包括:
- AFE_DMA_CON:DMA控制寄存器,使能/复位
- AFE_DMA_START:启动地址寄存器
- AFE_DMA_END:结束地址寄存器
- AFE_DMA_COUNT:传输计数寄存器
4.2 环形缓冲区管理
环形缓冲区,也叫循环缓冲区,是音频驱动里最常用的数据结构。为什么用环形?因为音频数据是源源不断的流,用环形缓冲区可以避免频繁的内存分配和释放。
我给你们画个简单的逻辑图:
+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ← 缓冲区(8个单元)
+---+---+---+---+---+---+---+---+
↑ ↑
读指针 写指针
读指针指向DMA正在读取的位置,写指针指向CPU写入的位置。当写指针追上读指针时,说明缓冲区满了,需要处理溢出。
我曾经在项目里遇到一个诡异的问题:播放音乐时每隔几秒就出现一次爆音。查了两天,最后发现是环形缓冲区的读指针和写指针没有正确同步,导致DMA读到了未更新的数据。解决方案很简单——加一个内存屏障(memory barrier)就好了。
我的经验:环形缓冲区的大小设置很关键。太小了容易溢出,太大了浪费内存。一般建议设置为音频周期(period)的2-4倍。比如采样率48kHz、周期大小1024帧,缓冲区大小设为4096帧就挺合适。
4.3 Scatter-Gather传输机制
Scatter-Gather,简称SG,是MTK音频DMA的一大亮点。它允许DMA从多个不连续的内存块中读取数据,或者写入到多个不连续的内存块。
为什么要用SG?你想想看,音频数据在内存中不一定是连续的。比如你从网络接收音频数据,可能分片存储在不同的内存区域。如果用普通DMA,你得先把数据拷贝到一块连续内存里,再启动DMA传输。这多了一步拷贝,效率就低了。
SG传输的核心是描述符链表(Descriptor Chain)。每个描述符包含:
- 源地址/目的地址
- 传输长度
- 下一个描述符的指针
- 控制标志(如中断使能、链结束标志)
看个简单的描述符结构体定义:
struct mtk_audio_sg_desc {
uint32_t src_addr; // 源地址
uint32_t dst_addr; // 目的地址
uint32_t length; // 传输长度
uint32_t next_desc; // 下一个描述符地址
uint32_t flags; // 控制标志
};
DMA控制器会自动遍历这个链表,一个描述符传输完成后,自动跳到下一个。直到遇到链结束标志,或者触发中断通知CPU。
注意:描述符链表必须放在物理连续的内存中,而且地址要对齐到16字节。我曾经因为描述符地址没对齐,导致DMA传输到一半卡住了,整个音频系统死掉。这个坑我踩过,你们别踩。
4.4 内存分配与对齐要求
MTK音频DMA对内存有严格的要求。说白了,不是随便malloc一块内存就能用的。
主要要求如下:
| 参数 | 要求 | 说明 |
|---|---|---|
| 物理地址连续 | 必须 | DMA只能访问物理连续内存 |
| 地址对齐 | 32字节 | 音频数据缓冲区需要32字节对齐 |
| 内存类型 | 非缓存(Non-cacheable) | 避免CPU缓存与DMA数据不一致 |
| 大小限制 | 2的幂次方 | 某些平台要求缓冲区大小为2的幂 |
在Linux内核中,我们通常使用dma_alloc_coherent()来分配符合要求的内存。这个函数会返回物理地址和虚拟地址,而且保证内存是非缓存的。
// 分配音频DMA缓冲区
dma_addr_t phys_addr;
void *virt_addr;
virt_addr = dma_alloc_coherent(dev,
BUFFER_SIZE,
&phys_addr,
GFP_KERNEL);
if (!virt_addr) {
pr_err("Failed to allocate DMA buffer\n");
return -ENOMEM;
}
避坑指南:我曾经在项目里用了kmalloc分配内存给DMA用,结果在播放高采样率音频时频繁出现数据错乱。后来才发现,kmalloc分配的内存可能是缓存的,CPU和DMA看到的数据不一致。换成dma_alloc_coherent后问题解决。
4.5 实际调试经验分享
最后,我分享几个实际调试中遇到的案例,希望能帮到你们。
案例1:播放时出现周期性杂音
现象:播放音乐时,每隔1秒左右出现一次"咔咔"声。
排查过程:我用逻辑分析仪抓了I2S信号,发现数据流中有周期性的空白段。进一步检查DMA中断,发现中断处理函数执行时间过长,导致DMA传输来不及更新。解决方案是优化中断处理函数,把耗时操作放到工作队列中执行。
案例2:录音数据丢失
现象:录音时,前几秒的数据总是丢失。
排查过程:检查环形缓冲区,发现DMA启动后,CPU还没来得及更新读指针,DMA就已经开始往缓冲区写数据了。解决方案是在启动DMA之前,先预填充一部分数据到缓冲区,或者使用双缓冲机制。
案例3:SG传输链断裂
现象:播放长音频文件时,播到一半突然停止。
排查过程:检查SG描述符链表,发现最后一个描述符的"下一个描述符指针"指向了无效地址。原因是描述符链表在内存中被其他模块覆盖了。解决方案是把描述符链表放在专用的内存区域,并加上保护机制。
总结一下:音频DMA和内存管理是音频驱动的基石。环形缓冲区解决数据流的连续性问题,SG传输解决内存碎片问题,正确的内存分配保证数据一致性。这三个点搞明白了,音频驱动就成功了一半。
嗯,今天就聊到这里。下一章我会讲音频时钟和采样率管理,那也是个大坑,到时候再跟你们细说。