4. Codec驱动(上):snd_soc_codec_driver 结构体、Codec寄存器读写、控件(kcontrol)注册

好,咱们进入Codec驱动的世界。这部分内容,说白了就是让内核认识你的音频编解码芯片。我刚开始接触ASoC时,觉得这部分最繁琐——寄存器多、控件多、还得跟DAPM打交道。但别怕,咱们一步步拆解。

4.1 snd_soc_codec_driver 结构体:Codec的“身份证”

每个Codec驱动,核心就是填充一个 snd_soc_codec_driver 结构体。它告诉内核:我这个芯片叫什么、能干啥、怎么读写寄存器。

来看一个典型的定义:

static struct snd_soc_codec_driver soc_codec_dev_wm8974 = {
    .probe = wm8974_probe,
    .remove = wm8974_remove,
    .suspend = wm8974_suspend,
    .resume = wm8974_resume,
    .read = wm8974_read,
    .write = wm8974_write,
    .reg_cache_size = WM8974_CACHE_SIZE,
    .reg_word_size = sizeof(u16),
    .reg_cache_default = wm8974_reg_defaults,
    .controls = wm8974_snd_controls,
    .num_controls = ARRAY_SIZE(wm8974_snd_controls),
    .dapm_widgets = wm8974_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(wm8974_dapm_widgets),
    .dapm_routes = wm8974_dapm_routes,
    .num_dapm_routes = ARRAY_SIZE(wm8974_dapm_routes),
};

这里有几个关键字段,我挑重要的说:

  • probe/remove:驱动加载和卸载时的回调。我习惯在这里做硬件初始化,比如复位Codec、设置默认寄存器值。
  • read/write:寄存器读写函数。这是Codec驱动的核心,后面会细讲。
  • reg_cache_*:寄存器缓存。为什么需要缓存?因为有些Codec不支持读操作,或者读操作很慢。缓存可以帮你快速获取寄存器值。
  • controls:控件数组。音量控制、开关、混音器都在这里定义。
  • dapm_widgets/dapm_routes:DAPM相关的组件和路径。这部分下一章会详细展开。
我的经验: 填充这个结构体时,别漏了 reg_cache_default。我曾经在一个项目中忘了提供默认寄存器值,结果Codec上电后状态完全不对,查了两天才发现是缓存初始化出了问题。

4.2 Codec寄存器读写:打通“任督二脉”

寄存器读写是Codec驱动的基础。你想想看,所有音频参数的调整——音量、采样率、滤波器——最终都要落实到寄存器操作上。

典型的读写函数长这样:

static int wm8974_write(struct snd_soc_codec *codec, unsigned int reg, unsigned int value)
{
    u8 data[2];
    int ret;

    data[0] = (reg & 0xff) << 1;  // 7位寄存器地址,左移1位
    data[1] = value & 0xff;        // 8位数据

    ret = i2c_master_send(codec->control_data, data, 2);
    if (ret != 2) {
        dev_err(codec->dev, "Failed to write register %u\n", reg);
        return -EIO;
    }

    return 0;
}

static unsigned int wm8974_read(struct snd_soc_codec *codec, unsigned int reg)
{
    u8 addr = (reg & 0xff) << 1;
    u8 value;
    int ret;

    ret = i2c_master_send(codec->control_data, &addr, 1);
    if (ret != 1) {
        dev_err(codec->dev, "Failed to send register address\n");
        return -EIO;
    }

    ret = i2c_master_recv(codec->control_data, &value, 1);
    if (ret != 1) {
        dev_err(codec->dev, "Failed to read register\n");
        return -EIO;
    }

    return value;
}

这里有几个要点:

  • 通信接口:I2C还是SPI?取决于你的Codec。上面例子是I2C的,SPI的话用 spi_write_then_read 之类的函数。
  • 地址格式:有些Codec的寄存器地址是7位,有些是8位,甚至16位。一定要看数据手册。
  • 错误处理:读写失败要返回错误码,别默默吞掉。我见过太多驱动在读写失败时直接返回0,导致上层以为操作成功了。
注意: 有些Codec的读操作需要先写地址再读数据,中间可能有延迟。我曾经在一个项目中遇到读操作总是返回0xFF,后来发现是读时序中少了一个ACK等待周期。

4.3 控件(kcontrol)注册:让用户能调音量

控件,说白了就是用户空间能操作的“旋钮”和“开关”。在ASoC中,控件通过 snd_kcontrol_new 结构体定义,然后注册到Codec驱动中。

来看一个音量控件的例子:

static const struct snd_kcontrol_new wm8974_snd_controls[] = {
    SOC_DOUBLE_R_TLV("Speaker Playback Volume",
        WM8974_SPKVOL1, WM8974_SPKVOL2,
        0, 127, 0, speaker_tlv),
    SOC_SINGLE("Speaker Playback Switch",
        WM8974_SPKVOL1, 6, 1, 0),
    SOC_DOUBLE_R_TLV("Headphone Playback Volume",
        WM8974_HPVOL1, WM8974_HPVOL2,
        0, 127, 0, hphone_tlv),
    SOC_SINGLE("Mic Boost Switch",
        WM8974_MICBOOST, 0, 1, 0),
};

这些宏是ASoC提供的“快捷方式”:

宏名称 用途 参数说明
SOC_SINGLE 单个开关或数值控件 名称、寄存器、位移、最大值、反转标志
SOC_DOUBLE 双声道控件(左右声道独立) 名称、左寄存器、右寄存器、位移、最大值、反转标志
SOC_DOUBLE_R_TLV 带音量映射的双声道控件 名称、左寄存器、右寄存器、位移、最大值、反转标志、TLV数组
SOC_SINGLE_TLV 带音量映射的单声道控件 名称、寄存器、位移、最大值、反转标志、TLV数组

TLV(Type-Length-Value)数组是干嘛的?它定义了音量值和分贝值之间的映射关系。举个例子:

static const DECLARE_TLV_DB_SCALE(speaker_tlv, -5700, 100, 0);

这表示:寄存器值0对应-57.00dB,每增加1步进增加1.00dB。最后一个参数0表示不是反转的(有些Codec音量值越大声音越小)。

关键点: 控件名称的命名有讲究。用户空间的音频管理器(如ALSA的amixer)会按名称查找控件。我习惯用“Playback Volume”、“Capture Switch”这样的命名,符合ALSA的惯例。

4.4 控件注册的完整流程

控件定义好了,怎么注册到系统中?流程其实很简单:

  1. 在 snd_soc_codec_driver 中设置 .controls 和 .num_controls。
  2. 在 probe 函数中调用 snd_soc_add_codec_controls()。
  3. ASoC框架会自动将这些控件注册到ALSA核心中。

嗯,这里要注意:有些驱动会在 probe 里手动调用 snd_soc_add_controls(),但更推荐的方式是直接填充结构体,让框架帮你做。少写代码,少出bug。

我曾经在一个项目中,为了图省事,把所有控件都放在一个数组里,结果发现有些控件需要根据硬件版本动态添加。后来我改成了在 probe 里根据版本号条件注册控件,这样更灵活。

4.5 避坑指南:控件注册的常见问题

做Codec驱动时,控件这块容易踩几个坑:

  • 控件名称重复:ALSA要求控件名称唯一。如果你有两个同名的控件,第二个会注册失败。我习惯在名称前加前缀,比如“Left Speaker Volume”和“Right Speaker Volume”。
  • 寄存器位宽搞错:有些Codec的寄存器是16位,但控件只操作其中几位。一定要用掩码和位移正确提取。
  • TLV数组定义错误:音量映射搞反了,用户调大音量结果声音变小。这种bug很难查,因为从代码上看完全正确。
  • 忽略缓存同步:如果你用了寄存器缓存,修改控件后要记得同步到硬件。否则用户调了音量,实际没变化。
调试技巧: 用 amixer contents 命令可以查看所有已注册的控件及其当前值。如果发现控件没出现,先检查 num_controls 是否设置正确,再检查 probe 函数是否成功执行。

好了,这一章的内容就到这里。Codec驱动的核心就是三件事:定义结构体、实现读写函数、注册控件。下一章我们会深入DAPM,看看那些自动化的电源管理是怎么实现的。