3、Linux ALSA框架:架构分层与核心组件
ALSA 框架,说白了就是 Linux 下音频驱动的「总调度室」。我刚接触时也觉得它很复杂,分层多、概念杂。但摸熟了之后会发现,它的设计其实很优雅——每一层各司其职,出了问题也容易定位。
3.1 ALSA 架构分层
ALSA 在用户空间和硬件之间,一共拆了三层。我习惯把它画成下面这张图:
从上往下看,数据流是这样的:
- 用户空间:应用程序调用 ALSA lib API,比如 aplay、arecord,或者你写的播放器。
- ALSA 核心层:管理声卡设备(snd_card)、PCM 设备、Control 接口。它不关心底层硬件细节。
- ASoC 层:这是嵌入式音频驱动的核心。它把驱动拆成三块——Machine、Platform、Codec。
- 硬件层:I2S 总线、DMA 控制器、Codec 芯片(比如 WM8960)。
3.2 snd_soc_dai_link 与 snd_soc_card
这两个结构体,是 ASoC 层里最重要的「接线图」。我调试过的板子,有一半的音频问题最后都出在这里——要么 link 没配对,要么 card 注册顺序错了。
3.2.1 snd_soc_card
它代表一块「声卡」。一个 card 可以包含多个 DAI link,每个 link 连接一个 CPU DAI 和一个 Codec DAI。
struct snd_soc_card {
const char *name;
const char *long_name;
struct snd_soc_dai_link *dai_link;
int num_links;
// ... 其他字段
};
注册时,驱动里会这样写:
static struct snd_soc_card my_card = {
.name = "WM8960-Audio",
.dai_link = my_dai_links,
.num_links = ARRAY_SIZE(my_dai_links),
};
static int my_probe(struct platform_device *pdev) {
return devm_snd_soc_register_card(&pdev->dev, &my_card);
}
3.2.2 snd_soc_dai_link
它描述一条音频数据通路。我见过新手把 codec_dai_name 写错,结果驱动加载成功但播放没声音——查了两天才发现是字符串拼写问题。
static struct snd_soc_dai_link my_dai_links[] = {
{
.name = "WM8960 Playback",
.stream_name = "Playback",
.cpu_dai_name = "2020000.i2s",
.codec_dai_name = "wm8960-hifi",
.platform_name = "2020000.i2s",
.codec_name = "wm8960.0-001a",
.ops = &my_ops,
.dai_fmt = SND_SOC_DAIFMT_I2S |
SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS,
},
};
| 字段 | 含义 | 常见坑 |
|---|---|---|
| cpu_dai_name | CPU 侧 I2S 接口的设备名 | 设备树中 compatible 不匹配 |
| codec_dai_name | Codec 侧的 DAI 名称 | 拼写错误,大小写敏感 |
| platform_name | DMA 引擎对应的平台设备 | 与 cpu_dai 共用时容易漏配 |
| dai_fmt | I2S 格式、时钟极性和主从模式 | 主从模式配反导致无数据 |
3.3 DMA 引擎与环形缓冲区
音频数据量大,用 CPU 逐字节搬运不现实。DMA 引擎就是干这个的——它负责把内存里的音频数据搬到 I2S 发送 FIFO,或者从 I2S 接收 FIFO 搬到内存。
3.3.1 环形缓冲区(Ring Buffer)
ALSA 的 PCM 缓冲区本质是一个环形缓冲区。应用程序写数据到缓冲区尾部,DMA 从缓冲区头部读取发送。两者通过指针协调:
- appl_ptr:应用程序当前写入位置。
- hw_ptr:DMA 当前读取位置。
- 当 appl_ptr 追上 hw_ptr,缓冲区满,应用程序阻塞等待。
- 当 hw_ptr 追上 appl_ptr,缓冲区空,DMA 静音输出。
为什么会发生 XRUN(欠载/溢出)?说白了就是 appl_ptr 和 hw_ptr 的追赶游戏没玩好。应用程序写慢了,DMA 读空了——这就是 underrun。反过来就是 overrun。
3.3.2 DMA 引擎配置
在 Platform 驱动里,DMA 的配置通常长这样:
static const struct snd_dmaengine_pcm_config my_dma_config = {
.prepare_slave_config = snd_dmaengine_pcm_prepare_slave_config,
.compat_filter_fn = NULL,
.pcm_hardware = &my_pcm_hardware,
.prealloc_buffer_size = 64 * 1024,
};
static int my_platform_probe(struct platform_device *pdev) {
return devm_snd_dmaengine_pcm_register(&pdev->dev, &my_dma_config, 0);
}
这里要注意 prealloc_buffer_size。设太小,高采样率下容易 XRUN;设太大,浪费内存。我一般设 64KB,对于 48kHz/16bit/双声道,大约能缓冲 340ms 的数据,够用。
3.3.3 DMA 传输流程
- 应用程序调用
snd_pcm_writei(),数据写入环形缓冲区。 - ALSA 核心层更新 appl_ptr,触发 DMA 传输请求。
- Platform 驱动的
trigger()回调启动 DMA 通道。 - DMA 控制器从缓冲区搬运数据到 I2S TX FIFO。
- I2S 控制器按 BCLK 节奏将数据串行发送到 Codec。
- DMA 传输完成,产生中断,更新 hw_ptr。
- 如果 hw_ptr 超过 appl_ptr 一个 period,ALSA 核心唤醒应用程序继续写入。
trigger() 回调里加打印,确认 DMA 是否被正确启动。如果 trigger 被调用了但没声音,多半是 DMA 通道配置错了——检查 dma_request_slave_channel 的参数。
3.4 本章小结
ALSA 框架的分层设计,让音频驱动开发变得模块化。你不需要同时理解所有细节——先搞清楚你的问题在哪一层,再深入那一层去查。snd_soc_dai_link 是连接 CPU 和 Codec 的桥梁,配错了就没声音。DMA 引擎和环形缓冲区决定了延迟和稳定性,调参是个细致活。