4、Linux ALSA驱动框架:核心数据结构与架构概述
好,我们进入第4章。这一章是理解整个音频子系统的基石——ALSA驱动框架。说白了,就是Linux内核怎么把声卡、编解码器、DMA这些硬件抽象成一套统一的软件接口。我刚开始接触ALSA时,也被那一堆snd_soc_xxx结构体搞得晕头转向。后来调了几个板子才明白,其实核心就那么几个数据结构,搞清楚了,整个框架就通了。
4.1 ALSA架构概述
ALSA在Linux音频系统中处于中间层。往上,它给用户空间提供/dev/snd/下的设备节点和alsa-lib库;往下,它直接操作I2C、I2S、DMA等硬件接口。
在嵌入式领域,我们主要跟ASoC(ALSA System on Chip)打交道。ASoC把音频驱动拆成了三个角色:
- Codec驱动:管理编解码芯片本身,比如WM8960的寄存器、PLL配置、ADC/DAC路径。
- Platform驱动:负责DMA传输和CPU侧的音频接口(I2S、PCM)。
- Machine驱动:把Codec和Platform绑在一起,描述板级硬件连接(比如用了哪个I2S口、哪个时钟源)。
这种分层设计,说白了就是为了复用。换一颗Codec,只要换Codec驱动;换一个CPU平台,只要换Platform驱动。Machine驱动则像胶水,把两者粘起来。
核心思想:ASoC通过 snd_soc_card 作为顶层容器,把 dai_link、codec、platform 组织在一起。你写驱动时,大部分时间就是在填充这些结构体。
4.2 核心数据结构详解
我们逐个来看。这些结构体定义在 include/sound/soc.h 里,建议你打开内核源码对照着看。
4.2.1 snd_soc_card —— 声卡顶层容器
snd_soc_card 代表一张“声卡”。它不直接对应硬件,而是描述一个音频子系统。比如你的板子上有一颗WM8960,那通常就只有一个card。
关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
name |
const char * |
声卡名称,用户空间通过它识别 |
dai_link |
struct snd_soc_dai_link * |
指向DAI链接数组,描述数据通路 |
num_links |
int |
dai_link数组的元素个数 |
dev |
struct device * |
关联的设备节点 |
probe |
int (*probe)(struct snd_soc_card *) |
card初始化回调,常用于配置GPIO、时钟 |
我的习惯:在Machine驱动的probe函数里,我会先检查card->dai_link的配置是否正确。曾经有个项目,因为dai_link的name字段拼写错误,导致Codec和Platform匹配不上,音频死活不出声。查了两天才发现是字符串不匹配。
4.2.2 snd_soc_dai_link —— 数据通路的描述
这个结构体描述了一条完整的音频数据路径:从CPU的I2S接口到Codec的数字音频接口(DAI)。
关键字段:
| 字段 | 说明 |
|---|---|
name |
链路名称,用于匹配和调试 |
stream_name |
流名称,PCM流创建时使用 |
cpu_dai_name |
CPU侧DAI的名称,对应Platform驱动 |
codec_dai_name |
Codec侧DAI的名称 |
codec_name |
Codec设备名称,用于查找已注册的Codec |
platform_name |
Platform设备名称,用于DMA传输 |
ops |
指向snd_soc_ops,包含startup/shutdown/hw_params/prepare等回调 |
嗯,这里要注意:cpu_dai_name 和 codec_dai_name 必须与DAI驱动中注册的名称完全一致。我见过有人把 wm8960-hifi 写成了 wm8960-hifi-0,结果内核报 Failed to bind。
4.2.3 snd_soc_codec —— 编解码器抽象
这个结构体代表一颗Codec芯片。WM8960的驱动最终会注册一个snd_soc_codec实例。
关键字段:
| 字段 | 说明 |
|---|---|
name |
Codec名称,用于dai_link匹配 |
dev |
指向I2C或SPI设备节点 |
regmap |
寄存器缓存,用于读写Codec寄存器 |
component |
指向snd_soc_component,新内核中Codec已合并为Component |
dapm |
DAPM上下文,管理动态电源和音频路径 |
controls |
ALSA控制接口,如音量、开关 |
避坑指南:我曾经在移植WM8960驱动时,忽略了regmap的配置。默认的regmap使用I2C单字节读写,但WM8960有些寄存器需要多字节操作。结果音量控制死活不生效。后来在regmap_config里设置了reg_bits = 7和val_bits = 9才搞定。嗯,WM8960的寄存器地址是7位,数据是9位,这个细节很容易忘。
4.2.4 snd_soc_platform —— DMA与CPU接口抽象
snd_soc_platform 负责CPU侧的音频数据传输。它管理DMA通道、FIFO、I2S控制器等。
关键字段:
| 字段 | 说明 |
|---|---|
name |
Platform名称 |
pcm_ops |
指向snd_pcm_ops,包含open/close/ioctl/hw_params/trigger/pointer等 |
pcm_new |
创建PCM设备的回调 |
pcm_free |
释放PCM设备的回调 |
说白了,Platform驱动就是告诉内核:我的CPU怎么把数据从内存搬到I2S接口。DMA描述符怎么配、中断怎么处理,都在这里。
4.3 数据结构之间的关系
这几个结构体不是孤立的。它们通过指针和名称互相引用,形成一个网状关系。我画个简化的关系图:
snd_soc_card
|
+-- dai_link[0]
| |-- cpu_dai_name ----> 匹配 Platform 驱动的 DAI
| |-- codec_name ----> 匹配 snd_soc_codec->name
| |-- codec_dai_name ----> 匹配 Codec 驱动的 DAI
| |-- platform_name ----> 匹配 snd_soc_platform->name
| |-- ops ----> hw_params 回调在这里设置音频格式
|
+-- dev ----> 设备节点,用于 GPIO、时钟等板级资源
匹配过程是这样的:
- 内核解析
dai_link,根据codec_name找到已注册的snd_soc_codec。 - 根据
cpu_dai_name和codec_dai_name找到对应的DAI结构。 - 根据
platform_name找到snd_soc_platform。 - 调用
dai_link->ops->hw_params设置采样率、位宽、时钟极性。 - 最终,PCM设备被创建,用户空间可以通过
/dev/snd/pcmC0D0p访问。
关键点:匹配完全基于字符串名称。所以驱动中 name 字段的命名必须一致且唯一。我习惯在Codec驱动和Machine驱动中用相同的命名规范,比如 "wm8960-codec"、"wm8960-dai",这样不容易出错。
4.4 一个简单的Machine驱动示例
我们来看一个实际例子。假设板子上只有一颗WM8960,通过I2S0连接。Machine驱动的核心代码大概是这样:
static struct snd_soc_dai_link my_board_dai_links[] = {
{
.name = "WM8960 HiFi",
.stream_name = "WM8960 HiFi",
.cpu_dai_name = "samsung-i2s.0",
.codec_name = "wm8960.0-001a",
.codec_dai_name = "wm8960-hifi",
.platform_name = "samsung-i2s.0",
.ops = &my_board_ops,
},
};
static struct snd_soc_card my_board_card = {
.name = "my-board-card",
.owner = THIS_MODULE,
.dai_link = my_board_dai_links,
.num_links = ARRAY_SIZE(my_board_dai_links),
};
static int my_board_probe(struct platform_device *pdev)
{
struct snd_soc_card *card = &my_board_card;
card->dev = &pdev->dev;
return devm_snd_soc_register_card(&pdev->dev, card);
}
你看,代码量其实不大。大部分工作是在填充结构体。但就是这些字段,决定了音频能否正常工作。
我的经验:调试时,先确认 dai_link 中的 cpu_dai_name 和 codec_dai_name 是否能在 /sys/kernel/debug/asoc/dais 中找到。如果找不到,说明驱动没注册成功,或者名称不匹配。这个debugfs目录是我调试ALSA的第一站。
4.5 小结
这一章我们覆盖了ALSA ASoC框架的四个核心数据结构:
- snd_soc_card:声卡容器,组织所有链路。
- snd_soc_dai_link:数据通路描述,连接CPU和Codec。
- snd_soc_codec:编解码器抽象,管理寄存器、DAPM、控制接口。
- snd_soc_platform:DMA和CPU接口抽象,负责数据传输。