3. ASoC核心数据结构:snd_soc_card、snd_soc_dai_link、snd_soc_pcm_runtime
好,咱们今天来啃三块硬骨头。这三兄弟——snd_soc_card、snd_soc_dai_link 和 snd_soc_pcm_runtime——是ASoC框架里最核心的数据结构。说白了,整个音频子系统的拓扑关系、数据通路、运行时状态,全靠它们来承载。
我刚开始接触ASoC时,对着这三个结构体看了三天,总觉得它们长得差不多。后来踩了几个坑才明白:它们分别对应着三个不同的生命周期和抽象层次。咱们一个一个来拆。
3.1 snd_soc_card:整个声卡的“户口本”
snd_soc_card 代表一个完整的声卡设备。你可以把它想象成一张声卡的“户口本”——里面记录着这个声卡叫什么、有哪些组件、怎么连接。
核心要点:一个系统里可以有多个 snd_soc_card,每个对应一个独立的声卡设备(比如 /dev/snd/controlC0、C1)。
来看看它的关键字段(我挑几个最常用的):
struct snd_soc_card {
const char *name; // 声卡名字,比如 "imx-wm8960"
const char *long_name; // 长名字,用于用户空间展示
struct device *dev; // 关联的设备节点
struct snd_card *snd_card; // ALSA 核心的 snd_card 指针
int num_links; // DAI link 的数量
struct snd_soc_dai_link *dai_link; // DAI link 数组
struct snd_soc_pcm_runtime *rtd; // 运行时实例数组
int num_rtd; // 运行时实例数量
struct snd_soc_dapm_context dapm; // DAPM 上下文
struct list_head codec_dev_list; // 挂载的 codec 设备列表
/* 回调函数 */
int (*probe)(struct snd_soc_card *card);
int (*late_probe)(struct snd_soc_card *card);
int (*remove)(struct snd_soc_card *card);
};
我个人习惯在板级文件或设备树里先定义好 snd_soc_card,然后通过 devm_snd_soc_register_card() 注册。这里有个坑——name 字段不能重名,否则注册会失败。我曾经在一个项目里同时挂了两块一样的 codec,忘了改名字,结果第二块死活注册不上,查了半天才发现是名字冲突。
小技巧:调试时可以用 cat /proc/asound/cards 查看当前系统注册了哪些声卡,名字对不对得上。
3.2 snd_soc_dai_link:连接CPU DAI和Codec的“桥梁”
snd_soc_dai_link 描述的是 一个CPU DAI和一个Codec DAI之间的连接关系。说白了,它就是一座桥——告诉框架:CPU这边的I2S接口和Codec那边的I2S接口要连起来。
这个结构体在机器驱动(Machine Driver)里定义,通常放在 snd_soc_card 的 dai_link 数组里。
struct snd_soc_dai_link {
const char *name; // link 的名字,比如 "WM8960 HiFi"
const char *stream_name; // 流名字,用于 PCM 设备命名
/* CPU DAI 侧 */
const char *cpu_name; // CPU DAI 的设备名
struct device_node *cpu_of_node; // CPU DAI 的设备树节点
const char *cpu_dai_name; // CPU DAI 的名字
/* Codec DAI 侧 */
const char *codec_name; // Codec 设备名
struct device_node *codec_of_node; // Codec 的设备树节点
const char *codec_dai_name; // Codec DAI 的名字
/* 平台侧 */
const char *platform_name; // DMA 平台设备名
struct device_node *platform_of_node;
/* 配置 */
struct snd_soc_pcm_stream *params; // 默认参数
unsigned int symmetric_rates:1; // 是否对称采样率
unsigned int symmetric_channels:1; // 是否对称通道数
unsigned int symmetric_samplebits:1;// 是否对称位宽
/* 回调 */
int (*init)(struct snd_soc_pcm_runtime *rtd);
int (*startup)(struct snd_soc_pcm_runtime *rtd);
void (*shutdown)(struct snd_soc_pcm_runtime *rtd);
};
你想想看,这里最让人头疼的是什么?是 匹配方式。你可以用名字匹配,也可以用设备树节点匹配。我建议优先用设备树节点(of_node),因为这样更灵活,也更容易维护。
注意:如果同时指定了 cpu_name 和 cpu_of_node,框架会优先使用 cpu_of_node。但千万别两个都留空,否则匹配会失败。我曾经犯过这个错——只填了 codec_name,忘了填 CPU 侧,结果 probe 时一直报找不到 CPU DAI。
还有一个字段值得关注:symmetric_rates。它表示这个 link 的播放和录音是否要使用相同的采样率。为什么需要这个?因为有些硬件(比如低端 I2S 控制器)不支持 playback 和 capture 用不同采样率。如果你不设这个标志,用户空间可能同时开两个不同采样率的流,然后硬件就罢工了。
3.3 snd_soc_pcm_runtime:运行时的“活数据”
前两个结构体是静态的——它们在驱动加载时就定好了。而 snd_soc_pcm_runtime 是 运行时动态创建 的。每当你注册一个 snd_soc_card,框架就会为每个 dai_link 创建一个对应的 rtd 实例。
你可以把 rtd 理解成 一个已经连接好的音频通路。它把静态的 link 描述变成了实实在在的、可以操作的运行时对象。
struct snd_soc_pcm_runtime {
struct device *dev; // 设备指针
struct snd_soc_card *card; // 所属的 card
struct snd_soc_dai_link *dai_link; // 对应的静态 link
struct snd_pcm *pcm; // ALSA PCM 设备实例
struct snd_compr *compr; // 压缩音频设备(如果有)
struct snd_soc_dai *cpu_dai; // CPU DAI 的运行时实例
struct snd_soc_dai *codec_dai; // Codec DAI 的运行时实例
struct snd_soc_dai **dais; // 多 DAI 情况下的数组
int num_dais; // DAI 数量
struct snd_soc_pcm_stream *dpcm[SNDRV_PCM_STREAM_LAST + 1]; // DPCM 流
/* 状态标志 */
unsigned int dev_registered:1; // 设备是否已注册
unsigned int pop_wait:1; // 是否在等待 pop 消除
struct list_head list; // 链表节点
};
这里最关键的字段是 cpu_dai 和 codec_dai。它们指向了 实际初始化后的 DAI 实例。在 probe 阶段,框架会根据 dai_link 里的匹配信息,找到对应的 DAI 驱动,然后创建这些运行时实例。
我记得有一次调试一个音频延迟问题,发现 playback 和 capture 走的不是同一个 cpu_dai。查了半天,原来是 dai_link 里配了两个不同的 CPU DAI 名字。嗯,这种低级错误,犯过一次就记住了。
3.4 三者的关系:一张图说清楚
这三个结构体的关系,我用一个比喻来解释:
- snd_soc_card 就像一栋房子的设计图纸——它定义了整栋楼的结构。
- snd_soc_dai_link 就像图纸上的一条管道——它画出了从水龙头到热水器的连接路径。
- snd_soc_pcm_runtime 就像实际安装好的管道——水真的能流过去了。
从代码角度看,它们的生命周期是这样的:
| 阶段 | 操作 | 涉及的结构体 |
|---|---|---|
| 定义 | 在机器驱动中静态定义 | snd_soc_card, snd_soc_dai_link |
| 注册 | 调用 snd_soc_register_card() | snd_soc_card |
| 匹配 | 框架遍历 dai_link,匹配 CPU/Codec 驱动 | snd_soc_dai_link |
| 创建 | 为每个 link 创建 rtd 实例 | snd_soc_pcm_runtime |
| 运行 | 用户空间打开 PCM 设备,开始音频流 | snd_soc_pcm_runtime |
核心记忆点:card 是静态的全局描述,link 是静态的连接描述,rtd 是动态的运行时实例。三者通过指针互相引用,形成一个完整的拓扑网络。
3.5 实际项目中的避坑指南
最后,分享几个我在实际项目中踩过的坑:
- dai_link 的 name 不能重复:同一个 card 里的多个 link,name 必须唯一。否则在创建 PCM 设备时会冲突。
- 别忘了设置 num_links:card 的
num_links必须和dai_link数组的实际元素个数一致。我见过有人改了 link 数组忘了改 num_links,结果只注册了一半。 - rtd 是动态分配的,别自己创建:框架会在
snd_soc_instantiate_card()里自动分配 rtd。你如果在机器驱动里手动创建,反而会出问题。 - 调试时多看看 rtd 里的指针:如果某个音频流打不开,先检查
rtd->cpu_dai和rtd->codec_dai是不是 NULL。如果是,说明 DAI 匹配失败了。
好了,这三个结构体就讲到这里。下一节咱们会深入 DAPM 的核心——widget 和 route,看看音频路径是怎么动态管理的。