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_card
  • dai_link:指向对应的 snd_soc_dai_link
  • cpu_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 四个结构体的协作关系

好了,四个核心结构体都介绍完了。它们之间怎么配合?我画个逻辑图给你看:

  1. Card 是顶层容器,包含多个 DAI Link
  2. 每个 DAI Link 描述一条音频通路,指定 CPU DAI 和 Codec DAI
  3. 运行时,每个 DAI Link 对应一个 pcm_runtime
  4. 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 最精彩的部分,准备好了吗?