4、Codec驱动:Codec抽象层、snd_soc_dai_driver、snd_soc_codec_driver、控件与kcontrol注册

好,咱们今天来聊聊Codec驱动。说实话,这是ASoC子系统里最「接地气」的一块。你想想看,CPU和DMA把音频数据搬过来了,最后总得有个东西把数字信号变成模拟声音吧?这个活儿,就是Codec干的。

在ASoC框架里,Codec被抽象成了两层:一个是硬件描述层,一个是控制接口层。我刚开始接触时也犯过迷糊,总觉得这两层分得太细。后来踩了几个坑才明白——这种设计,说白了就是为了让驱动代码能复用。你换一颗Codec芯片,只需要改硬件描述部分,控制逻辑基本不用动。

4.1 Codec抽象层:把硬件「翻译」给内核听

Codec抽象层干的事,就是把芯片手册里的寄存器、引脚、音频接口这些物理概念,翻译成内核能理解的数据结构。核心就是两个结构体:snd_soc_dai_driver 和 snd_soc_codec_driver。

我个人习惯先看 snd_soc_dai_driver。它描述的是Codec的数字音频接口(DAI),也就是跟CPU那边对接的物理层。比如采样率支持哪些、格式支持哪些、时钟极性怎么配。

static struct snd_soc_dai_driver my_codec_dai = {
    .name = "my-codec-hifi",
    .playback = {
        .stream_name = "Playback",
        .channels_min = 2,
        .channels_max = 2,
        .rates = SNDRV_PCM_RATE_8000_48000,
        .formats = SNDRV_PCM_FMTBIT_S16_LE,
    },
    .capture = {
        .stream_name = "Capture",
        .channels_min = 1,
        .channels_max = 2,
        .rates = SNDRV_PCM_RATE_8000_48000,
        .formats = SNDRV_PCM_FMTBIT_S16_LE,
    },
    .ops = &my_codec_dai_ops,
};

你看,这个结构体把Codec能干什么事说得清清楚楚。我在项目中遇到过一个问题:某颗Codec的Playback支持48kHz,但Capture只支持16kHz。当时没仔细看手册,直接复制了Playback的参数给Capture,结果录音全是噪声。嗯,这个坑我替你们踩过了。

注意:rates 和 formats 用的是位掩码,不是数值。别写成 48000,要写成 SNDRV_PCM_RATE_48000。我见过有人直接写数字,编译不报错,但运行时内核根本不认。

4.2 snd_soc_codec_driver:Codec的「身份证」

接下来是 snd_soc_codec_driver。这个结构体描述的是Codec芯片本身的属性,比如有多少个寄存器、读写回调函数、偏置电压控制等。

static struct snd_soc_codec_driver my_codec_driver = {
    .probe = my_codec_probe,
    .remove = my_codec_remove,
    .suspend = my_codec_suspend,
    .resume = my_codec_resume,
    .controls = my_codec_controls,
    .num_controls = ARRAY_SIZE(my_codec_controls),
    .dapm_widgets = my_codec_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(my_codec_dapm_widgets),
    .dapm_routes = my_codec_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(my_codec_dapm_routes),
};

这里有个关键点:controls 和 dapm_widgets 是分开注册的。为什么?因为控件(kcontrol)是给用户空间用的,比如调节音量、切换输入源。而DAPM widget是给内核电源管理用的,控制Codec内部哪些模块上电、哪些下电。

我曾经犯过一个错误:把音量控制写成了DAPM widget。结果每次调音量,内核都以为要切换电源状态,搞得Codec频繁上下电,声音断断续续。你想想看,这多尴尬。

4.3 控件与kcontrol注册:让用户能「拧旋钮」

控件,也就是kcontrol,是Codec驱动里最贴近用户的部分。用户空间的工具(比如alsamixer、amixer)就是通过kcontrol来调节音量、切换通道的。

注册kcontrol有两种方式:一种是手动定义 snd_kcontrol_new 结构体,另一种是用宏定义。我个人推荐用宏,代码更简洁,也不容易出错。

static const struct snd_kcontrol_new my_codec_controls[] = {
    SOC_SINGLE("Master Playback Volume", MY_CODEC_REG_VOL, 0, 255, 0),
    SOC_SINGLE("Master Capture Volume", MY_CODEC_REG_CAP_VOL, 0, 255, 0),
    SOC_DOUBLE("Headphone Playback Volume", MY_CODEC_REG_HP_VOL,
               0, 8, 127, 0),
    SOC_ENUM("Input Source", my_codec_input_enum),
};

你看,SOC_SINGLE 是单通道控件,SOC_DOUBLE 是双通道(左右声道独立控制),SOC_ENUM 是枚举选择(比如切换麦克风还是线路输入)。

小技巧:控件的名字最好跟芯片手册保持一致。比如手册里叫"Master Volume",你就别自己改成"Main Volume"。否则用户看alsamixer时一脸懵,找不到对应的控制项。

注册过程其实很简单。在 snd_soc_codec_driver 里填好 .controls 和 .num_controls,ASoC框架会在probe阶段自动帮你注册。但要注意一点:控件的注册顺序会影响它们在用户空间的排列。我习惯把常用的控件(比如主音量)放在前面,不常用的(比如数字滤波器模式)放在后面。

4.4 实战中的避坑指南

好,最后分享几个我实际项目中踩过的坑,希望能帮你省点时间。

  • 寄存器地址别搞错:Codec芯片的寄存器地址通常是8位或16位,但有些芯片的地址是分页的。我遇到过一颗芯片,音量寄存器在Page 0,但静音控制却在Page 1。如果你不切换页面,写进去的数据根本不对。
  • 控件范围要匹配:SOC_SINGLE 的最后一个参数是偏移量(shift),中间那个是最大值。比如音量寄存器是8位,最大值255,但实际芯片只用了低7位(0-127)。如果你写成255,用户调到128以上时,芯片可能产生爆音。
  • 别忘了DAPM路由:控件注册好了,但如果没有对应的DAPM路由,音频路径可能不通。比如你注册了"Headphone Playback Volume",但没把DAPM widget连到输出引脚上,耳机就是不出声。

我曾经在一个项目里,花了整整两天排查耳机无声的问题。最后发现,DAPM路由里少了一条从DAC到HP_OUT的连接。加上去之后,声音就出来了。嗯,这种低级错误,犯过一次就再也不会忘了。

总结一下:Codec驱动其实不难,关键是理解抽象层的设计思路。把硬件描述和控制逻辑分开,代码才能干净、可维护。下一章咱们聊聊DAPM,那才是ASoC的精髓所在。