3、Machine驱动:Machine驱动的角色、snd_soc_card实例化、dai_link配置、platform和codec绑定

好,我们继续往下走。前面两章我们把 ASoC 的框架和 Codec、Platform 驱动都聊了一遍。现在该聊聊 Machine 驱动了。

说实话,Machine 驱动在 ASoC 里是个「粘合剂」的角色。它本身不处理音频数据,也不管硬件寄存器怎么配。它的任务就一个——把 Codec 和 Platform 绑在一起,告诉内核:「嘿,我这块板子上,Codec 是这颗,DMA 走那个通道,音频路径这么连。」

我个人习惯把 Machine 驱动比作「接线图」。你想想看,Codec 和 Platform 就像两个独立的模块,各自有各自的接口。Machine 驱动就是那张图纸,告诉系统怎么把它们焊在一起。

3.1 Machine驱动的角色

Machine 驱动到底干些什么?我总结下来,核心就三件事:

  • 实例化 snd_soc_card:这是整个音频子系统的顶层结构体。一个 card 就代表一个声卡。
  • 配置 DAI link:告诉内核,Codec 的哪个 DAI 和 Platform 的哪个 DAI 连在一起。
  • 绑定 Platform 和 Codec:让音频数据通路跑通。

嗯,这里要注意。Machine 驱动不是必须的。如果你的板子很简单,可以用设备树直接描述。但大多数产品级的项目,都会写一个 Machine 驱动来做精细控制。

核心理解:Machine 驱动是「策略层」,Codec 和 Platform 是「执行层」。策略层决定谁和谁连,执行层负责具体干活。

3.2 snd_soc_card 实例化

先看个最简单的例子。我在项目中经常这样写:

static struct snd_soc_card my_card = {
    .name = "MyBoard Audio Card",
    .owner = THIS_MODULE,
    .dai_link = my_dai_links,
    .num_links = ARRAY_SIZE(my_dai_links),
};

这个结构体就是声卡的「身份证」。name 字段会在用户空间看到,比如 aplay -l 列出来的名字。owner 不用多说,填 THIS_MODULE 就行。

真正重要的是 dai_link 和 num_links。这两个字段决定了你的声卡有几个音频通路。

我曾经接手过一个项目,同事把 num_links 写死了,结果后面加了一个新的 DAI link 忘了改这里。调试了两天才发现是计数不对。嗯,这种低级错误,犯过一次就记住了。

实例化之后,还要注册:

static int my_board_probe(struct platform_device *pdev)
{
    struct snd_soc_card *card = &my_card;

    card->dev = &pdev->dev;
    return devm_snd_soc_register_card(&pdev->dev, card);
}

这里有个小细节。我建议用 devm_snd_soc_register_card 而不是 snd_soc_register_card。前者是设备管理版本,驱动卸载时会自动注销,省得你忘了写 remove 函数。

个人经验:用 devm_ 系列函数,能省掉一半的 cleanup 代码。我现在的项目里,基本看不到手动注销的代码了。

3.3 dai_link 配置

DAI link 是 Machine 驱动的灵魂。它定义了音频数据的流动路径。看一个典型配置:

static struct snd_soc_dai_link my_dai_links[] = {
    {
        .name = "MyCodec",
        .stream_name = "MyCodec Playback",
        .cpu_dai_name = "2020000.i2s",
        .codec_dai_name = "wm8960-hifi",
        .platform_name = "2020000.i2s",
        .codec_name = "wm8960.0-001a",
        .ops = &my_link_ops,
    },
};

每个字段什么意思?我拆开讲:

  • name:这个 link 的名字,调试时有用。
  • stream_name:流名称,PCM 设备创建时会用到。
  • cpu_dai_name:CPU 侧的 DAI 名字,一般是 I2S 控制器。
  • codec_dai_name:Codec 侧的 DAI 名字,比如 wm8960-hifi。
  • platform_name:DMA 平台设备的名字,通常和 cpu_dai 一样。
  • codec_name:Codec 设备的名字,格式是 "驱动名.总线地址"。
  • ops:回调函数集,用来做时钟、格式等配置。

你可能会问,这些名字从哪来?从设备树里来。比如:

&i2s1 {
    status = "okay";
};

&i2c1 {
    wm8960: wm8960@1a {
        compatible = "wlf,wm8960";
        reg = <0x1a>;
    };
};

设备树里 i2s1 的 name 就是 "2020000.i2s",wm8960 的 name 就是 "wm8960.0-001a"。这些名字在内核里是唯一的。

避坑指南:我曾经因为设备树里 reg 写错了,导致 codec_name 对不上,驱动 probe 失败。查了两天才发现是 i2c 地址写成了 0x1b。记住,设备树里的 reg 和 codec_name 里的地址必须一致。

3.4 platform 和 codec 绑定

绑定过程,说白了就是 ASoC 核心帮你做「配对」。你提供了名字,核心去注册的设备链表里找。找到了就绑上,找不到就报错。

绑定的时机在 snd_soc_register_card 调用时。核心会遍历每个 DAI link,根据 name 字段查找对应的设备。如果所有设备都 ready 了,就调用 soc_probe_link_dais 完成绑定。

这里有个常见的坑:时序问题。如果 Codec 驱动还没 probe 完,Machine 驱动就先 probe 了,绑定就会失败。解决办法有两种:

  • 延迟注册:在 Machine 的 probe 里用 schedule_work 延迟注册 card。
  • 设备依赖:在设备树里用 sound-dai 属性建立依赖关系。

我个人更推荐第二种。设备树里写清楚依赖,内核的驱动模型会自动处理顺序。比如:

sound {
    compatible = "my-board,audio";
    model = "MyBoard Audio";
    cpu-dai = &i2s1;
    codec-dai = &wm8960;
};

这样,内核会确保 i2s1 和 wm8960 都 probe 完了,再来 probe sound 节点。

关键点:绑定不是简单的「名字匹配」。ASoC 核心还会检查 DAI 的格式、时钟、PCM 能力是否匹配。如果 Codec 只支持 16bit,但 Platform 配了 24bit,绑定会失败。

3.5 回调函数 ops

最后聊聊 ops。这是 Machine 驱动里最灵活的部分。你可以在这里做任何板级特有的配置:

static int my_link_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *params)
{
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
    struct snd_soc_dai *codec_dai = rtd->codec_dai;
    int ret;

    /* 设置 CPU DAI 格式 */
    ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S |
                              SND_SOC_DAIFMT_NB_NF |
                              SND_SOC_DAIFMT_CBM_CFM);
    if (ret < 0)
        return ret;

    /* 设置 Codec DAI 格式 */
    ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S |
                              SND_SOC_DAIFMT_NB_NF |
                              SND_SOC_DAIFMT_CBM_CFM);
    if (ret < 0)
        return ret;

    /* 设置系统时钟 */
    ret = snd_soc_dai_set_sysclk(codec_dai, WM8960_SYSCLK_MCLK,
                                 12288000, SND_SOC_CLOCK_IN);
    if (ret < 0)
        return ret;

    return 0;
}

static struct snd_soc_ops my_link_ops = {
    .hw_params = my_link_hw_params,
};

这段代码做了三件事:设置 I2S 格式、配置时钟极性、提供 MCLK。这些都是板级相关的,不能写在 Codec 驱动里。

我记得有个项目,硬件工程师把 MCLK 从 12.288MHz 改成了 24.576MHz,但没告诉我。结果音频一直有杂音。查了半天才发现是时钟频率不对。从那以后,我每次都会在 hw_params 里打印出实际设置的时钟值,方便调试。

调试技巧:在 ops 回调里加 dev_dbg 打印,能看到每次音频流打开时配了什么参数。这对排查问题非常有帮助。

3.6 小结

Machine 驱动的内容就这些。说白了,它就是一张「接线图」,告诉 ASoC 核心怎么把 Codec 和 Platform 连起来。核心工作就是实例化 card、配好 DAI link、写好 ops 回调。

下一章我们会聊更高级的话题——DPCM 和动态路由。到时候你会看到,Machine 驱动还能做更复杂的事情。