4、音频编解码器驱动:Codec驱动框架、Codec初始化流程、Codec控制接口

好,我们进入第四章。音频编解码器驱动,简称 Codec 驱动。

说实话,Codec 是音频链路里最「个性」的器件。MCU 端的 I2S 接口大同小异,但 Codec 芯片,每家都有自己的脾气。你想想看,有的 Codec 要配置 50 多个寄存器,有的还带 DSP、带 EQ、带动态范围压缩。嗯,驱动写不好,声音出不来,或者出来全是噪音,这种事我见过太多次了。

4.1 Codec 驱动框架:Zephyr 怎么管 Codec?

Zephyr 的 Codec 驱动框架,说白了就是一套标准接口。它把 Codec 抽象成几个核心操作:初始化、配置、控制、关闭。

我个人习惯,先看设备树。Codec 在设备树里长这样:

/ {
    codec: wm8960@1a {
        compatible = "wlf,wm8960";
        reg = <0x1a>;
        #audio-codec-cells = <0>;
    };
};

驱动框架的核心数据结构是 audio_codec_api。每个 Codec 驱动都要实现这个结构体里的函数指针。我列一下关键成员:

API 函数 作用 我踩过的坑
configure 配置采样率、位宽、声道数 有些 Codec 必须先关再配,否则寄存器写不进去
start_input 启动 ADC 路径 启动顺序很重要,先开时钟再开模拟电路
start_output 启动 DAC 路径 静音脚没拉高,输出全是直流偏置
stop_input 停止 ADC 停止后要等几个 ms 再关电源,防止 pop 声
stop_output 停止 DAC 同上,先软静音再关
set_property 设置音量、静音、EQ 等 音量寄存器可能是双声道独立的,别只写一个

核心要点:Zephyr 的 Codec 驱动不直接操作 I2S 总线。它只负责配置 Codec 内部寄存器。I2S 的时钟和帧同步由另一个驱动管。两者通过音频时钟框架协同。

4.2 Codec 初始化流程:从上电到出声

Codec 初始化,我把它拆成 5 步。每一步都有讲究。

  1. 硬件复位:拉低复位引脚,保持至少 10ms。有些 Codec 内部有上电自检,复位时间不够会检测失败。
  2. 电源上电序列:先开数字电源,再开模拟电源。反过来可能会锁死 I2C 总线。我曾经因为这个排查了整整两天。
  3. 时钟配置:设置 PLL 分频,让 Codec 内部时钟与 MCLK 同步。这一步错了,后面所有配置都白搭。
  4. 音频接口配置:设置 I2S 格式、位宽、主从模式。这里要和 MCU 端的 I2S 配置完全一致。
  5. 模拟路径配置:打开 ADC/DAC、设置输入输出增益、解除静音。

代码里大概长这样:

static int wm8960_initialize(const struct device *dev)
{
    struct wm8960_data *data = dev->data;

    /* 1. 硬件复位 */
    gpio_pin_set(data->reset_gpio, 0);
    k_msleep(20);
    gpio_pin_set(data->reset_gpio, 1);
    k_msleep(10);

    /* 2. 电源上电 */
    wm8960_reg_write(dev, WM8960_PWR1, 0x00);  // 先关所有电源
    k_msleep(5);
    wm8960_reg_write(dev, WM8960_PWR1, 0xFE);  // 开数字部分

    /* 3. 时钟配置 */
    wm8960_reg_write(dev, WM8960_CLOCK1, 0x00); // MCLK = 12.288MHz

    /* 4. 音频接口 */
    wm8960_reg_write(dev, WM8960_IFACE1, 0x40); // I2S, 16bit, 从模式

    /* 5. 模拟路径 */
    wm8960_reg_write(dev, WM8960_LOUT1, 0x79);  // 左声道音量
    wm8960_reg_write(dev, WM8960_ROUT1, 0x79);  // 右声道音量

    return 0;
}

注意:初始化顺序不能乱。我见过有人先配模拟路径再配时钟,结果 Codec 直接不工作。因为内部 PLL 没锁定时,模拟电路的状态是不确定的。

4.3 Codec 控制接口:音量、静音、EQ 怎么调?

Codec 控制接口,说白了就是读写寄存器。但 Zephyr 把它封装成了 audio_codec_set_propertyaudio_codec_get_property

属性类型定义在 audio_codec_property 枚举里。常用的有:

  • AUDIO_CODEC_PROPERTY_OUTPUT_VOLUME:输出音量
  • AUDIO_CODEC_PROPERTY_INPUT_VOLUME:输入音量
  • AUDIO_CODEC_PROPERTY_MUTE:静音控制
  • AUDIO_CODEC_PROPERTY_AGC:自动增益控制

举个例子,调音量:

struct audio_codec_property volume = {
    .property_type = AUDIO_CODEC_PROPERTY_OUTPUT_VOLUME,
    .value = 50,  // 0-100 百分比
};

audio_codec_set_property(codec_dev, &volume);

这里有个细节。不同 Codec 的音量寄存器映射不一样。有的用线性值,有的用 dB 值。Zephyr 的框架要求驱动内部做转换。比如 WM8960,音量寄存器 0-127 对应 -73dB 到 +6dB。驱动里要写个映射表。

我的经验:静音控制最好用硬件引脚实现,而不是只写寄存器。因为有些 Codec 在系统休眠后寄存器会丢失,但硬件引脚状态不会变。这样能避免唤醒后突然爆音。

EQ 和动态范围压缩(DRC)属于高级控制。Zephyr 的框架没有直接定义这些属性,但你可以通过 AUDIO_CODEC_PROPERTY_VENDOR 扩展。我一般会在驱动里加一个 set_eq_coefficients 函数,然后在 set_property 里根据 vendor 属性 ID 分发。

嗯,说到这里,我想起一个坑。有些 Codec 的 EQ 系数是 24 位有符号数,但寄存器是 16 位的。需要分两次写入。而且写入顺序不能错,否则 EQ 滤波器会瞬间产生一个很大的增益,把喇叭烧了。我曾经在量产前发现这个问题,吓出一身冷汗。

4.4 调试技巧:Codec 不工作怎么办?

Codec 驱动写完了,上电没声音。别慌,按这个顺序排查:

  1. I2C 通信正常吗?用逻辑分析仪抓一下,看 Codec 有没有 ACK。
  2. 时钟对吗?用示波器量 MCLK 和 LRCLK。LRCLK 应该是采样率,比如 48kHz。
  3. 寄存器读回来对吗?写一个值,再读回来,看是不是一致。
  4. 模拟路径通了吗?用手摸 Codec 的输入引脚,听喇叭有没有嗡嗡声。

我曾经遇到一个 case,所有寄存器读写都正常,就是没声音。最后发现是 Codec 的 HP_L 和 HP_R 引脚默认是浮空,需要配置成输出模式。嗯,这种坑,数据手册里往往写得很隐晦。

好了,这一章就到这里。Codec 驱动是音频系统里最贴近硬件的部分,也是问题最多的地方。下一章我们讲音频管道,看看数据从 Codec 出来之后怎么走。