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 实际项目中的避坑指南

最后,分享几个我在实际项目中踩过的坑:

  1. dai_link 的 name 不能重复:同一个 card 里的多个 link,name 必须唯一。否则在创建 PCM 设备时会冲突。
  2. 别忘了设置 num_links:card 的 num_links 必须和 dai_link 数组的实际元素个数一致。我见过有人改了 link 数组忘了改 num_links,结果只注册了一半。
  3. rtd 是动态分配的,别自己创建:框架会在 snd_soc_instantiate_card() 里自动分配 rtd。你如果在机器驱动里手动创建,反而会出问题。
  4. 调试时多看看 rtd 里的指针:如果某个音频流打不开,先检查 rtd->cpu_dai 和 rtd->codec_dai 是不是 NULL。如果是,说明 DAI 匹配失败了。

好了,这三个结构体就讲到这里。下一节咱们会深入 DAPM 的核心——widget 和 route,看看音频路径是怎么动态管理的。