2. ASoC核心数据结构:snd_soc_card、snd_soc_dai_link、snd_soc_pcm_runtime、snd_soc_component
好,咱们直接进入正题。ASoC 框架里,有几个数据结构你绕不开。它们就像盖房子的四根柱子——snd_soc_card、snd_soc_dai_link、snd_soc_pcm_runtime、snd_soc_component。搞懂它们,你才算真正入了 ASoC 的门。
我个人习惯,看源码先看结构体定义。因为结构体里的每个成员,都藏着设计者的意图。你想想看,Linux 内核那么多子系统,为什么 ASoC 要搞这么一套?说白了,就是为了把音频链路抽象出来,让驱动开发变得模块化。
2.1 snd_soc_card:整块声卡的“总管家”
snd_soc_card 代表一块完整的声卡。它不直接操作硬件,而是把各个组件串起来。我在项目中遇到过,有人把多个 DAI Link 挂到同一个 Card 上,结果注册顺序乱了,导致 probe 失败。嗯,这里要注意:Card 的 name 字段必须唯一,否则内核会报错。
核心成员一览:
name:声卡名称,用户空间通过它识别设备dai_link:指向 DAI Link 数组的指针num_links:DAI Link 的数量dev:关联的设备结构体probe/remove:声卡初始化/卸载回调
// 一个典型的 Card 定义
static struct snd_soc_card my_card = {
.name = "MyAudioCard",
.owner = THIS_MODULE,
.dai_link = my_dai_links,
.num_links = ARRAY_SIZE(my_dai_links),
};
你可能会问:Card 什么时候被创建?通常在机器驱动(Machine Driver)的 probe 函数里。我建议你把 Card 看作一个容器,它不关心底层是 I2S 还是 PCM,只负责把各个部件组装好。
2.2 snd_soc_dai_link:连接 CPU DAI 和 Codec 的“桥梁”
snd_soc_dai_link 描述了一条音频数据通路。它定义了 CPU 端的 DAI 和 Codec 端的 DAI 如何配对。我曾经调试过一个板子,声音只有左声道有输出,查了半天发现是 DAI Link 里 channels_min 设成了 1。你看,这种低级错误,往往最坑人。
避坑指南:我曾经在项目中把 cpu_dai_name 和 codec_dai_name 写反了,结果内核报 No matching DAI found。记住:CPU DAI 是 SoC 内部的数据接口,Codec DAI 是外部音频芯片的接口。
| 字段 | 说明 | 常见取值 |
|---|---|---|
name |
链路名称,用于调试 | "I2S Playback" |
cpu_dai_name |
CPU 端 DAI 名称 | "samsung-i2s.0" |
codec_dai_name |
Codec 端 DAI 名称 | "wm8960-hifi" |
platform_name |
DMA 平台名称 | "samsung-pcm" |
stream_name |
数据流名称 | "HiFi Playback" |
这里有个细节:snd_soc_dai_link 支持 dynamic 标志。如果设为 1,表示这条链路支持动态 PCM 运行时创建。嗯,这个特性在 DPCM(Dynamic PCM)场景下特别有用,后面章节我会细讲。
2.3 snd_soc_pcm_runtime:运行时状态的“活档案”
snd_soc_pcm_runtime 是 ASoC 在运行时创建的结构体。它把 Card、DAI Link、Component 全部关联起来。说白了,它就是 Card 和 DAI Link 的“运行时快照”。
我刚开始看代码时,总搞不清 snd_soc_pcm_runtime 和 snd_soc_dai_link 的区别。后来才明白:dai_link 是静态描述,pcm_runtime 是动态实例。你想想看,一个 DAI Link 可能被多个 PCM 设备复用,但每个 PCM 设备都有自己的 pcm_runtime。
关键成员:
card:指向所属的snd_soc_carddai_link:指向对应的snd_soc_dai_linkcpu_dai:CPU 端 DAI 的运行时实例codec_dai:Codec 端 DAI 的运行时实例platform:DMA 平台的运行时实例pcm:关联的snd_pcm结构体
// 内核中创建 pcm_runtime 的简化流程
struct snd_soc_pcm_runtime *rtd;
rtd = kzalloc(sizeof(*rtd), GFP_KERNEL);
rtd->card = card;
rtd->dai_link = dai_link;
// 后续会初始化 cpu_dai、codec_dai 等成员
注意:snd_soc_pcm_runtime 是在 snd_soc_instantiate_card() 函数中创建的。如果你在调试时发现某个 PCM 设备没有声音,可以检查一下 rtd 是否成功创建。我习惯在 probe 函数里加一句 dev_info 打印 rtd 的地址,方便定位问题。
2.4 snd_soc_component:音频硬件的“抽象层”
snd_soc_component 是 ASoC 中最重要的抽象之一。它统一了 Codec、Platform 和 CPU DAI 的接口。在旧版本内核中,这三个东西是分开的,后来社区觉得太乱,就搞了个 snd_soc_component 把它们统一起来。
我记得有一次移植 Codec 驱动,发现 snd_soc_component 的 probe 回调没有被调用。查了半天,原来是 component_driver 没有正确注册。嗯,这里要注意:每个 Component 必须实现 probe 和 remove 回调,否则内核会直接跳过。
个人经验:我建议你在写新驱动时,优先使用 snd_soc_component_driver 结构体来注册。它比老旧的 snd_soc_codec_driver 更灵活,而且支持更多高级特性,比如 compress_ops 和 pcm_new。
| 成员 | 说明 | 典型实现 |
|---|---|---|
name |
组件名称 | "wm8960.0-001a" |
dapm_widgets |
DAPM 控件数组 | 音频路径中的 mixer、mux 等 |
num_dapm_widgets |
DAPM 控件数量 | ARRAY_SIZE(widgets) |
controls |
ALSA 控制接口数组 | 音量、开关等 |
num_controls |
控制接口数量 | ARRAY_SIZE(controls) |
你可能会问:Component 和 DAI 是什么关系?简单来说,一个 Component 可以包含多个 DAI。比如一个 Codec 芯片可能有 I2S 和 PCM 两个 DAI 接口,它们都属于同一个 Component。在 snd_soc_component 中,通过 dai_drv 数组来管理这些 DAI。
2.5 四个结构体的协作关系
好了,四个核心结构体都介绍完了。它们之间怎么配合?我画个逻辑图给你看:
- Card 是顶层容器,包含多个 DAI Link
- 每个 DAI Link 描述一条音频通路,指定 CPU DAI 和 Codec DAI
- 运行时,每个 DAI Link 对应一个 pcm_runtime
- pcm_runtime 内部持有 Component 的引用,通过 Component 操作硬件
一句话总结:Card 是声卡,DAI Link 是图纸,pcm_runtime 是施工队,Component 是建筑材料。四者缺一不可。
最后说个我踩过的坑:在 snd_soc_card 的 probe 函数里,不要直接访问 pcm_runtime。因为此时 pcm_runtime 还没有创建。正确的做法是在 late_probe 回调中操作,或者通过 snd_soc_find_dai_link() 函数查找。
下一章,我会带你深入 DAPM(动态音频电源管理)的世界。那可是 ASoC 最精彩的部分,准备好了吗?