3. DMA引擎与音频数据传输
音频数据怎么从内存搬到I2S接口?说白了,靠CPU一个个字节搬肯定不行。那样CPU啥也别干了,光伺候音频中断就够呛。所以,我们得用DMA——直接内存访问。
我刚开始接触音频驱动时,总觉得DMA就是个“自动搬运工”。后来踩了几个坑才明白,DMA描述符链、循环模式、Cache一致性,每一个都是坑。今天咱们就把这些掰开揉碎了讲清楚。
3.1 DMA描述符链:数据搬运的“任务清单”
DMA控制器本身不聪明。它不知道“搬完这段数据接下来搬哪段”。所以我们需要给它一张“任务清单”——这就是描述符链。
每个描述符(Descriptor)包含:
- 源地址:数据从哪来(比如内存缓冲区)
- 目的地址:数据搬到哪去(比如I2S TX FIFO)
- 传输长度:搬多少字节
- 下一个描述符指针:搬完这个,下一个任务在哪
描述符链的结构,我画了张图帮你理解:
你看,描述符0搬完buf0,自动跳去描述符1搬buf1,再跳去描述符2搬buf2。最后一个描述符的next指针指回描述符0——这就形成了循环。
关键点:描述符链的最后一个节点必须指向第一个节点,否则DMA搬完一轮就停了。音频就会断断续续。
3.2 循环DMA模式:音频的“永动机”
音频播放是连续的。你想想看,一首歌3分钟,DMA得一直搬数据。如果每次搬完都要CPU重新配置,那跟中断搬运没啥区别。
循环DMA模式就是解决这个问题的。描述符链首尾相连,DMA自己转圈搬。CPU只需要在初始化时配好链,然后就可以去干别的事了。
我习惯把音频缓冲区切成N个等长的period(周期)。每个period对应一个描述符。比如:
| 参数 | 典型值 | 说明 |
|---|---|---|
| period_size | 1024 帧 | 每个周期包含的音频帧数 |
| period_count | 4 | 循环缓冲区中的周期数量 |
| buffer_size | 4096 帧 | 总缓冲区大小 = period_size × period_count |
DMA每搬完一个period,会触发一次中断。ALSA在中断处理里更新指针位置,把新数据填进去。这就是经典的双缓冲或多缓冲机制。
我的经验:period_count一般设4比较稳妥。设2的话,如果应用层写数据慢了,容易导致XRUN(缓冲区欠载/溢出)。设太多又浪费内存。4是个不错的平衡点。
3.3 音频数据的内存布局
音频数据在内存里怎么排?这取决于音频格式。我见过不少新手在这上面栽跟头。
以最常见的16位立体声为例:
// 一个音频帧(frame)包含左右声道各一个采样
// 采样格式:16位有符号整数,小端序
内存地址偏移: 0 2 4 6 8 10
数据内容: [L0] [R0] [L1] [R1] [L2] [R2]
<--- 帧0 ---> <--- 帧1 ---> <--- 帧2 --->
关键概念:
- 采样(Sample):单个声道的一个数据点。16位就是2字节。
- 帧(Frame):所有声道的一个采样集合。立体声就是2个采样,4字节。
- 周期(Period):一次DMA传输的数据量。通常是1024帧。
不同的音频格式,帧大小不一样:
| 格式 | 位深 | 声道数 | 帧大小 |
|---|---|---|---|
| S16_LE | 16 | 2 | 4字节 |
| S24_LE | 24 | 2 | 6字节(3字节×2) |
| S32_LE | 32 | 2 | 8字节 |
| S16_LE 5.1 | 16 | 6 | 12字节 |
注意:DMA传输的单位是字节,不是帧。配置DMA传输长度时,一定要算清楚:transfer_bytes = period_frames × frame_bytes。我曾经见过有人直接传period_frames,结果声音全是噪声——因为DMA只搬了一半数据。
3.4 Cache一致性处理:最容易忽略的坑
好,数据布局对了,描述符链也配好了。声音出来了吗?不一定。还有个隐藏Boss——Cache一致性。
现代CPU都有Cache。CPU写内存时,数据可能先留在Cache里,没真正写进DDR。DMA是直接读写DDR的,它看不到Cache。这就出问题了:
- 播放场景:CPU把音频数据写入内存缓冲区,但数据还在Cache里。DMA从DDR搬运时,搬的是旧数据。出来的声音就是断的、重复的。
- 录音场景:DMA把录音数据写进DDR,但CPU读的时候,Cache里还是旧数据。读到的全是垃圾。
怎么解决?两种办法:
方案一:使用一致性DMA缓冲区
在Linux里,用 dma_alloc_coherent() 分配的缓冲区,硬件保证CPU和DMA看到的是一致的。代价是性能稍低,因为每次访问都绕过Cache。
// 分配一致性DMA缓冲区
dma_addr_t dma_phys;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_phys, GFP_KERNEL);
// cpu_addr 是CPU看到的虚拟地址
// dma_phys 是DMA看到的物理地址
// 两者指向同一块物理内存,且Cache一致
方案二:手动Cache同步
如果为了性能想用Cacheable内存,那就得在合适的时候手动刷Cache。Linux提供了这些API:
// 播放:CPU写完数据后,刷Cache让DMA看到
dma_sync_single_for_device(dev, dma_addr, size, DMA_TO_DEVICE);
// 录音:DMA写完数据后,无效化Cache让CPU读到新数据
dma_sync_single_for_cpu(dev, dma_addr, size, DMA_FROM_DEVICE);
我的建议:音频数据量不大,用 dma_alloc_coherent() 最省心。我早期做项目时为了追求那点性能用Cacheable内存+手动同步,结果在某个平台上Cache line对齐没处理好,声音偶尔爆音。查了两天才找到原因。后来我学乖了——音频这种实时性要求高的,一致性缓冲区更靠谱。
3.5 总结:DMA音频传输的完整流程
把上面这些串起来,一个完整的DMA音频播放流程是这样的:
- 分配缓冲区:用
dma_alloc_coherent分配N个period的缓冲区 - 构建描述符链:每个period对应一个描述符,首尾相连形成循环
- 启动DMA:把第一个描述符地址告诉DMA控制器,开始搬运
- 中断处理:每搬完一个period,DMA触发中断。ALSA更新hw_ptr,唤醒等待的应用层
- 应用层填充:应用层收到通知,把新数据写入刚搬完的period
- 循环往复:DMA继续转圈搬,直到播放停止
这个流程里,任何一个环节出问题,声音就出问题。要么XRUN,要么噪声,要么断流。嗯,调试的时候别慌,按这个链条一个一个排查,总能找到问题。