第一部分 WM8960 Codec 驱动

第一章 Codec 驱动在系统架构中的位置

WM8960 是一款低功耗立体声 Codec,广泛应用于嵌入式音频系统。它通过 I2C 总线 与主 SoC 通信,通过 I2S 接口 与 CPU DAI 传输音频数据。

在 Linux 5.10 中,WM8960 驱动位于 sound/soc/codecs/wm8960.c,其架构位置如下:

[用户空间] → [ALSA 核心] → [ASoC 框架] → [Machine 驱动] → [Platform 驱动 (I2S)] → [Codec 驱动 (WM8960)]
                                                                      ↑
                                                                   [I2C 总线]
                                                              [Linux I2C 核心]
                                                                      ↑
                                                                 [WM8960 硬件]

第二章 核心数据结构

2.1 wm8960_priv:Codec 私有数据结构

/**
 * @struct wm8960_priv
 * @brief WM8960 Codec 的私有数据结构。
 *        管理 I2C 通信、寄存器缓存、电源状态和音频配置。
 */
struct wm8960_priv {
    struct snd_soc_codec *codec;     /**< 指向 ASoC Codec 抽象 */
    struct i2c_client *client;      /**< 指向 I2C 客户端 */
    struct regmap *regmap;           /**< 寄存器映射核心 */
    u16 cache[WM8960_REG_MAX];       /**< 寄存器缓存数组 */
    int control;                     /**< 控制路径状态 */
    u32 pll_in;                      /**< PLL 输入频率 */
    u32 pll_out;                     /**< PLL 输出频率 */
    int sysclk;                      /**< 系统时钟频率 */
    int bclk;                        /**< 位时钟频率 */
    int lrclk;                       /**< 帧时钟频率 */
    unsigned int playback_active:1;  /**< 播放流激活标志 */
    unsigned int capture_active:1;   /**< 捕获流激活标志 */
    struct mutex mutex;              /**< 并发控制互斥锁 */
};
​
/**
 * @struct wm8960_pll_config
 * @brief WM8960 PLL 配置结构。
 */
struct wm8960_pll_config {
    unsigned int freq_in;            /**< PLL 输入频率 */
    unsigned int freq_out;           /**< PLL 输出频率 */
    unsigned int pll_div;            /**< PLL 分频值 */
    unsigned int pll_int;            /**< PLL 整数分频 */
};
​
/**
 * @struct wm8960_clock_pll
 * @brief 时钟路径配置结构。
 */
struct wm8960_clock_pll {
    struct wm8960_pll_config pll_cfg;   /**< PLL 配置 */
    unsigned int sysclk;                /**< 系统时钟 */
    unsigned int bclk;                  /**< 位时钟 */
    unsigned int lrclk;                 /**< 帧时钟 */
};

2.2 WM8960 寄存器映射

/* 寄存器地址定义 (WM8960 数据手册) */
#define WM8960_LEFT_INPUT_VOLUME       0x00 /**< 左声道输入音量 */
#define WM8960_RIGHT_INPUT_VOLUME      0x01 /**< 右声道输入音量 */
#define WM8960_LEFT_OUTPUT_VOLUME      0x02 /**< 左声道输出音量 */
#define WM8960_RIGHT_OUTPUT_VOLUME     0x03 /**< 右声道输出音量 */
#define WM8960_LOUT1_VOLUME            0x04 /**< LOUT1 音量 */
#define WM8960_ROUT1_VOLUME            0x05 /**< ROUT1 音量 */
#define WM8960_LOUT2_VOLUME            0x06 /**< LOUT2 音量 */
#define WM8960_ROUT2_VOLUME            0x07 /**< ROUT2 音量 */
#define WM8960_POWER_MANAGEMENT_1      0x08 /**< 电源管理 1 */
#define WM8960_POWER_MANAGEMENT_2      0x09 /**< 电源管理 2 */
#define WM8960_POWER_MANAGEMENT_3      0x0A /**< 电源管理 3 */
#define WM8960_AUDIO_INTERFACE_1       0x0B /**< 音频接口 1 */
#define WM8960_AUDIO_INTERFACE_2       0x0C /**< 音频接口 2 */
#define WM8960_CLOCKING_1              0x0D /**< 时钟控制 1 */
#define WM8960_CLOCKING_2              0x0E /**< 时钟控制 2 */
#define WM8960_ADDITIONAL_CONTROL_1    0x0F /**< 附加控制 1 */
#define WM8960_ADDITIONAL_CONTROL_2    0x10 /**< 附加控制 2 */
#define WM8960_ADDITIONAL_CONTROL_3    0x11 /**< 附加控制 3 */
#define WM8960_ADDITIONAL_CONTROL_4    0x12 /**< 附加控制 4 */
#define WM8960_DAC_VOLUME              0x13 /**< DAC 音量 */
#define WM8960_LEFT_ADC_VOLUME         0x14 /**< 左 ADC 音量 */
#define WM8960_RIGHT_ADC_VOLUME        0x15 /**< 右 ADC 音量 */
#define WM8960_DAC_INVERT              0x16 /**< DAC 反转 */
#define WM8960_DAC_CTRL                0x17 /**< DAC 控制 */
#define WM8960_GPIO_CONFIG             0x18 /**< GPIO 配置 */
#define WM8960_JD_CONFIG               0x19 /**< 插孔检测配置 */
#define WM8960_JD_INVERT               0x1A /**< 插孔检测反转 */
#define WM8960_LOUT1_MIXER             0x1B /**< LOUT1 混音器 */
#define WM8960_ROUT1_MIXER             0x1C /**< ROUT1 混音器 */
#define WM8960_SPEAKER_MIXER           0x1D /**< 扬声器混音器 */
#define WM8960_ADDITIONAL_CONTROL_5    0x1E /**< 附加控制 5 */
#define WM8960_ADDITIONAL_CONTROL_6    0x1F /**< 附加控制 6 */
#define WM8960_ALC_CONTROL             0x20 /**< ALC 控制 */
#define WM8960_LEFT_INPUT_MIXER        0x21 /**< 左输入混音器 */
#define WM8960_RIGHT_INPUT_MIXER       0x22 /**< 右输入混音器 */
#define WM8960_LEFT_OUTPUT_MIXER       0x23 /**< 左输出混音器 */
#define WM8960_RIGHT_OUTPUT_MIXER      0x24 /**< 右输出混音器 */
#define WM8960_LEFT_ADC_OUTPUT_MIXER   0x25 /**< 左 ADC 输出混音器 */
#define WM8960_RIGHT_ADC_OUTPUT_MIXER  0x26 /**< 右 ADC 输出混音器 */
#define WM8960_REG_MAX                 0x27 /**< 寄存器总数 */

2.3 wm8960_control:混音器控制描述符

/**
 * @struct wm8960_control
 * @brief 混音器/音量控制描述符,用于向 ASoC 核心注册。
 */
static const struct snd_kcontrol_new wm8960_controls[] = {
    SOC_DOUBLE_R("Headphone Volume", WM8960_LOUT1_VOLUME, WM8960_ROUT1_VOLUME,
                 0, 127, 0),
    SOC_DOUBLE_R("Speaker Volume", WM8960_LOUT2_VOLUME, WM8960_ROUT2_VOLUME,
                 0, 127, 0),
    SOC_SINGLE("DAC Playback Volume", WM8960_DAC_VOLUME, 0, 255, 0),
    SOC_SINGLE("Left Input Volume", WM8960_LEFT_INPUT_VOLUME, 0, 31, 0),
    SOC_SINGLE("Right Input Volume", WM8960_RIGHT_INPUT_VOLUME, 0, 31, 0),
    SOC_DOUBLE_R("ADC Capture Volume", WM8960_LEFT_ADC_VOLUME,
                 WM8960_RIGHT_ADC_VOLUME, 0, 255, 0),
    SOC_SINGLE("Mic Boost", WM8960_LEFT_INPUT_VOLUME, 6, 3, 0),
};

第三章 核心功能实现

3.1 I2C 读写与寄存器操作

/**
 * @brief 通过 I2C 写入 WM8960 寄存器。
 *
 * @param codec 指向 snd_soc_codec。
 * @param reg 寄存器地址。
 * @param val 写入值。
 * @return 0 成功,负数错误。
 */
static int wm8960_write_reg(struct snd_soc_codec *codec, u8 reg, u16 val)
{
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    u8 msg[3] = {reg, (val >> 8) & 0xFF, val & 0xFF};
    int ret;
​
    // 1. 通过 I2C 传输数据
    ret = i2c_master_send(wm8960->client, msg, 3);
    if (ret != 3) {
        dev_err(&wm8960->client->dev, "I2C write failed\n");
        return -EIO;
    }
​
    // 2. 更新缓存
    wm8960->cache[reg] = val;
    return 0;
}
​
/**
 * @brief 通过 I2C 读取 WM8960 寄存器。
 *
 * @param codec 指向 snd_soc_codec。
 * @param reg 寄存器地址。
 * @param val 输出寄存器值。
 * @return 0 成功,负数错误。
 */
static int wm8960_read_reg(struct snd_soc_codec *codec, u8 reg, u16 *val)
{
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    u8 msg[1] = {reg};
    u8 buf[2];
    int ret;
​
    // 1. 发送寄存器地址
    ret = i2c_master_send(wm8960->client, msg, 1);
    if (ret != 1) {
        dev_err(&wm8960->client->dev, "I2C write address failed\n");
        return -EIO;
    }
​
    // 2. 读取数据
    ret = i2c_master_recv(wm8960->client, buf, 2);
    if (ret != 2) {
        dev_err(&wm8960->client->dev, "I2C read failed\n");
        return -EIO;
    }
​
    *val = (buf[0] << 8) | buf[1];
    return 0;
}
​
/**
 * @brief 使用 regmap 批量初始化寄存器。
 *
 * @param wm8960 指向 wm8960_priv。
 * @param reglist 寄存器初始化表。
 * @param num 表项数量。
 */
static void wm8960_init_regs(struct wm8960_priv *wm8960,
                             const struct reg_default *reglist,
                             int num)
{
    int i;
    for (i = 0; i < num; i++) {
        wm8960_write_reg(wm8960->codec, reglist[i].reg, reglist[i].def);
    }
}
​
/**
 * @brief 寄存器初始化表 (数据手册推荐值)。
 */
static const struct reg_default wm8960_reg_defaults[] = {
    { WM8960_LEFT_INPUT_VOLUME, 0x0017 },
    { WM8960_RIGHT_INPUT_VOLUME, 0x0017 },
    { WM8960_LEFT_OUTPUT_VOLUME, 0x0000 },
    { WM8960_RIGHT_OUTPUT_VOLUME, 0x0000 },
    { WM8960_LOUT1_VOLUME, 0x0079 },
    { WM8960_ROUT1_VOLUME, 0x0079 },
    { WM8960_LOUT2_VOLUME, 0x0079 },
    { WM8960_ROUT2_VOLUME, 0x0079 },
    { WM8960_POWER_MANAGEMENT_1, 0x0000 },
    { WM8960_POWER_MANAGEMENT_2, 0x0000 },
    { WM8960_POWER_MANAGEMENT_3, 0x0000 },
    { WM8960_AUDIO_INTERFACE_1, 0x0004 },
    { WM8960_AUDIO_INTERFACE_2, 0x0000 },
    { WM8960_CLOCKING_1, 0x0000 },
    { WM8960_CLOCKING_2, 0x0000 },
    { WM8960_ADDITIONAL_CONTROL_1, 0x0000 },
    { WM8960_ADDITIONAL_CONTROL_2, 0x0000 },
    { WM8960_ADDITIONAL_CONTROL_3, 0x0000 },
    { WM8960_ADDITIONAL_CONTROL_4, 0x0000 },
    { WM8960_DAC_VOLUME, 0x00C0 },
    { WM8960_LEFT_ADC_VOLUME, 0x00C0 },
    { WM8960_RIGHT_ADC_VOLUME, 0x00C0 },
    { WM8960_DAC_INVERT, 0x0000 },
    { WM8960_DAC_CTRL, 0x0000 },
    { WM8960_GPIO_CONFIG, 0x0000 },
    { WM8960_JD_CONFIG, 0x0000 },
    { WM8960_JD_INVERT, 0x0000 },
    { WM8960_LOUT1_MIXER, 0x0000 },
    { WM8960_ROUT1_MIXER, 0x0000 },
    { WM8960_SPEAKER_MIXER, 0x0000 },
    { WM8960_ADDITIONAL_CONTROL_5, 0x0000 },
    { WM8960_ADDITIONAL_CONTROL_6, 0x0000 },
    { WM8960_ALC_CONTROL, 0x0000 },
    { WM8960_LEFT_INPUT_MIXER, 0x0000 },
    { WM8960_RIGHT_INPUT_MIXER, 0x0000 },
    { WM8960_LEFT_OUTPUT_MIXER, 0x0000 },
    { WM8960_RIGHT_OUTPUT_MIXER, 0x0000 },
    { WM8960_LEFT_ADC_OUTPUT_MIXER, 0x0000 },
    { WM8960_RIGHT_ADC_OUTPUT_MIXER, 0x0000 },
};

3.2 电源管理

/**
 * @brief 启用 WM8960 电源。
 *
 * @param codec 指向 snd_soc_codec。
 * @param on 1 启用,0 禁用。
 * @return 0 成功。
 */
static int wm8960_set_power(struct snd_soc_codec *codec, int on)
{
    u16 pwr1, pwr2, pwr3;
​
    if (on) {
        // 1. 启用所有必要的电源域
        pwr1 = WM8960_PWR1_REF | WM8960_PWR1_DAC | WM8960_PWR1_ADC |
               WM8960_PWR1_LOUT1 | WM8960_PWR1_ROUT1;
        pwr2 = WM8960_PWR2_LIN | WM8960_PWR2_RIN | WM8960_PWR2_LMIC |
               WM8960_PWR2_RMIC;
        pwr3 = WM8960_PWR3_SPK_L | WM8960_PWR3_SPK_R;
​
        wm8960_write_reg(codec, WM8960_POWER_MANAGEMENT_1, pwr1);
        wm8960_write_reg(codec, WM8960_POWER_MANAGEMENT_2, pwr2);
        wm8960_write_reg(codec, WM8960_POWER_MANAGEMENT_3, pwr3);
    } else {
        // 2. 关闭所有电源域
        wm8960_write_reg(codec, WM8960_POWER_MANAGEMENT_1, 0);
        wm8960_write_reg(codec, WM8960_POWER_MANAGEMENT_2, 0);
        wm8960_write_reg(codec, WM8960_POWER_MANAGEMENT_3, 0);
    }
​
    return 0;
}

3.3 音频路径配置

/**
 * @brief 配置音频接口参数 (I2S 格式、主/从模式、采样率)。
 *
 * @param codec 指向 snd_soc_codec。
 * @param params 指向 snd_pcm_hw_params。
 * @return 0 成功。
 */
static int wm8960_hw_params(struct snd_pcm_substream *substream,
                            struct snd_pcm_hw_params *params,
                            struct snd_soc_dai *dai)
{
    struct snd_soc_codec *codec = dai->codec;
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    u16 iface1, iface2, clock1, clock2;
    u32 rate = params_rate(params);
    u32 channels = params_channels(params);
    u32 bits = snd_pcm_format_width(params_format(params));
​
    // 1. 配置音频接口格式 (I2S 模式,主模式)
    iface1 = WM8960_IF1_MODE_I2S | WM8960_IF1_MASTER;
    if (bits == 16)
        iface1 |= WM8960_IF1_WL_16BIT;
    else if (bits == 24)
        iface1 |= WM8960_IF1_WL_24BIT;
    else if (bits == 32)
        iface1 |= WM8960_IF1_WL_32BIT;
    wm8960_write_reg(codec, WM8960_AUDIO_INTERFACE_1, iface1);
​
    // 2. 计算 BCLK 分频
    u32 bclk_freq = rate * channels * bits;
    u32 mclk_freq = wm8960->sysclk;
    u32 bclk_div = mclk_freq / bclk_freq;
    iface2 = (bclk_div & 0xF) << 8;
    wm8960_write_reg(codec, WM8960_AUDIO_INTERFACE_2, iface2);
​
    // 3. 配置采样率分频 (LRCLK = BCLK / 通道数 / 位宽)
    u32 lrclk_div = bclk_freq / (channels * bits);
    clock1 = (lrclk_div & 0x1F) << 4;
    wm8960_write_reg(codec, WM8960_CLOCKING_1, clock1);
​
    // 4. 启用时钟
    clock2 = WM8960_CLK2_BCLK | WM8960_CLK2_LRCLK;
    wm8960_write_reg(codec, WM8960_CLOCKING_2, clock2);
​
    wm8960->bclk = bclk_freq;
    wm8960->lrclk = bclk_freq / (channels * bits);
    return 0;
}
​
/**
 * @brief 设置 DAI 格式 (I2S、Left-Justified 等)。
 *
 * @param codec 指向 snd_soc_codec。
 * @param fmt 格式配置。
 * @return 0 成功。
 */
static int wm8960_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
    struct snd_soc_codec *codec = dai->codec;
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    u16 iface1 = 0;
​
    // 1. 设置 I2S 格式
    switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
    case SND_SOC_DAIFMT_I2S:
        iface1 = WM8960_IF1_MODE_I2S;
        break;
    case SND_SOC_DAIFMT_LEFT_J:
        iface1 = WM8960_IF1_MODE_LEFT_J;
        break;
    case SND_SOC_DAIFMT_RIGHT_J:
        iface1 = WM8960_IF1_MODE_RIGHT_J;
        break;
    case SND_SOC_DAIFMT_DSP_A:
        iface1 = WM8960_IF1_MODE_DSP_A;
        break;
    case SND_SOC_DAIFMT_DSP_B:
        iface1 = WM8960_IF1_MODE_DSP_B;
        break;
    default:
        return -EINVAL;
    }
​
    // 2. 设置主从模式
    switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
    case SND_SOC_DAIFMT_CBS_CFS:
        iface1 |= WM8960_IF1_MASTER;
        break;
    case SND_SOC_DAIFMT_CBM_CFM:
        iface1 |= WM8960_IF1_SLAVE;
        break;
    default:
        return -EINVAL;
    }
​
    wm8960_write_reg(codec, WM8960_AUDIO_INTERFACE_1, iface1);
    return 0;
}

3.4 混音器与路由配置

/**
 * @brief 配置音频路由路径。
 *        使用 ASoC 的 DAPM 框架定义连接关系。
 */
static const struct snd_soc_dapm_widget wm8960_dapm_widgets[] = {
    /* 输入路径 */
    SND_SOC_DAPM_INPUT("LIN"),
    SND_SOC_DAPM_INPUT("RIN"),
    SND_SOC_DAPM_INPUT("MIC1"),
    SND_SOC_DAPM_INPUT("MIC2"),
​
    /* 输出路径 */
    SND_SOC_DAPM_OUTPUT("LOUT1"),
    SND_SOC_DAPM_OUTPUT("ROUT1"),
    SND_SOC_DAPM_OUTPUT("LOUT2"),
    SND_SOC_DAPM_OUTPUT("ROUT2"),
​
    /* DAC/ADC */
    SND_SOC_DAPM_DAC("DAC", "Playback", WM8960_POWER_MANAGEMENT_1, 1, 0),
    SND_SOC_DAPM_ADC("ADC", "Capture", WM8960_POWER_MANAGEMENT_1, 2, 0),
​
    /* 混音器 */
    SND_SOC_DAPM_MIXER("Left Output Mixer", WM8960_LEFT_OUTPUT_MIXER, 0, 0, NULL, 0),
    SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_RIGHT_OUTPUT_MIXER, 0, 0, NULL, 0),
};
​
static const struct snd_soc_dapm_route wm8960_audio_map[] = {
    /* 路径: LIN -> 左输入混音器 -> 混音器 -> LOUT1 */
    {"Left Output Mixer", "LIN", "LIN"},
    {"LOUT1", NULL, "Left Output Mixer"},
​
    /* DAC -> 输出混音器 */
    {"Left Output Mixer", "DAC", "DAC"},
    {"LOUT1", NULL, "Left Output Mixer"},
​
    /* 麦克风路径 */
    {"ADC", NULL, "MIC1"},
    {"Right Output Mixer", "RIN", "RIN"},
    {"ROUT1", NULL, "Right Output Mixer"},
};

3.5 触发播放/捕获

/**
 * @brief 启动或停止音频传输。
 *
 * @param substream 指向 snd_pcm_substream。
 * @param cmd 触发命令。
 * @param dai 指向 snd_soc_dai。
 * @return 0 成功。
 */
static int wm8960_trigger(struct snd_pcm_substream *substream, int cmd,
                          struct snd_soc_dai *dai)
{
    struct snd_soc_codec *codec = dai->codec;
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    u16 pwr1;
​
    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
        // 启动时确保电源已启用
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
            wm8960->playback_active = 1;
            pwr1 = read_reg(codec, WM8960_POWER_MANAGEMENT_1);
            pwr1 |= WM8960_PWR1_DAC | WM8960_PWR1_DAC_LRCLK |
                    WM8960_PWR1_DAC_BCLK | WM8960_PWR1_DAC_PLL;
            write_reg(codec, WM8960_POWER_MANAGEMENT_1, pwr1);
        } else {
            wm8960->capture_active = 1;
            pwr1 = read_reg(codec, WM8960_POWER_MANAGEMENT_1);
            pwr1 |= WM8960_PWR1_ADC | WM8960_PWR1_ADC_BCLK |
                    WM8960_PWR1_ADC_LRCLK;
            write_reg(codec, WM8960_POWER_MANAGEMENT_1, pwr1);
        }
        break;
​
    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
            wm8960->playback_active = 0;
            pwr1 = read_reg(codec, WM8960_POWER_MANAGEMENT_1);
            pwr1 &= ~(WM8960_PWR1_DAC | WM8960_PWR1_DAC_LRCLK |
                      WM8960_PWR1_DAC_BCLK | WM8960_PWR1_DAC_PLL);
            write_reg(codec, WM8960_POWER_MANAGEMENT_1, pwr1);
        } else {
            wm8960->capture_active = 0;
            pwr1 = read_reg(codec, WM8960_POWER_MANAGEMENT_1);
            pwr1 &= ~(WM8960_PWR1_ADC | WM8960_PWR1_ADC_BCLK |
                      WM8960_PWR1_ADC_LRCLK);
            write_reg(codec, WM8960_POWER_MANAGEMENT_1, pwr1);
        }
        break;
​
    default:
        return -EINVAL;
    }
​
    return 0;
}

3.6 Codec 驱动注册 (Probe 函数)

/**
 * @brief WM8960 I2C 驱动 Probe 函数。
 *        由 I2C 核心在匹配成功后调用。
 *
 * @param client 指向 i2c_client。
 * @param id 指向 i2c_device_id。
 * @return 0 成功。
 */
static int wm8960_probe(struct i2c_client *client)
{
    struct wm8960_priv *wm8960;
    struct snd_soc_codec *codec;
    int ret;
​
    // 1. 分配私有数据
    wm8960 = devm_kzalloc(&client->dev, sizeof(*wm8960), GFP_KERNEL);
    if (!wm8960)
        return -ENOMEM;
    i2c_set_clientdata(client, wm8960);
    wm8960->client = client;
    mutex_init(&wm8960->mutex);
​
    // 2. 初始化 regmap
    wm8960->regmap = devm_regmap_init_i2c(client, &wm8960_regmap_config);
    if (IS_ERR(wm8960->regmap)) {
        dev_err(&client->dev, "Failed to init regmap\n");
        return PTR_ERR(wm8960->regmap);
    }
​
    // 3. 读取芯片 ID 验证硬件
    ret = regmap_read(wm8960->regmap, 0x00, &val);
    if (ret) {
        dev_err(&client->dev, "Failed to read chip ID\n");
        return ret;
    }
    if (val != WM8960_ID) {
        dev_err(&client->dev, "Invalid chip ID 0x%04x\n", val);
        return -ENODEV;
    }
​
    // 4. 初始化寄存器缓存
    wm8960_init_regs(wm8960, wm8960_reg_defaults, ARRAY_SIZE(wm8960_reg_defaults));
​
    // 5. 注册 ASoC Codec 设备
    ret = snd_soc_register_codec(&client->dev, &soc_codec_dev_wm8960,
                                 wm8960_dai, ARRAY_SIZE(wm8960_dai));
    if (ret) {
        dev_err(&client->dev, "Failed to register codec\n");
        return ret;
    }
​
    dev_info(&client->dev, "WM8960 codec registered at I2C address 0x%02x\n",
             client->addr);
    return 0;
}
​
/**
 * @brief WM8960 移除函数。
 *
 * @param client 指向 i2c_client。
 */
static int wm8960_remove(struct i2c_client *client)
{
    snd_soc_unregister_codec(&client->dev);
    return 0;
}
​
/* I2C 设备 ID 表 */
static const struct i2c_device_id wm8960_id[] = {
    { "wm8960", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, wm8960_id);
​
/* I2C 驱动结构 */
static struct i2c_driver wm8960_i2c_driver = {
    .driver = {
        .name = "wm8960",
        .of_match_table = wm8960_of_match,
    },
    .probe = wm8960_probe,
    .remove = wm8960_remove,
    .id_table = wm8960_id,
};
​
module_i2c_driver(wm8960_i2c_driver);

第四章 调试核心难点

4.1 I2C 通信失败

现象dmesg 显示 "I2C write failed",Codec 无法访问。

原因

  • I2C 地址错误(应改为 0x1A 或 0x2A)。

  • I2C 总线时钟频率过高(应 ≤ 400kHz)。

  • Codec 未上电。

调试方法

  1. 检查 I2C 地址

    i2cdetect -y <bus>
  2. 强制设置频率

    echo 100000 > /sys/class/i2c-adapter/i2c-<bus>/power/rate
  3. 使用逻辑分析仪:观察 SDA/SCL 信号。

4.2 音频无声

现象aplay 正常,但耳机/扬声器无声。

原因

  • 电源域未完全启用。

  • 音量设为 0。

  • 音频路由路径未正确配置。

调试方法

  1. 检查电源寄存器

    devmem2 <i2c_addr>+0x08  # WM8960_POWER_MANAGEMENT_1
    devmem2 <i2c_addr>+0x09  # WM8960_POWER_MANAGEMENT_2
  2. 检查音量

    amixer sset 'Headphone' 80%
    amixer sset 'Speaker' 80%
  3. 检查路由

    # 查看 DAPM 状态
    cat /sys/kernel/debug/asoc/wm8960/DAPM/status

4.3 采样率不匹配导致卡顿

现象:音频播放时出现爆音或卡顿。

原因

  • BCLK/LRCLK 分频计算错误。

  • PLL 未正确锁定。

  • 主时钟频率不匹配。

调试方法

  1. 检查时钟寄存器

    devmem2 <i2c_addr>+0x0D  # WM8960_CLOCKING_1
    devmem2 <i2c_addr>+0x0E  # WM8960_CLOCKING_2
  2. 测量 BCLK/LRCLK:使用示波器。

  3. 强制使用固定分频

    # 在驱动中硬编码分频值

第五章 与其他控制器的协同

控制器 协同方式 调试关键点
I2C 控制器 配置 Codec 寄存器 地址匹配、时钟频率
I2S 控制器 传输音频数据 格式匹配、BCLK/LRCLK 同步
GPIO 控制器 控制 Codec 复位 复位脉冲宽度
Power 管理 上下电时序 电源等待时间
时钟控制器 提供 MCLK 时钟 时钟源频率精度

第二部分 从 snd_card 注册到 PCM 设备暴露

第一章 ALSA 核心架构全景

ALSA (Advanced Linux Sound Architecture) 是 Linux 内核的音频子系统。它为用户空间提供统一的 API,将复杂的硬件控制抽象为标准的设备节点。

1.1 ALSA 核心组件

[用户空间] -> [alsa-lib] -> [字符设备 (/dev/snd/...)] -> [ALSA 核心] -> [硬件驱动]
                 |
            +----+----+
            | 声卡 (snd_card)  |
            | - 最多 32 张卡 |
            +----+----+
                 |
                 +---> PCM 设备 (snd_pcm) --------> [播放/捕获流]
                 +---> 控制设备 (snd_ctl) --------> [音量/开关]
                 +---> 时序设备 (snd_timer) ------> [定时器]
                 +---> 原始 MIDI (snd_rawmidi) ---> [MIDI]
                 +---> 混音器 (snd_mixer) --------> [混音器]

1.2 ALSA 核心数据结构

这是 ALSA 核心中最基础的数据结构,是驱动与核心的契约

/**
 * @struct snd_card
 * @brief ALSA 声卡的核心抽象。
 *        每个物理声卡对应一个 snd_card,由驱动分配。
 *        它作为所有子设备 (PCM, Control) 的容器。
 */
struct snd_card {
    int number;                      /**< 声卡编号 (0-31) */
    char id[16];                     /**< 声卡 ID (如 "hw:0") */
    char driver[32];                 /**< 驱动名称 */
    char shortname[32];              /**< 简短名称 */
    char longname[80];               /**< 长名称 */
    struct list_head devices;        /**< 子设备列表 (PCM, Control, etc.) */
    struct snd_pcm *pcms;            /**< PCM 设备指针 */
    unsigned int pcm_count;          /**< PCM 设备数量 */
    void *private_data;              /**< 驱动私有数据 */
    struct device *dev;              /**< 关联的设备 */
    struct module *module;           /**< 所属内核模块 */
};
​
/**
 * @struct snd_pcm
 * @brief PCM 设备抽象,代表一个 PCM 接口 (如 hw:0,0)。
 *        包含播放和捕获流。
 */
struct snd_pcm {
    struct snd_card *card;           /**< 所属声卡 */
    struct device *dev;              /**< 设备指针 */
    struct snd_pcm_str streams[2];   /**< 播放流 (0) 和 捕获流 (1) */
    unsigned int device;             /**< 设备编号 (0-31) */
    char *name;                      /**< 设备名称 */
};
​
/**
 * @struct snd_pcm_str
 * @brief 代表一个 PCM 流 (播放或捕获)。
 *        包含一个或多个 substream (如多声道)。
 */
struct snd_pcm_str {
    struct snd_pcm_substream *substream; /**< 子流列表 */
    unsigned int substream_count;        /**< 子流数量 */
    unsigned int direction;              /**< 方向:SNDRV_PCM_STREAM_PLAYBACK 或 SNDRV_PCM_STREAM_CAPTURE */
};
​
/**
 * @struct snd_pcm_substream
 * @brief 代表一个 PCM 子流,是驱动操作的核心。
 *        驱动通过 ops 回调处理音频数据。
 */
struct snd_pcm_substream {
    struct snd_pcm_str *pstr;        /**< 所属的 PCM 流 */
    struct snd_pcm_ops *ops;         /**< 驱动操作回调 */
    struct snd_pcm_runtime *runtime; /**< 运行时状态 (缓冲区大小, 格式, 指针) */
    struct device *dev;              /**< 设备指针 */
    void *private_data;              /**< 驱动私有数据 */
};
​
/**
 * @struct snd_pcm_ops
 * @brief PCM 驱动操作回调,驱动必须实现这些函数。
 *        这是硬件与 ALSA 核心交互的桥梁。
 */
struct snd_pcm_ops {
    int (*open)(struct snd_pcm_substream *substream);
    int (*close)(struct snd_pcm_substream *substream);
    int (*hw_params)(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params);
    int (*hw_free)(struct snd_pcm_substream *substream);
    int (*prepare)(struct snd_pcm_substream *substream);
    int (*trigger)(struct snd_pcm_substream *substream, int cmd);
    snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
    int (*copy)(struct snd_pcm_substream *substream, int channel, snd_pcm_uframes_t pos, void __user *buf, snd_pcm_uframes_t count);
    struct page *(*page)(struct snd_pcm_substream *substream, unsigned long offset);
};

1.3 核心注册流程

[驱动 Probe 函数]
    ↓
1. snd_card_new() 分配声卡结构体
    ↓
2. snd_pcm_new() 创建 PCM 设备
    ↓
3. snd_pcm_set_ops() 设置 PCM 操作回调
    ↓
4. snd_pcm_lib_preallocate_pages() 预分配 DMA 缓冲区
    ↓
5. snd_card_register() 注册声卡并创建设备节点
    ↓
[系统生成 /dev/snd/pcmC0D0p /dev/snd/controlC0 等节点]
    ↓
[用户空间通过 alsa-lib 访问]

第二章 纯 ALSA 驱动核心代码实现

为了展示最完整的“首要流程”,我编写一个简单的虚拟声卡驱动。它不依赖任何硬件,只展示如何通过 ALSA 核心 API 注册自己的功能。这个驱动是理解 ALSA 核心的最佳范例。

2.1 虚拟声卡驱动头文件

/**
 * @file virtual_alsa_driver.c
 * @brief 纯 ALSA 虚拟声卡驱动,用于展示从驱动到 ALSA 核心的注册流程。
 */
​
#include <linux/module.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/slab.h>
#include <sound/core.h>
#include <sound/pcm.h>
​
/**
 * @struct virtual_chip
 * @brief 声卡的私有数据结构,驱动需要管理的所有状态。
 */
struct virtual_chip {
    struct snd_card *card;          /**< ALSA 声卡核心结构 */
    struct snd_pcm *pcm;            /**< PCM 设备 */
    struct snd_pcm_substream *substream; /**< 当前子流 */
    unsigned char *buffer;          /**< 音频缓冲区 (模拟硬件内存) */
    size_t buffer_size;             /**< 缓冲区大小 */
    size_t period_size;             /**< 周期大小 */
    unsigned int channels;          /**< 通道数 */
    unsigned int rate;              /**< 采样率 */
    unsigned int format;            /**< 采样格式 */
};
​
/**
 * @enum virtual_states
 * @brief 虚拟声卡的状态机,模拟硬件状态。
 */
enum virtual_states {
    VIRTUAL_STATE_STOPPED = 0,
    VIRTUAL_STATE_PLAYING,
    VIRTUAL_STATE_PAUSED,
};

2.2 核心 PCM 操作实现

/**
 * @brief 打开 PCM 设备,分配资源。
 *
 * @param substream 指向 snd_pcm_substream。
 * @return 0 成功。
 */
static int virtual_open(struct snd_pcm_substream *substream)
{
    struct virtual_chip *chip = snd_pcm_substream_chip(substream);
    int ret;
​
    // 1. 检查是否被独占 (模拟硬件)
    if (chip->substream)
        return -EBUSY;
​
    // 2. 分配 DMA 缓冲区 (通过 ALSA 核心 API)
    ret = snd_pcm_lib_malloc_pages(substream, 64 * 1024);
    if (ret < 0) {
        dev_err(substream->device->dev, "Failed to allocate buffer\n");
        return ret;
    }
​
    // 3. 保存状态
    chip->substream = substream;
    chip->buffer = substream->runtime->dma_area;
    chip->buffer_size = substream->runtime->dma_bytes;
​
    // 4. 设置硬件约束
    ret = snd_pcm_hw_constraint_integer(substream->runtime, SNDRV_PCM_HW_PARAM_CHANNELS);
    if (ret < 0) {
        dev_err(substream->device->dev, "Failed to set constraint\n");
        return ret;
    }
​
    return 0;
}
​
/**
 * @brief 关闭 PCM 设备,释放资源。
 *
 * @param substream 指向 snd_pcm_substream。
 * @return 0 成功。
 */
static int virtual_close(struct snd_pcm_substream *substream)
{
    struct virtual_chip *chip = snd_pcm_substream_chip(substream);
​
    chip->substream = NULL;
    chip->buffer = NULL;
    snd_pcm_lib_free_pages(substream);
​
    return 0;
}
​
/**
 * @brief 配置硬件参数 (格式、速率、通道、缓冲区大小)。
 *
 * @param substream 指向 snd_pcm_substream。
 * @param params 指向 snd_pcm_hw_params。
 * @return 0 成功。
 */
static int virtual_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *params)
{
    struct virtual_chip *chip = snd_pcm_substream_chip(substream);
    unsigned int buffer_size = params_buffer_bytes(params);
    int ret;
​
    // 1. 检查参数范围 (通常由约束保证)
    if (buffer_size > 64 * 1024) {
        dev_err(substream->device->dev, "Buffer size too large\n");
        return -EINVAL;
    }
​
    // 2. 分配实际的 DMA 缓冲区 (已经分配,只需要设置大小)
    ret = snd_pcm_lib_malloc_pages(substream, buffer_size);
    if (ret < 0) {
        dev_err(substream->device->dev, "Failed to allocate buffer\n");
        return ret;
    }
​
    // 3. 保存参数
    chip->buffer_size = buffer_size;
    chip->period_size = params_period_size(params);
    chip->channels = params_channels(params);
    chip->rate = params_rate(params);
    chip->format = snd_pcm_format_physical_width(params_format(params));
​
    return 0;
}
​
/**
 * @brief 释放硬件参数配置。
 *
 * @param substream 指向 snd_pcm_substream。
 * @return 0 成功。
 */
static int virtual_hw_free(struct snd_pcm_substream *substream)
{
    return snd_pcm_lib_free_pages(substream);
}
​
/**
 * @brief 准备数据传输,重置状态。
 *
 * @param substream 指向 snd_pcm_substream。
 * @return 0 成功。
 */
static int virtual_prepare(struct snd_pcm_substream *substream)
{
    struct virtual_chip *chip = snd_pcm_substream_chip(substream);
    (void)chip; // 在模拟中,这里可以设置硬件
    return 0;
}
​
/**
 * @brief 触发 PCM 传输的开始/停止/暂停。
 *
 * @param substream 指向 snd_pcm_substream。
 * @param cmd 触发命令。
 * @return 0 成功。
 */
static int virtual_trigger(struct snd_pcm_substream *substream, int cmd)
{
    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
        pr_info("Virtual: Audio playing started\n");
        break;
    case SNDRV_PCM_TRIGGER_STOP:
        pr_info("Virtual: Audio playing stopped\n");
        break;
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        pr_info("Virtual: Audio paused\n");
        break;
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        pr_info("Virtual: Audio resumed\n");
        break;
    default:
        return -EINVAL;
    }
    return 0;
}
​
/**
 * @brief 获取当前 PCM 指针位置。
 *
 * @param substream 指向 snd_pcm_substream。
 * @return 当前缓冲区中的帧偏移量。
 */
static snd_pcm_uframes_t virtual_pointer(struct snd_pcm_substream *substream)
{
    // 返回一个模拟位置,这里只是循环演示
    static snd_pcm_uframes_t pos = 0;
    struct virtual_chip *chip = snd_pcm_substream_chip(substream);
    size_t period_frames = chip->period_size;
​
    // 模拟位置移动
    pos = (pos + 1) % (chip->buffer_size / 4); // 假设 16-bit 每帧 4 字节
    if (pos % period_frames == 0) {
        snd_pcm_period_elapsed(substream); // 通知周期完成
    }
    return pos;
}
​
/**
 * @brief 复制数据到/从用户空间。
 *
 * @param substream 指向 snd_pcm_substream。
 * @param channel 当前通道。
 * @param pos 缓冲区中的位置。
 * @param buf 用户空间缓冲区指针。
 * @param count 复制大小 (字节)。
 * @return 0 成功。
 */
static int virtual_copy_user(struct snd_pcm_substream *substream,
                             int channel, snd_pcm_uframes_t pos,
                             void __user *buf, snd_pcm_uframes_t count)
{
    struct virtual_chip *chip = snd_pcm_substream_chip(substream);
    snd_pcm_uframes_t frame_size = (snd_pcm_uframes_t)(chip->format * chip->channels / 8);
    snd_pcm_uframes_t offset = pos * frame_size;
    u8 *hw_addr = chip->buffer + offset;
    snd_pcm_uframes_t copy_len = count * frame_size;
​
    if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
        // 从用户空间复制到硬件缓冲区
        if (copy_to_user(buf, hw_addr, copy_len) != 0) {
            return -EFAULT;
        }
    } else {
        // 从硬件缓冲区复制到用户空间
        if (copy_from_user(hw_addr, buf, copy_len) != 0) {
            return -EFAULT;
        }
    }
    return 0;
}

2.3 PCM 操作结构体注册

/**
 * @brief PCM 硬件约束结构,定义支持的范围。
 */
static const struct snd_pcm_hardware virtual_hardware = {
    .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
             SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID),
    .formats = (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE),
    .rates = (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 | SNDRV_PCM_RATE_16000 |
              SNDRV_PCM_RATE_22050 | SNDRV_PCM_RATE_32000 | SNDRV_PCM_RATE_44100 |
              SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000),
    .rate_min = 8000,
    .rate_max = 96000,
    .channels_min = 1,
    .channels_max = 2,
    .buffer_bytes_max = 64 * 1024,
    .period_bytes_min = 256,
    .period_bytes_max = 64 * 1024,
    .periods_min = 2,
    .periods_max = 32,
};
​
/**
 * @brief PCM 操作回调结构体,指向具体的实现函数。
 */
static struct snd_pcm_ops virtual_pcm_ops = {
    .open = virtual_open,
    .close = virtual_close,
    .hw_params = virtual_hw_params,
    .hw_free = virtual_hw_free,
    .prepare = virtual_prepare,
    .trigger = virtual_trigger,
    .pointer = virtual_pointer,
    .copy_user = virtual_copy_user,
};

2.4 声卡注册核心流程 (Probe)

/**
 * @brief 虚拟声卡驱动的 Probe 函数。
 *        这是 ALSA 驱动注册的“首要流程”核心。
 *
 * @param pdev Platform 设备指针。
 * @return 0 成功。
 */
static int virtual_audio_probe(struct platform_device *pdev)
{
    struct snd_card *card = NULL;
    struct virtual_chip *chip = NULL;
    struct snd_pcm *pcm = NULL;
    int ret;
​
    dev_info(&pdev->dev, "Probing virtual audio device\n");
​
    // 第一步:创建声卡结构体 (snd_card_new)
    // 这是 ALSA 核心的入口点,分配一个 snd_card 实例。
    ret = snd_card_new(&pdev->dev, -1, "virtual", THIS_MODULE,
                       sizeof(struct virtual_chip), &card);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to create new sound card\n");
        return ret;
    }
​
    // 获取驱动私有数据
    chip = card->private_data;
    chip->card = card;
​
    // 第二步:创建 PCM 设备 (snd_pcm_new)
    // 创建一个 PCM 设备,编号 0,支持播放和捕获。
    ret = snd_pcm_new(card, "Virtual Audio Device", 0, 1, 1, &pcm);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to create PCM device\n");
        goto error;
    }
    chip->pcm = pcm;
​
    // 第三步:设置 PCM 操作回调 (snd_pcm_set_ops)
    // 关联驱动实现的 ops 函数与 PCM 设备。
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &virtual_pcm_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &virtual_pcm_ops);
​
    // 第四步:设置 PCM 硬件信息 (可选,通过 runtime 设置)
    // 这一步将 hardware 结构体与 PCM runtime 关联。
    snd_pcm_set_hw_ops(pcm, &virtual_hardware);
​
    // 第五步:预分配 DMA 缓冲区 (snd_pcm_lib_preallocate_pages)
    // 为 PCM 设备预分配 DMA 缓冲区空间。
    ret = snd_pcm_lib_preallocate_pages(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                                        SNDRV_DMA_TYPE_DEV, &pdev->dev,
                                        64 * 1024, 64 * 1024);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to preallocate playback buffer\n");
        goto error;
    }
​
    ret = snd_pcm_lib_preallocate_pages(pcm, SNDRV_PCM_STREAM_CAPTURE,
                                        SNDRV_DMA_TYPE_DEV, &pdev->dev,
                                        64 * 1024, 64 * 1024);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to preallocate capture buffer\n");
        goto error;
    }
​
    // 设置卡名等元数据
    strcpy(card->driver, "Virtual");
    strcpy(card->shortname, "Virtual Audio Device");
    strcpy(card->longname, "Virtual Audio Device for ALSA Core Demo");
​
    // 第六步:注册声卡 (snd_card_register)
    // 这是使声卡对用户空间可见的关键操作。
    // 执行后,/dev/snd/pcmC0D0p 等设备节点将被创建。
    ret = snd_card_register(card);
    if (ret < 0) {
        dev_err(&pdev->dev, "Failed to register sound card\n");
        goto error;
    }
​
    dev_info(&pdev->dev, "Virtual audio device registered as card%d\n", card->number);
    return 0;
​
error:
    snd_card_free(card);
    return ret;
}

2.5 移除函数

/**
 * @brief 虚拟声卡驱动的 Remove 函数。
 *
 * @param pdev Platform 设备指针。
 * @return 0 成功。
 */
static int virtual_audio_remove(struct platform_device *pdev)
{
    struct snd_card *card = platform_get_drvdata(pdev);
​
    if (!card) {
        dev_err(&pdev->dev, "No sound card data found\n");
        return -EINVAL;
    }
​
    // 注销声卡,释放所有资源 (包括 PCM 和控制设备)
    snd_card_free(card);
    platform_set_drvdata(pdev, NULL);
​
    dev_info(&pdev->dev, "Virtual audio device removed\n");
    return 0;
}

2.6 驱动入口

static struct platform_driver virtual_audio_driver = {
    .driver = {
        .name = "virtual_audio_driver",
    },
    .probe = virtual_audio_probe,
    .remove = virtual_audio_remove,
};
​
static int __init virtual_audio_init(void)
{
    return platform_driver_register(&virtual_audio_driver);
}
​
static void __exit virtual_audio_exit(void)
{
    platform_driver_unregister(&virtual_audio_driver);
}
​
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Pure ALSA Virtual Audio Driver Demo - First Flow");

第三章 用户空间验证

在加载驱动后,用户空间可以通过以下方式访问:

3.1 检查设备节点

ls -l /dev/snd/
# 预期输出: controlC0, pcmC0D0p, pcmC0D0c, timer 等

3.2 使用 ALSA 工具

# 列出设备
cat /proc/asound/cards
# 输出: 0 [Virtual        ]: Virtual - Virtual Audio Device
​
# 测试播放
aplay -D hw:0,0 /usr/share/sounds/alsa/Noise.wav
​
# 测试录制
arecord -d 5 -f cd -t raw /tmp/test.raw

3.3 使用 ALSA 调试文件系统

cat /proc/asound/card0/pcm0p/sub0/hw_params
cat /proc/asound/card0/pcm0p/sub0/sw_params

第四章 ASoC 与纯 ALSA 的关系

4.1 关系图

+-----------------------------------------+
| 用户空间 (alsa-lib, aplay, arecord)      |
+-----------------------------------------+
           ^
           |
+-----------------------------------------+
| ALSA 核心 (ALSA Core)                    |
| - snd_card_new                           |
| - snd_pcm_new                            |
| - snd_card_register                      |
+-----------------------------------------+
           ^
           |
+------------------------+     +------------------------+
| 纯 ALSA 驱动           |     | ASoC 框架 (ASoC)      |
| - 直接使用 core API    |     | - snd_soc_card        |
| - 适合简单设备         |     | - snd_soc_dai         |
|                        |     | - snd_soc_codec       |
|                        |     | - snd_soc_pcm         |
+------------------------+     +------------------------+

4.2 ASoC 封装的核心逻辑

ASoC 框架并没有替换 ALSA 核心,而是封装了它。ASoC 的 snd_soc_register_card 内部最终会调用 ALSA 核心 API:

// 伪代码:ASoC 内部实现
int snd_soc_register_card(struct snd_soc_card *card) {
    struct snd_card *snd_card;
    int ret;
​
    // 1. 调用 snd_card_new
    ret = snd_card_new(card->dev, card->num, card->driver_name,
                       THIS_MODULE, 0, &snd_card);
    if (ret < 0) return ret;
​
    // 2. 遍历所有 dai_link,创建 PCM 设备
    for (i = 0; i < card->num_links; i++) {
        ret = snd_pcm_new(snd_card, link->name, i, 1, 1, &pcm);
        // 设置 snd_pcm_ops
        snd_pcm_set_ops(pcm, ... , &soc_pcm_ops);
    }
​
    // 3. 注册声卡
    ret = snd_card_register(snd_card);
    // ...
}

第五章 纯 ALSA 驱动调试核心难点

5.1 声卡未注册

现象cat /proc/asound/cards 没有输出。

原因

  • snd_card_new 失败。

  • snd_card_register 被过早调用(之前未分配足够资源)。

调试方法

  1. 检查日志

    dmesg | grep snd
  2. 使用 perf 跟踪

    perf record -e snd_* -a -- modprobe virtual_audio_driver
  3. 检查约束:确保 snd_pcm_new 成功。

5.2 PCM 访问失败

现象aplay 输出 aplay: main:788: audio open error: No such file or directory

原因

  • PCM 设备未创建。

  • 设备节点未生成。

调试方法

  1. 检查设备节点

    ls -l /dev/snd/pcm*
  2. 检查 snd_card_register

    cat /proc/devices | grep pcm
  3. 直接查看注册的声卡

    cat /sys/class/sound/card*/device/uevent

5.3 纯 ALSA vs ASoC:何时选择

场景 选择 理由
简单虚拟设备 纯 ALSA 代码少,直接控制
嵌入式 SoC ASoC 标准框架,代码复用
PCI 音频卡 纯 ALSA 参考 snd-dummy
USB 音频 USB-Audio 已有驱动

第六章 与其他框架的协同

框架 协同方式 调试关键点
snd_dmaengine_pcm 通过 DMA 传输音频数据 缓冲区预分配、DMA 地址
snd_timer 音频定时器 周期中断、精度
snd_ctl 混音器控制 音量、开关、选择
snd_mixer 混音器抽象 路由、电平

第三部分 I2S 控制器与 ASoC 音频子系统

第一章 I2S 控制器在 Platform Bus 中的位置

I2S (Inter-IC Sound) 是用于传输数字音频数据的串行总线接口。在 SoC 中,I2S 控制器通常作为 Platform 设备挂载在内部总线上,负责将内存中的 PCM 音频数据转换为 I2S 格式发送到外部 Codec,或从 Codec 接收音频数据。

在 Linux 5.10 中,音频子系统使用 ASoC (ALSA System on Chip) 框架,分为三层:

  1. Machine 驱动:描述整个音频系统,将 CPU DAI 和 Codec DAI 绑定。

  2. Platform 驱动(本篇文章重点):即 I2S 控制器驱动,作为 CPU DAI。

  3. Codec 驱动:外部音频 Codec 芯片驱动。

1.1 硬件关键概念

  • I2S 接口:支持主/从模式,支持多种帧格式 (I2S, Left-Justified, Right-Justified, DSP_A/B)。

  • 采样率:支持 8kHz ~ 192kHz。

  • 数据位宽:支持 16/24/32 位。

  • DMA 引擎:支持双通道 (播放和录音) 音频数据传输。

  • 时钟:需要 mclk (主时钟),bclk (位时钟),lrclk (帧时钟)。


第二章 核心数据结构

2.1 snd_soc_dai_driver:CPU DAI 描述符

/**
 * @struct snd_soc_dai_driver
 * @brief 描述 CPU DAI (I2S 控制器) 的能力和操作。
 *        用于向 ASoC 核心注册。
 */
struct snd_soc_dai_driver {
    const char *name;               /**< DAI 名称,如 "rk3288-i2s" */
    struct snd_soc_dai_ops *ops;    /**< DAI 操作回调 (hw_params, set_fmt, trigger) */
    struct snd_soc_dai_playback playback; /**< 播放能力 */
    struct snd_soc_dai_capture capture;   /**< 录音能力 */
    struct snd_soc_dai_ops *ops;    /**< DAI 操作回调 */
};

2.2 snd_soc_dai_ops:DAI 操作回调

/**
 * @struct snd_soc_dai_ops
 * @brief DAI 操作回调函数集合。
 *        对 I2S 控制器的核心控制接口。
 */
struct snd_soc_dai_ops {
    int (*hw_params)(struct snd_pcm_substream *, struct snd_pcm_hw_params *, struct snd_soc_dai *);
    int (*trigger)(struct snd_pcm_substream *, int cmd, struct snd_soc_dai *);
    int (*set_fmt)(struct snd_soc_dai *, unsigned int fmt);
    int (*set_clkdiv)(struct snd_soc_dai *, int div_id, int div);
    int (*startup)(struct snd_pcm_substream *, struct snd_soc_dai *);
    void (*shutdown)(struct snd_pcm_substream *, struct snd_soc_dai *);
};

2.3 i2s_priv:I2S 控制器私有数据结构

/**
 * @struct i2s_priv
 * @brief Rockchip I2S 控制器的私有数据结构。
 *        管理硬件寄存器、时钟、DMA 通道和运行时状态。
 */
struct i2s_priv {
    void __iomem *base;             /**< 映射后的寄存器基址 */
    int irq;                        /**< 中断号 */
    struct clk *mclk;               /**< 主时钟 */
    struct clk *hclk;               /**< 总线时钟 */
    struct clk *bclk;               /**< 位时钟 (可选) */
    struct snd_dmaengine_dai_dma_data dma_data_tx; /**< 发送 DMA 参数 */
    struct snd_dmaengine_dai_dma_data dma_data_rx; /**< 接收 DMA 参数 */
    struct snd_soc_dai *dai;        /**< 关联的 DAI */
    spinlock_t lock;                /**< 硬件保护锁 */
    u32 version;                    /**< I2S 版本号 */
    u32 sample_rate;                /**< 当前采样率 */
    u32 channels;                   /**< 当前通道数 */
    u32 bclk_ratio;                 /**< BCLK/LRCK 比率 */
};

2.4 I2S 寄存器映射

/* 寄存器偏移量 (Rockchip I2S) */
#define I2S_CTRL                    0x00
#define I2S_TX_DATA                 0x04
#define I2S_RX_DATA                 0x08
#define I2S_STATUS                  0x0C
#define I2S_INT_EN                  0x10
#define I2S_INT_STAT                0x14
#define I2S_CFG                     0x18
#define I2S_CLK_CFG                 0x1C
#define I2S_DMA_CTRL                0x20
#define I2S_DMA_STATUS              0x24
#define I2S_VERSION                 0x28
​
/* 控制寄存器位定义 */
#define I2S_CTRL_ENABLE             (1 << 0)  /**< I2S 使能 */
#define I2S_CTRL_TX_MODE            (1 << 1)  /**< 发送模式 */
#define I2S_CTRL_RX_MODE            (1 << 2)  /**< 接收模式 */
#define I2S_CTRL_I2S_MODE           (0 << 3)  /**< I2S 模式 */
#define I2S_CTRL_LEFT_J_MODE        (1 << 3)  /**< 左对齐模式 */
#define I2S_CTRL_MASTER_MODE        (1 << 4)  /**< 主模式 */
#define I2S_CTRL_SLAVE_MODE         (0 << 4)  /**< 从模式 */
#define I2S_CTRL_16BIT              (0 << 5)  /**< 16 位 */
#define I2S_CTRL_24BIT              (1 << 5)  /**< 24 位 */
#define I2S_CTRL_32BIT              (2 << 5)  /**< 32 位 */

第三章 核心代码实现

3.1 I2S 硬件初始化

/**
 * @brief 初始化 I2S 控制器硬件。
 *
 * @param i2s 指向 i2s_priv 结构。
 * @return 0 成功。
 */
static int i2s_hw_init(struct i2s_priv *i2s)
{
    u32 ctrl;
​
    // 1. 检查 I2S 版本
    i2s->version = readl(i2s->base + I2S_VERSION) & 0xFF;
    dev_info(i2s->dai->dev, "I2S version %d\n", i2s->version);
​
    // 2. 复位 I2S
    ctrl = readl(i2s->base + I2S_CTRL);
    ctrl &= ~(I2S_CTRL_ENABLE);
    writel(ctrl, i2s->base + I2S_CTRL);
​
    // 3. 配置为 I2S 模式,主模式,16 位
    ctrl = I2S_CTRL_I2S_MODE | I2S_CTRL_MASTER_MODE | I2S_CTRL_16BIT;
    writel(ctrl, i2s->base + I2S_CTRL);
​
    // 4. 配置 BCLK 比率 (默认 32 * 2 = 64 BCLK/LRCK)
    writel(64, i2s->base + I2S_CLK_CFG);
​
    // 5. 启用 DMA 中断
    writel(0x3, i2s->base + I2S_INT_EN);
​
    return 0;
}

3.2 配置音频参数

/**
 * @brief 配置音频参数 (采样率、位宽、格式)。
 *
 * @param i2s 指向 i2s_priv 结构。
 * @param params 指向 snd_pcm_hw_params。
 * @return 0 成功。
 */
static int i2s_hw_params(struct snd_pcm_substream *substream,
                         struct snd_pcm_hw_params *params,
                         struct snd_soc_dai *dai)
{
    struct i2s_priv *i2s = snd_soc_dai_get_drvdata(dai);
    u32 ctrl, clk_cfg;
    u32 rate = params_rate(params);
    u32 channels = params_channels(params);
    u32 bits = snd_pcm_format_width(params_format(params));
​
    // 1. 计算 BCLK 频率 = 采样率 * 通道数 * 位宽
    u32 bclk_rate = rate * channels * bits;
​
    // 2. 设置主时钟频率 (假设 MCLK = 12.288MHz)
    clk_set_rate(i2s->mclk, 12288000);
​
    // 3. 配置 BCLK 比率
    u32 ratio = 12288000 / bclk_rate;
    writel(ratio, i2s->base + I2S_CLK_CFG);
​
    // 4. 配置控制寄存器
    ctrl = readl(i2s->base + I2S_CTRL);
    ctrl &= ~(I2S_CTRL_16BIT | I2S_CTRL_24BIT | I2S_CTRL_32BIT);
    if (bits == 16)
        ctrl |= I2S_CTRL_16BIT;
    else if (bits == 24)
        ctrl |= I2S_CTRL_24BIT;
    else if (bits == 32)
        ctrl |= I2S_CTRL_32BIT;
​
    if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
        ctrl |= I2S_CTRL_TX_MODE;
    else
        ctrl |= I2S_CTRL_RX_MODE;
​
    writel(ctrl, i2s->base + I2S_CTRL);
​
    // 5. 保存当前配置
    i2s->sample_rate = rate;
    i2s->channels = channels;
​
    return 0;
}

3.3 触发启动/停止

/**
 * @brief 触发 I2S 传输的启动或停止。
 *
 * @param substream 指向 snd_pcm_substream。
 * @param cmd 触发命令 (START, STOP, PAUSE, RESUME)。
 * @param dai 指向 snd_soc_dai。
 * @return 0 成功。
 */
static int i2s_trigger(struct snd_pcm_substream *substream,
                       int cmd, struct snd_soc_dai *dai)
{
    struct i2s_priv *i2s = snd_soc_dai_get_drvdata(dai);
    u32 ctrl;
    int ret = 0;
​
    ctrl = readl(i2s->base + I2S_CTRL);
​
    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
    case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
        ctrl |= I2S_CTRL_ENABLE;
        writel(ctrl, i2s->base + I2S_CTRL);
        break;
    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
    case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
        ctrl &= ~I2S_CTRL_ENABLE;
        writel(ctrl, i2s->base + I2S_CTRL);
        break;
    default:
        ret = -EINVAL;
        break;
    }
​
    return ret;
}

3.4 设置 DAI 格式

/**
 * @brief 设置 DAI 格式 (I2S, Left-Justified, 主/从模式)。
 *
 * @param dai 指向 snd_soc_dai。
 * @param fmt 格式配置。
 * @return 0 成功。
 */
static int i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
    struct i2s_priv *i2s = snd_soc_dai_get_drvdata(dai);
    u32 ctrl;
​
    ctrl = readl(i2s->base + I2S_CTRL);
​
    // 1. 配置 DAI 格式
    ctrl &= ~(I2S_CTRL_I2S_MODE | I2S_CTRL_LEFT_J_MODE);
    switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
    case SND_SOC_DAIFMT_I2S:
        ctrl |= I2S_CTRL_I2S_MODE;
        break;
    case SND_SOC_DAIFMT_LEFT_J:
        ctrl |= I2S_CTRL_LEFT_J_MODE;
        break;
    default:
        return -EINVAL;
    }
​
    // 2. 配置主/从模式
    ctrl &= ~(I2S_CTRL_MASTER_MODE | I2S_CTRL_SLAVE_MODE);
    switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
    case SND_SOC_DAIFMT_CBS_CFS:
        ctrl |= I2S_CTRL_MASTER_MODE;
        break;
    case SND_SOC_DAIFMT_CBM_CFM:
        ctrl |= I2S_CTRL_SLAVE_MODE;
        break;
    default:
        return -EINVAL;
    }
​
    writel(ctrl, i2s->base + I2S_CTRL);
    return 0;
}

3.5 DMA 配置

/**
 * @brief 配置 DMA 参数,用于音频数据传输。
 *
 * @param i2s 指向 i2s_priv 结构。
 * @param substream 指向 snd_pcm_substream。
 * @return 0 成功。
 */
static int i2s_dma_init(struct i2s_priv *i2s, struct snd_pcm_substream *substream)
{
    struct snd_dmaengine_dai_dma_data *dma_data;
​
    if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
        dma_data = &i2s->dma_data_tx;
        dma_data->addr = i2s->base + I2S_TX_DATA;
        dma_data->addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
        dma_data->maxburst = 8;
    } else {
        dma_data = &i2s->dma_data_rx;
        dma_data->addr = i2s->base + I2S_RX_DATA;
        dma_data->addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
        dma_data->maxburst = 8;
    }
​
    snd_soc_dai_init_dma_data(i2s->dai, &i2s->dma_data_tx, &i2s->dma_data_rx);
    return 0;
}

3.6 DAI 驱动注册

/**
 * @brief DAI 驱动结构体。
 */
static const struct snd_soc_dai_ops i2s_dai_ops = {
    .hw_params = i2s_hw_params,
    .trigger = i2s_trigger,
    .set_fmt = i2s_set_fmt,
};
​
/**
 * @brief DAI 驱动描述符。
 */
static struct snd_soc_dai_driver i2s_dai_driver = {
    .name = "rk3288-i2s",
    .playback = {
        .stream_name = "Playback",
        .channels_min = 2,
        .channels_max = 8,
        .rates = SNDRV_PCM_RATE_8000_192000,
        .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
    },
    .capture = {
        .stream_name = "Capture",
        .channels_min = 2,
        .channels_max = 8,
        .rates = SNDRV_PCM_RATE_8000_192000,
        .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
    },
    .ops = &i2s_dai_ops,
};

3.7 Platform 驱动 Probe

/**
 * @brief I2S Platform 驱动 Probe 函数。
 *
 * @param pdev Platform 设备指针。
 * @return 0 成功。
 */
static int i2s_probe(struct platform_device *pdev)
{
    struct i2s_priv *i2s;
    struct resource *res;
    int ret, irq;
​
    // 1. 分配私有数据
    i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
    if (!i2s)
        return -ENOMEM;
    platform_set_drvdata(pdev, i2s);
​
    // 2. 获取 I/O 资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "No memory resource\n");
        return -ENXIO;
    }
    i2s->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(i2s->base))
        return PTR_ERR(i2s->base);
​
    // 3. 获取时钟
    i2s->mclk = devm_clk_get(&pdev->dev, "mclk");
    if (IS_ERR(i2s->mclk))
        return PTR_ERR(i2s->mclk);
    i2s->hclk = devm_clk_get(&pdev->dev, "hclk");
    if (IS_ERR(i2s->hclk))
        return PTR_ERR(i2s->hclk);
    i2s->bclk = devm_clk_get_optional(&pdev->dev, "bclk");
    if (IS_ERR(i2s->bclk))
        return PTR_ERR(i2s->bclk);
​
    clk_prepare_enable(i2s->mclk);
    clk_prepare_enable(i2s->hclk);
    if (i2s->bclk)
        clk_prepare_enable(i2s->bclk);
​
    // 4. 获取中断
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        ret = irq;
        goto err_clk;
    }
    i2s->irq = irq;
​
    spin_lock_init(&i2s->lock);
​
    // 5. 硬件初始化
    i2s_hw_init(i2s);
​
    // 6. 注册中断
    ret = devm_request_irq(&pdev->dev, i2s->irq, i2s_irq_handler,
                           IRQF_SHARED, "i2s", i2s);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ\n");
        goto err_clk;
    }
​
    // 7. 注册 DAI
    ret = devm_snd_soc_register_component(&pdev->dev, &i2s_component,
                                          &i2s_dai_driver, 1);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register DAI\n");
        goto err_clk;
    }
​
    // 8. 注册 PCM (通过 DMA)
    ret = devm_snd_soc_pcm_ops_init(&pdev->dev, &i2s_pcm_ops);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register PCM\n");
        goto err_clk;
    }
​
    dev_info(&pdev->dev, "I2S controller registered\n");
    return 0;
​
err_clk:
    if (i2s->bclk)
        clk_disable_unprepare(i2s->bclk);
    clk_disable_unprepare(i2s->hclk);
    clk_disable_unprepare(i2s->mclk);
    return ret;
}

3.8 中断处理函数

/**
 * @brief I2S 中断处理函数。
 *
 * @param irq 中断号。
 * @param dev_id 指向 i2s_priv 结构。
 * @return IRQ_HANDLED。
 */
static irqreturn_t i2s_irq_handler(int irq, void *dev_id)
{
    struct i2s_priv *i2s = dev_id;
    u32 int_stat;
​
    // 1. 读取中断状态
    int_stat = readl(i2s->base + I2S_INT_STAT);
​
    // 2. 处理 TX 中断
    if (int_stat & (1 << 0)) {
        writel(1 << 0, i2s->base + I2S_INT_STAT);
        // 通知 DMA 完成
        snd_pcm_period_elapsed(i2s->dai->playback_substream);
    }
​
    // 3. 处理 RX 中断
    if (int_stat & (1 << 1)) {
        writel(1 << 1, i2s->base + I2S_INT_STAT);
        snd_pcm_period_elapsed(i2s->dai->capture_substream);
    }
​
    // 4. 处理错误中断
    if (int_stat & (1 << 2)) {
        dev_err(i2s->dai->dev, "I2S error\n");
        writel(1 << 2, i2s->base + I2S_INT_STAT);
    }
​
    return IRQ_HANDLED;
}

第四章 Machine 驱动 (I2S + Codec 绑定)

4.1 Machine 驱动核心逻辑

/**
 * @struct audio_machine_priv
 * @brief Machine 驱动私有数据,管理 I2S 和 Codec 的连接。
 */
struct audio_machine_priv {
    struct snd_soc_card card;       /**< ASoC 声卡抽象 */
    struct snd_soc_dai_link links;  /**< DAI 链路 */
    struct device *dev;             /**< 设备指针 */
    struct snd_soc_ops ops;         /**< ASoC 操作 */
};
​
/**
 * @brief 音频流启用函数。
 *
 * @param substream 指向 snd_pcm_substream。
 * @param dai 指向 snd_soc_dai。
 * @return 0 成功。
 */
static int audio_hw_params(struct snd_pcm_substream *substream,
                           struct snd_pcm_hw_params *params,
                           struct snd_soc_dai *dai)
{
    struct snd_soc_card *card = dai->card;
    struct snd_soc_dai *codec_dai = asoc_rtd_to_codec(card->rtd, 0);
    struct snd_soc_dai *cpu_dai = asoc_rtd_to_cpu(card->rtd, 0);
    int ret;
​
    // 1. 配置 Codec DAI
    ret = snd_soc_dai_hw_params(codec_dai, substream, params);
    if (ret < 0) {
        dev_err(card->dev, "Failed to set codec params\n");
        return ret;
    }
​
    // 2. 配置 CPU DAI
    ret = snd_soc_dai_hw_params(cpu_dai, substream, params);
    if (ret < 0) {
        dev_err(card->dev, "Failed to set CPU params\n");
        return ret;
    }
​
    return 0;
}
​
/**
 * @brief Machine 驱动 Probe 函数。
 *
 * @param pdev Platform 设备指针。
 * @return 0 成功。
 */
static int audio_machine_probe(struct platform_device *pdev)
{
    struct audio_machine_priv *machine;
    struct snd_soc_card *card;
    struct snd_soc_dai_link *link;
    int ret;
​
    // 1. 分配私有数据
    machine = devm_kzalloc(&pdev->dev, sizeof(*machine), GFP_KERNEL);
    if (!machine)
        return -ENOMEM;
    platform_set_drvdata(pdev, machine);
    machine->dev = &pdev->dev;
​
    // 2. 分配声卡
    card = &machine->card;
    card->name = "rk3288-audio";
    card->dev = &pdev->dev;
    card->owner = THIS_MODULE;
​
    // 3. 设置 DAI 链路
    link = &machine->links;
    link->name = "I2S Link";
    link->stream_name = "I2S Playback";
    link->codec_name = "wm8960.0-001a";  // Codec 名称
    link->cpu_name = "rk3288-i2s.0";     // CPU DAI 名称
    link->codec_dai_name = "wm8960-hifi"; // Codec DAI 名称
    link->cpu_dai_name = "rk3288-i2s";    // CPU DAI 名称
    link->platform_name = "rk3288-i2s.0"; // Platform 名称
    link->ops = &machine->ops;
​
    card->dai_link = link;
    card->num_links = 1;
​
    // 4. 注册声卡
    ret = snd_soc_register_card(card);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register card\n");
        return ret;
    }
​
    dev_info(&pdev->dev, "Audio machine registered\n");
    return 0;
}

第五章 I2S 调试核心难点

5.1 播放无声音

现象:使用 aplay 播放音频文件,音箱无声,dmesg 无错误。

原因

  • I2S 时钟未正确配置。

  • BCLK/LRCK 比率计算错误。

  • Codec 未正确初始化。

调试方法

  1. 检查 I2S 寄存器

    devmem2 <base>+0x00  # I2S_CTRL
    devmem2 <base>+0x1C  # I2S_CLK_CFG
  2. 检查时钟频率

    cat /sys/kernel/debug/clk/clk_summary | grep i2s
  3. 测试 Codec

    # 使用 amixer 检查 Codec 音量
    amixer scontrol
    amixer sset 'Headphone' 80%

5.2 音频卡顿或爆音

现象:播放音频时出现周期性卡顿或爆音。

原因

  • DMA 传输延迟。

  • BCLK 频率不稳定。

  • 缓冲区不足。

调试方法

  1. 检查 DMA 状态

    devmem2 <base>+0x24  # I2S_DMA_STATUS
  2. 增加缓冲区大小

    # 在 aplay 中使用 --buffer-size 参数
    aplay --buffer-size=4096 test.wav
  3. 检查 BCLK 精度

    # 测量 BCLK 频率
    echo "bclk_rate=1000000" > /sys/kernel/debug/clk/clk_debug

5.3 录音数据错误

现象:录制的音频数据全为零或杂音。

原因

  • Codec 录音通道未使能。

  • 采样率不匹配。

  • 数据位宽配置错误。

调试方法

  1. 检查 Codec 录音使能

    amixer sset 'Capture' 80%
    amixer sset 'Mic' on
  2. 检查采样率

    cat /proc/asound/card0/stream0
  3. 检查数据格式

    arecord -d 5 -f cd -t raw /tmp/test.raw
    od -x /tmp/test.raw

第六章 与其他控制器的协同

控制器 协同方式 调试关键点
DMA 控制器 音频数据传输 缓冲区延迟、描述符链
Clock 控制器 提供音频时钟 (MCLK/BCLK) 频率精度、时钟抖动
Pinctrl 控制器 配置 I2S 引脚功能 引脚复用、驱动强度
Power Domain I2S 电源管理 上下电时序
GPIO 控制器 控制 Codec 复位/电源 复位时序

第四部分 WM8960 Codec 驱动接入 ALSA 子系统深度解析

第一章 WM8960 Codec 在 ALSA 框架中的接入位置

WM8960 作为 I2C 设备,通过 i2c_driver 注册到 I2C 核心,再通过 ASoC 框架注册到 ALSA 子系统。其接入流程如下:

[I2C 核心]
    ↓
[WM8960 I2C 驱动 (i2c_driver)]
    ↓
    → 调用 snd_soc_register_codec()
    ↓
[ASoC 框架]
    ↓
    → 绑定到 Machine 驱动的 snd_soc_dai_link
    ↓
    → snd_soc_register_card()
    ↓
    → 调用 snd_card_new() / snd_pcm_new()
    ↓
[ALSA 核心]
    ↓
    → 创建设备节点 /dev/snd/pcmC0D0p /dev/snd/controlC0
    ↓
[用户空间]
    ↓
    → alsa-lib 通过 /dev/snd/ 访问
    ↓
[应用层]
​

关键接入点

  1. I2C 层面i2c_driver -> wm8960_probe()

  2. ASoC Codec 层面wm8960_probe() -> snd_soc_register_codec()

  3. ASoC DAI 层面wm8960_dai 描述 Codec 的音频接口

  4. 控制层面wm8960_controls 注册 ALSA 控制设备 (/dev/snd/controlC0)


第二章 核心数据结构

2.1 snd_soc_codec_driver:Codec 驱动的核心描述

/**
 * @struct snd_soc_codec_driver
 * @brief ASoC Codec 驱动的核心描述符,包含所有的操作回调。
 *        WM8960 通过此结构注册到 ASoC 框架。
 */
static struct snd_soc_codec_driver soc_codec_dev_wm8960 = {
    .probe = wm8960_codec_probe,       /**< Codec probe 回调 */
    .remove = wm8960_codec_remove,     /**< Codec remove 回调 */
    .suspend = wm8960_suspend,         /**< 挂起回调 */
    .resume = wm8960_resume,           /**< 恢复回调 */
    .controls = wm8960_controls,       /**< 控制链表 */
    .num_controls = ARRAY_SIZE(wm8960_controls), /**< 控制数量 */
    .dapm_widgets = wm8960_dapm_widgets, /**< DAPM 小部件 */
    .num_dapm_widgets = ARRAY_SIZE(wm8960_dapm_widgets), /**< DAPM 小部件数量 */
    .dapm_routes = wm8960_audio_map,   /**< DAPM 路由 */
    .num_dapm_routes = ARRAY_SIZE(wm8960_audio_map), /**< DAPM 路由数量 */
};

2.2 snd_soc_dai_driver:Codec DAI 描述符

/**
 * @struct snd_soc_dai_driver
 * @brief 描述 WM8960 Codec 的音频接口 (DAI) 能力和操作。
 *        通过 soc_codec_dev_wm8960 间接注册,最终绑定到 Machine 驱动。
 */
static struct snd_soc_dai_driver wm8960_dai = {
    .name = "wm8960-hifi",          /**< DAI 名称,Machine 驱动需匹配此名称 */
    .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 = 2,          /**< 最小通道数 */
        .channels_max = 2,          /**< 最大通道数 */
        .rates = SNDRV_PCM_RATE_8000_48000, /**< 支持的采样率 */
        .formats = SNDRV_PCM_FMTBIT_S16_LE, /**< 支持的格式 */
    },
    .ops = &wm8960_dai_ops,         /**< DAI 操作回调 */
};

2.3 snd_soc_dai_link:Machine 驱动的绑定桥梁

/**
 * @struct snd_soc_dai_link
 * @brief Machine 驱动定义,连接 CPU DAI (I2S) 和 Codec DAI (WM8960)。
 *        此结构在 Machine 驱动中定义,最终绑定到 ASoC 核心。
 */
static struct snd_soc_dai_link wm8960_dai_link = {
    .name = "WM8960 HiFi",          /**< 链接名称 */
    .stream_name = "WM8960 HiFi",   /**< 流名称 */
    .codec_name = "wm8960.0-001a",  /**< 指向 Codec 的设备名称 */
    .codec_dai_name = "wm8960-hifi", /**< 指向 Codec 的 DAI 名称 */
    .cpu_name = "rockchip-i2s.0",   /**< 指向 CPU DAI 的设备名称 */
    .cpu_dai_name = "rockchip-i2s", /**< 指向 CPU DAI 的名称 */
    .platform_name = "rockchip-i2s.0", /**< 指向 Platform 设备名称 */
    .ops = &wm8960_snd_ops,         /**< 链接操作 */
    .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBS_CFS, /**< DAI 格式 */
};

第三章 核心接入代码实现

3.1 I2C 驱动注册到 ASoC Codec

/**
 * @brief WM8960 Codec 的 ASoC Codec 驱动入口。
 *        在 i2c_probe 中调用,完成 ASoC 注册。
 */
static int wm8960_probe(struct i2c_client *client)
{
    struct wm8960_priv *wm8960;
    int ret;
​
    // 1. 分配私有数据
    wm8960 = devm_kzalloc(&client->dev, sizeof(*wm8960), GFP_KERNEL);
    if (!wm8960)
        return -ENOMEM;
    i2c_set_clientdata(client, wm8960);
    wm8960->client = client;
​
    // 2. 初始化 regmap (I2C 读写封装)
    wm8960->regmap = devm_regmap_init_i2c(client, &wm8960_regmap_config);
    if (IS_ERR(wm8960->regmap)) {
        dev_err(&client->dev, "Failed to init regmap\n");
        return PTR_ERR(wm8960->regmap);
    }
​
    // 3. 读取芯片 ID 验证硬件
    u32 val;
    ret = regmap_read(wm8960->regmap, 0x00, &val);
    if (ret) {
        dev_err(&client->dev, "Failed to read chip ID\n");
        return ret;
    }
    if (val != WM8960_ID) {
        dev_err(&client->dev, "Invalid chip ID 0x%04x\n", val);
        return -ENODEV;
    }
​
    // 4. 初始化硬件寄存器
    wm8960_init_regs(wm8960, wm8960_reg_defaults, ARRAY_SIZE(wm8960_reg_defaults));
​
    // 5. 注册 ASoC Codec (核心接入点)
    //    此函数将 soc_codec_dev_wm8960 和 wm8960_dai 注册到 ASoC 核心
    ret = snd_soc_register_codec(&client->dev, &soc_codec_dev_wm8960,
                                 &wm8960_dai, 1);
    if (ret) {
        dev_err(&client->dev, "Failed to register codec\n");
        return ret;
    }
​
    dev_info(&client->dev, "WM8960 codec registered to ASoC\n");
    return 0;
}
​
/**
 * @brief 移除函数,从 ASoC 注销 Codec。
 */
static int wm8960_remove(struct i2c_client *client)
{
    // 调用 snd_soc_unregister_codec 注销 ASoC Codec
    snd_soc_unregister_codec(&client->dev);
    return 0;
}

3.2 Machine 驱动绑定 (完成 DAI 链接)

Machine 驱动负责将 CPU DAI 和 Codec DAI 连接,最终触发 ALSA 核心注册。

/**
 * @brief Machine 驱动的 probe 函数,用于绑定 WM8960。
 *
 * @param pdev Platform 设备指针。
 * @return 0 成功。
 */
static int audio_machine_probe(struct platform_device *pdev)
{
    struct snd_soc_card *card;
    int ret;
​
    // 1. 分配 ASoC 声卡
    card = devm_kzalloc(&pdev->dev, sizeof(*card), GFP_KERNEL);
    if (!card)
        return -ENOMEM;
    card->name = "rockchip-wm8960";
    card->dev = &pdev->dev;
    card->owner = THIS_MODULE;
​
    // 2. 设置 DAI 链接 (绑定 CPU DAI 和 Codec DAI)
    //    此时在 ASoC 核心中搜索匹配的 Codec 设备
    card->dai_link = &wm8960_dai_link;
    card->num_links = 1;
​
    // 3. 注册声卡 (核心接入点)
    //    内部调用 snd_card_new() -> snd_pcm_new() -> snd_card_register()
    ret = snd_soc_register_card(card);
    if (ret) {
        dev_err(&pdev->dev, "Failed to register ASoC card\n");
        return ret;
    }
​
    dev_info(&pdev->dev, "ASoC card registered: rockchip-wm8960\n");
    return 0;
}

3.3 控制接口注册 (ALSA 混音器)

/**
 * @brief 控制接口定义,包括混音器、音量、开关等。
 *        通过 soc_codec_dev_wm8960.controls 注册到 ALSA 控制设备。
 */
static const struct snd_kcontrol_new wm8960_controls[] = {
    /* 耳机音量控制 */
    SOC_DOUBLE_R_TLV("Headphone Volume", WM8960_LOUT1_VOLUME,
                     WM8960_ROUT1_VOLUME, 0, 127, 0, wm8960_tlv),
​
    /* 扬声器音量控制 */
    SOC_DOUBLE_R_TLV("Speaker Volume", WM8960_LOUT2_VOLUME,
                     WM8960_ROUT2_VOLUME, 0, 127, 0, wm8960_tlv),
​
    /* DAC 音量 */
    SOC_SINGLE_TLV("DAC Playback Volume", WM8960_DAC_VOLUME, 0, 255, 0,
                   wm8960_tlv),
​
    /* 输入音量 */
    SOC_SINGLE_TLV("Left Input Volume", WM8960_LEFT_INPUT_VOLUME, 0, 31, 0,
                   wm8960_tlv),
    SOC_SINGLE_TLV("Right Input Volume", WM8960_RIGHT_INPUT_VOLUME, 0, 31, 0,
                   wm8960_tlv),
​
    /* 切换开关 */
    SOC_SINGLE("Speaker Enable", WM8960_SPEAKER_MIXER, 0, 1, 0),
    SOC_SINGLE("Headphone Enable", WM8960_LOUT1_MIXER, 0, 1, 0),
    SOC_SINGLE("Mic Boost", WM8960_LEFT_INPUT_VOLUME, 6, 3, 0),
};
​
/**
 * @brief 音量 TLV 表 (用于增益/衰减曲线)
 */
static const unsigned int wm8960_tlv[] = {
    SND_SOC_TLV_DB_RANGE(-100, 0, 0),
    SND_SOC_TLV_DB_RANGE(0, 127, 100),
};

3.4 DAPM 注册

DAPM 定义音频路径,确保启用/禁用音频路径时自动管理电源。

/**
 * @brief DAPM 小部件,定义硬件组件。
 */
static const struct snd_soc_dapm_widget wm8960_dapm_widgets[] = {
    /* 物理输入 */
    SND_SOC_DAPM_INPUT("LIN"),
    SND_SOC_DAPM_INPUT("RIN"),
    SND_SOC_DAPM_INPUT("MIC1"),
    SND_SOC_DAPM_INPUT("MIC2"),
​
    /* 物理输出 */
    SND_SOC_DAPM_OUTPUT("LOUT1"),
    SND_SOC_DAPM_OUTPUT("ROUT1"),
    SND_SOC_DAPM_OUTPUT("LOUT2"),
    SND_SOC_DAPM_OUTPUT("ROUT2"),
​
    /* 核心 DAC/ADC */
    SND_SOC_DAPM_DAC("DAC", "Playback", WM8960_POWER_MANAGEMENT_1, 1, 0),
    SND_SOC_DAPM_ADC("ADC", "Capture", WM8960_POWER_MANAGEMENT_1, 2, 0),
​
    /* 混音器 */
    SND_SOC_DAPM_MIXER("Left Output Mixer", WM8960_LEFT_OUTPUT_MIXER, 0, 0, NULL, 0),
    SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_RIGHT_OUTPUT_MIXER, 0, 0, NULL, 0),
};
​
/**
 * @brief DAPM 路由,定义音频路径连接关系。
 *        由 ASoC 核心动态管理电源。
 */
static const struct snd_soc_dapm_route wm8960_audio_map[] = {
    { "LOUT1", NULL, "Left Output Mixer" },
    { "ROUT1", NULL, "Right Output Mixer" },
    { "Left Output Mixer", "DAC", "DAC" },
    { "Right Output Mixer", "DAC", "DAC" },
    { "ADC", NULL, "MIC1" },
    { "ADC", NULL, "MIC2" },
};

第四章 Codec 接入 ALSA 调试核心难点

4.1 声卡设备节点不出现

现象ls /dev/snd/ 没有 controlC0pcmC0D0p

原因

  • Codec 在 ASoC 中未成功注册。

  • Machine 驱动的 DAI 链接名称不匹配。

  • I2C 通信失败导致 Codec 无法被识别。

调试方法

  1. 检查 ASoC 注册日志

    dmesg | grep "ASoC"
    dmesg | grep "wm8960"
  2. 检查 Codec 是否在 ASoC 列表中

    cat /proc/asound/cards
  3. 检查 I2C 地址

    i2cdetect -y <bus>
  4. 使用 trace-cmd 跟踪注册过程

    trace-cmd record -e snd_soc:* -e snd:* -a -- modprobe wm8960
    trace-cmd report | grep "register"

4.2 Codec 无法通过 I2C 通信

现象i2cdetect 检测不到 WM8960,dmesg 显示 "Failed to read chip ID"。

原因

  • I2C 地址错误(WM8960 常见为 0x1A)。

  • Codec 未上电或复位引脚未拉高。

  • I2C 总线被占用或频率过高。

调试方法

  1. 确认硬件地址i2cdetect -y <bus>

  2. 检查复位 GPIOcat /sys/kernel/debug/gpio | grep reset

  3. 使用逻辑分析仪:观察 SDA/SCL 时序。

4.3 控制控件不生效 (kcontrol)

现象amixer sset 'Headphone Volume' 80% 无效果,但 regmap 显示寄存器值已被修改。

原因

  • 电源未启用,DAC/混音器处于关闭状态。

  • DAPM 路由未正确配置。

  • 音频接口主/从模式不匹配。

调试方法

  1. 检查电源寄存器

    devmem2 <i2c_addr>+0x08
    devmem2 <i2c_addr>+0x09
  2. 检查 DAPM 状态

    cat /sys/kernel/debug/asoc/wm8960/DAPM/status
  3. 检查 I2S 格式

    cat /sys/kernel/debug/asoc/wm8960/registers

4.4 播放/捕获无声

现象aplay 正常,但耳机无声;arecord 正常,但录制的文件无声音。

原因

  • 音频路径未使能(如 Mixer 关闭)。

  • 主时钟 (MCLK) 未提供。

  • 采样率不匹配。

调试方法

  1. 检查 DAPM 路由

    cat /sys/kernel/debug/asoc/card0/DAPM/status
  2. 测量 MCLK:使用示波器。

  3. 强制启用混音器

    amixer sset 'Left Output Mixer' on
    amixer sset 'Right Output Mixer' on
    amixer sset 'Headphone Enable' on

第五章 与其他框架的协同

框架/核心 协同方式 调试关键点
I2C 核心 提供 I2C 读写接口 地址匹配、时钟频率
Regmap 封装 I2C 读写,缓存寄存器 缓存一致性、延迟
ASoC 核心 管理 Codec DAI 注册、DAPM 电源路径、混音器路由
ALSA 核心 提供 PCM 和控制设备 设备节点注册、IOCTL
Machine 驱动 绑定 CPU DAI 与 Codec DAI 名称匹配、格式配置
CPU DAI 通过 I2S 传输音频数据 BCLK/LRCLK 同步、格式匹配

第六章 完整接入流程总结

1. I2C 总线扫描 → 找到 WM8960 (地址 0x1A)
2. I2C 核心匹配 wm8960 i2c_driver
3. wm8960_probe() 执行
   - 初始化 regmap
   - 读取芯片 ID
   - 初始化寄存器
   - 调用 snd_soc_register_codec()
4. ASoC 核心注册 Codec DAI (wm8960_dai)
5. Machine 驱动 probe
   - 定义 snd_soc_dai_link
   - 调用 snd_soc_register_card()
6. ASoC 核心搜索匹配的 DAI
   - 找到 "wm8960-hifi" (Codec DAI)
   - 找到 "rockchip-i2s" (CPU DAI)
7. ASoC 核心调用 snd_card_new() / snd_pcm_new()
8. ALSA 核心创建设备节点
   - /dev/snd/controlC0
   - /dev/snd/pcmC0D0p (播放)
   - /dev/snd/pcmC0D0c (捕获)
9. 用户空间通过 alsa-lib 访问

第五部分 从麦克风驱动到 FFmpeg 播放与录音全流程

第一章 麦克风驱动在 WM8960 中的实现

1.1 麦克风硬件路径

WM8960 支持两路麦克风输入 (MIC1/MIC2),通过内部混音器路由到 ADC,再通过 I2S 接口发送到 CPU。

[MIC1/MIC2] → [麦克风偏置] → [输入增益控制] → [ADC] → [I2S 接口] → [CPU DAI]

1.2 麦克风寄存器配置

/**
 * @struct wm8960_mic_config
 * @brief WM8960 麦克风配置结构。
 */
struct wm8960_mic_config {
    u8 mic_boost:2;           /**< 麦克风增益 (0=0dB, 1=10dB, 2=20dB, 3=30dB) */
    u8 mic_enable:1;          /**< 麦克风使能 */
    u8 mic_input:1;           /**< 输入选择 (0=MIC1, 1=MIC2) */
    u8 mic_bias:1;            /**< 偏置电压使能 */
    u8 mic_voltage:1;         /**< 偏置电压 (0=2.5V, 1=3.3V) */
};
​
/* 麦克风相关寄存器偏移 */
#define WM8960_LEFT_INPUT_VOLUME  0x00
#define WM8960_RIGHT_INPUT_VOLUME 0x01
#define WM8960_POWER_MANAGEMENT_2 0x09
#define WM8960_LEFT_INPUT_MIXER   0x21
#define WM8960_RIGHT_INPUT_MIXER  0x22
​
/* 宏定义 */
#define WM8960_PWR2_LMIC          (1 << 0)  /**< 左麦克风电源 */
#define WM8960_PWR2_RMIC          (1 << 1)  /**< 右麦克风电源 */
#define WM8960_PWR2_LIN           (1 << 2)  /**< 左输入电源 */
#define WM8960_PWR2_RIN           (1 << 3)  /**< 右输入电源 */
#define WM8960_PWR2_LMICBIAS      (1 << 4)  /**< 左麦克风偏置 */
#define WM8960_PWR2_RMICBIAS      (1 << 5)  /**< 右麦克风偏置 */

1.3 麦克风驱动实现 (基于 WM8960)

/**
 * @brief 配置 WM8960 麦克风参数。
 *
 * @param codec 指向 snd_soc_codec。
 * @param config 指向 wm8960_mic_config。
 * @return 0 成功。
 */
static int wm8960_mic_config(struct snd_soc_codec *codec,
                             struct wm8960_mic_config *config)
{
    u16 pwr2;
    u16 input_vol;
    u16 mixer;
​
    // 1. 启用麦克风电源
    pwr2 = read_reg(codec, WM8960_POWER_MANAGEMENT_2);
    if (config->mic_enable) {
        pwr2 |= WM8960_PWR2_LMIC | WM8960_PWR2_RMIC;
        if (config->mic_bias) {
            pwr2 |= WM8960_PWR2_LMICBIAS | WM8960_PWR2_RMICBIAS;
        }
    } else {
        pwr2 &= ~(WM8960_PWR2_LMIC | WM8960_PWR2_RMIC |
                  WM8960_PWR2_LMICBIAS | WM8960_PWR2_RMICBIAS);
    }
    write_reg(codec, WM8960_POWER_MANAGEMENT_2, pwr2);
​
    // 2. 配置输入增益 (0-31)
    input_vol = read_reg(codec, WM8960_LEFT_INPUT_VOLUME);
    input_vol &= ~0x1F;
    input_vol |= (config->mic_boost << 5) | 0x10;  // 默认 16 增益
    write_reg(codec, WM8960_LEFT_INPUT_VOLUME, input_vol);
    write_reg(codec, WM8960_RIGHT_INPUT_VOLUME, input_vol);
​
    // 3. 配置混音器路由
    mixer = read_reg(codec, WM8960_LEFT_INPUT_MIXER);
    if (config->mic_input == 0) {
        mixer |= (1 << 0);  // 启用 MIC1 -> 混音器
    } else {
        mixer |= (1 << 1);  // 启用 MIC2 -> 混音器
    }
    write_reg(codec, WM8960_LEFT_INPUT_MIXER, mixer);
    write_reg(codec, WM8960_RIGHT_INPUT_MIXER, mixer);
​
    return 0;
}
​
/**
 * @brief 麦克风增益控制 (ALSA kcontrol)。
 */
static int wm8960_mic_boost_control(struct snd_kcontrol *kcontrol,
                                   struct snd_ctl_elem_value *ucontrol)
{
    struct snd_soc_codec *codec = snd_kcontrol_chip(kcontrol);
    struct wm8960_priv *wm8960 = snd_soc_codec_get_drvdata(codec);
    struct wm8960_mic_config config;
​
    config.mic_enable = ucontrol->value.integer.value[0];
    config.mic_boost = ucontrol->value.integer.value[1];
    config.mic_input = ucontrol->value.integer.value[2];
    config.mic_bias = ucontrol->value.integer.value[3];
​
    return wm8960_mic_config(codec, &config);
}
​
/* 麦克风控制注册 */
static const struct snd_kcontrol_new wm8960_mic_controls[] = {
    SOC_SINGLE_BOOL_EXT("Mic Enable", 0, wm8960_mic_boost_control, NULL),
    SOC_SINGLE("Mic Boost", WM8960_LEFT_INPUT_VOLUME, 5, 3, 0),
};

1.4 麦克风驱动调试

现象arecord 录制无声。

原因

  • 麦克风电源未启用。

  • 混音器路由未配置。

  • 偏置电压未提供。

调试方法

  1. 检查电源寄存器

    devmem2 <i2c_addr>+0x09  # WM8960_POWER_MANAGEMENT_2
  2. 检查混音器寄存器

    devmem2 <i2c_addr>+0x21  # WM8960_LEFT_INPUT_MIXER
  3. 检查偏置电压

    # 使用示波器测量 MICBIAS 引脚电压

第二章 声音降噪 (Noise Reduction) 技术

2.1 降噪算法选择

算法 优势 劣势 应用场景
谱减法 (Spectral Subtraction) 计算简单,实时性好 音乐噪声 (Musical Noise) 实时通信
维纳滤波 (Wiener Filtering) 噪声抑制效果好 计算复杂度高 高质量录音
自适应滤波 (Adaptive Filtering) 可适应环境变化 收敛时间慢 移动设备
深度学习降噪 (RNNoise) 效果最佳 资源消耗大 高端设备

2.2 Linux 降噪实现 (使用 SpeexDSP)

/**
 * @brief 使用 SpeexDSP 进行谱减法降噪。
 *
 * @param input 输入音频数据 (16-bit PCM)。
 * @param output 输出降噪后音频数据。
 * @param frame_size 帧大小 (通常 160 采样点)。
 * @param noise_level 噪声等级 (0-1)。
 * @return 0 成功。
 */
static int noise_reduction_speex(short *input, short *output,
                                 int frame_size, float noise_level)
{
    static SpeexPreprocessState *state = NULL;
    static int initialized = 0;
​
    if (!initialized) {
        // 初始化 Speex 降噪状态
        state = speex_preprocess_state_init(frame_size, 16000);
        if (!state) return -1;
        // 配置降噪参数
        speex_preprocess_ctl(state, SPEEX_PREPROCESS_SET_DENOISE, &noise_level);
        initialized = 1;
    }
​
    // 执行降噪处理
    if (speex_preprocess_run(state, input) != 1) {
        return -1;
    }
​
    // 复制结果到输出缓冲区
    memcpy(output, input, frame_size * sizeof(short));
    return 0;
}
​
/**
 * @brief 使用 FFmpeg 的降噪滤波器 (afftdn)。
 *
 * @param ctx 指向 AVFilterContext。
 * @param frame 指向 AVFrame。
 * @param output 输出 AVFrame。
 * @return 0 成功。
 */
static int noise_reduction_ffmpeg(AVFilterContext *ctx,
                                  AVFrame *frame, AVFrame *output)
{
    int ret;
​
    // 1. 将输入帧发送到滤波器
    ret = av_buffersrc_add_frame(ctx, frame);
    if (ret < 0) {
        fprintf(stderr, "Failed to add frame to filter\n");
        return ret;
    }
​
    // 2. 从滤波器获取输出帧
    ret = av_buffersink_get_frame(ctx, output);
    if (ret < 0) {
        fprintf(stderr, "Failed to get frame from filter\n");
        return ret;
    }
​
    return 0;
}

2.3 FFmpeg 降噪滤波器配置

/**
 * @brief 配置 FFmpeg 降噪滤波器链。
 *
 * @param filter_graph 指向 AVFilterGraph。
 * @return 0 成功。
 */
static int configure_noise_filter(AVFilterGraph *filter_graph)
{
    AVFilterContext *buffersink_ctx = NULL;
    AVFilterContext *buffersrc_ctx = NULL;
    AVFilter *buffersrc = avfilter_get_by_name("abuffer");
    AVFilter *buffersink = avfilter_get_by_name("abuffersink");
    char args[256];
​
    // 1. 创建源滤波器
    snprintf(args, sizeof(args),
             "time_base=1/48000:sample_rate=48000:sample_fmt=s16:channel_layout=stereo");
    avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
                                 args, NULL, filter_graph);
​
    // 2. 创建降噪滤波器 (afftdn)
    AVFilter *noise_filter = avfilter_get_by_name("afftdn");
    avfilter_graph_create_filter(&buffersink_ctx, noise_filter, "afftdn",
                                 "nr=15", NULL, filter_graph);
​
    // 3. 连接滤波器链
    avfilter_link(buffersrc_ctx, 0, buffersink_ctx, 0);
    avfilter_graph_config(filter_graph, NULL);
​
    return 0;
}

第三章 FFmpeg 音频播放与录音中间件

3.1 播放音频文件

/**
 * @brief 使用 FFmpeg 播放音频文件。
 *
 * @param filename 音频文件路径。
 * @param device ALSA 设备名称 (如 "hw:0,0")。
 * @return 0 成功。
 */
static int ffmpeg_play_audio(const char *filename, const char *device)
{
    AVFormatContext *fmt_ctx = NULL;
    AVCodecContext *codec_ctx = NULL;
    AVCodec *codec = NULL;
    AVFrame *frame = NULL;
    AVPacket *packet = NULL;
    SwrContext *swr_ctx = NULL;
    int audio_stream_idx = -1;
    int ret;
​
    // 1. 注册所有组件
    av_register_all();
    avformat_network_init();
​
    // 2. 打开输入文件
    ret = avformat_open_input(&fmt_ctx, filename, NULL, NULL);
    if (ret < 0) {
        fprintf(stderr, "Failed to open input file\n");
        return ret;
    }
​
    // 3. 查找流信息
    ret = avformat_find_stream_info(fmt_ctx, NULL);
    if (ret < 0) {
        fprintf(stderr, "Failed to find stream info\n");
        goto error;
    }
​
    // 4. 查找音频流
    for (int i = 0; i < fmt_ctx->nb_streams; i++) {
        if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_idx = i;
            break;
        }
    }
    if (audio_stream_idx < 0) {
        fprintf(stderr, "No audio stream found\n");
        goto error;
    }
​
    // 5. 分配解码器上下文
    codec_ctx = avcodec_alloc_context3(NULL);
    avcodec_parameters_to_context(codec_ctx,
                                  fmt_ctx->streams[audio_stream_idx]->codecpar);
​
    // 6. 查找并打开解码器
    codec = avcodec_find_decoder(codec_ctx->codec_id);
    if (!codec) {
        fprintf(stderr, "Failed to find codec\n");
        goto error;
    }
    ret = avcodec_open2(codec_ctx, codec, NULL);
    if (ret < 0) {
        fprintf(stderr, "Failed to open codec\n");
        goto error;
    }
​
    // 7. 创建重采样器 (转换为 ALSA 期望的格式)
    swr_ctx = swr_alloc();
    av_opt_set_int(swr_ctx, "in_channel_layout", codec_ctx->channel_layout, 0);
    av_opt_set_int(swr_ctx, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
    av_opt_set_int(swr_ctx, "in_sample_rate", codec_ctx->sample_rate, 0);
    av_opt_set_int(swr_ctx, "out_sample_rate", 48000, 0);
    av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", codec_ctx->sample_fmt, 0);
    av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
    swr_init(swr_ctx);
​
    // 8. 分配帧和包
    frame = av_frame_alloc();
    packet = av_packet_alloc();
​
    // 9. 打开 ALSA 设备
    snd_pcm_t *pcm;
    snd_pcm_open(&pcm, device, SND_PCM_STREAM_PLAYBACK, 0);
    snd_pcm_set_params(pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED,
                       1, 48000, 1, 20000);
​
    // 10. 解码并播放
    while (av_read_frame(fmt_ctx, packet) >= 0) {
        if (packet->stream_index == audio_stream_idx) {
            ret = avcodec_send_packet(codec_ctx, packet);
            if (ret < 0) break;
​
            while (ret >= 0) {
                ret = avcodec_receive_frame(codec_ctx, frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
                if (ret < 0) goto error;
​
                // 重采样
                AVFrame *out_frame = av_frame_alloc();
                out_frame->format = AV_SAMPLE_FMT_S16;
                out_frame->channel_layout = AV_CH_LAYOUT_STEREO;
                out_frame->sample_rate = 48000;
                out_frame->nb_samples = frame->nb_samples;
                av_frame_get_buffer(out_frame, 0);
​
                swr_convert(swr_ctx, out_frame->data, out_frame->nb_samples,
                           (const uint8_t **)frame->data, frame->nb_samples);
​
                // 写入 ALSA
                snd_pcm_writei(pcm, out_frame->data[0], out_frame->nb_samples);
​
                av_frame_free(&out_frame);
            }
        }
        av_packet_unref(packet);
    }
​
error:
    // 清理资源
    if (pcm) snd_pcm_close(pcm);
    if (swr_ctx) swr_free(&swr_ctx);
    if (frame) av_frame_free(&frame);
    if (packet) av_packet_free(&packet);
    if (codec_ctx) avcodec_free_context(&codec_ctx);
    if (fmt_ctx) avformat_close_input(&fmt_ctx);
    return 0;
}

3.2 从麦克风录音

/**
 * @brief 使用 ALSA 从麦克风录音。
 *
 * @param device ALSA 设备名称 (如 "hw:0,0")。
 * @param filename 输出 WAV 文件路径。
 * @param duration 录音时长 (秒)。
 * @return 0 成功。
 */
static int alsa_record_mic(const char *device, const char *filename,
                           int duration)
{
    snd_pcm_t *pcm;
    snd_pcm_hw_params_t *params;
    snd_pcm_uframes_t frames = 1024;
    char *buffer;
    int ret;
    FILE *output;
​
    // 1. 打开 ALSA 设备
    ret = snd_pcm_open(&pcm, device, SND_PCM_STREAM_CAPTURE, 0);
    if (ret < 0) {
        fprintf(stderr, "Failed to open PCM device\n");
        return ret;
    }
​
    // 2. 配置硬件参数
    snd_pcm_hw_params_alloca(&params);
    snd_pcm_hw_params_any(pcm, params);
    snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED);
    snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE);
    snd_pcm_hw_params_set_channels(pcm, params, 2);
    snd_pcm_hw_params_set_rate(pcm, params, 48000, 0);
    snd_pcm_hw_params_set_period_size(pcm, params, frames, 0);
    snd_pcm_hw_params(pcm, params);
​
    // 3. 准备录音
    snd_pcm_prepare(pcm);
​
    // 4. 分配缓冲区
    buffer = malloc(frames * 4);  // 2 channels * 2 bytes/sample
​
    // 5. 打开输出文件 (WAV)
    output = fopen(filename, "wb");
    // 写入 WAV 头 (简化)
    write_wav_header(output, 48000, 2, 16, duration * 48000);
​
    // 6. 录制音频
    int total_frames = duration * 48000;
    while (total_frames > 0) {
        int frames_to_read = (total_frames > frames) ? frames : total_frames;
        ret = snd_pcm_readi(pcm, buffer, frames_to_read);
        if (ret < 0) {
            fprintf(stderr, "PCM read error\n");
            break;
        }
        fwrite(buffer, frames_to_read * 4, 1, output);
        total_frames -= frames_to_read;
    }
​
    // 7. 清理资源
    fclose(output);
    free(buffer);
    snd_pcm_close(pcm);
​
    return 0;
}

3.3 音频环回 (从 MIC 到喇叭)

/**
 * @brief 实时音频环回:从麦克风采集并立即播放到喇叭。
 *
 * @param device ALSA 设备名称。
 * @param latency 延迟 (毫秒)。
 * @return 0 成功。
 */
static int audio_loopback(const char *device, int latency)
{
    snd_pcm_t *playback_pcm, *capture_pcm;
    snd_pcm_hw_params_t *params;
    char *buffer;
    int ret;
​
    // 1. 打开播放和录制设备
    ret = snd_pcm_open(&playback_pcm, device, SND_PCM_STREAM_PLAYBACK, 0);
    if (ret < 0) return ret;
    ret = snd_pcm_open(&capture_pcm, device, SND_PCM_STREAM_CAPTURE, 0);
    if (ret < 0) {
        snd_pcm_close(playback_pcm);
        return ret;
    }
​
    // 2. 配置参数 (同步配置)
    snd_pcm_hw_params_alloca(&params);
    snd_pcm_hw_params_any(capture_pcm, params);
    snd_pcm_hw_params_set_access(capture_pcm, params, SND_PCM_ACCESS_RW_INTERLEAVED);
    snd_pcm_hw_params_set_format(capture_pcm, params, SND_PCM_FORMAT_S16_LE);
    snd_pcm_hw_params_set_channels(capture_pcm, params, 2);
    snd_pcm_hw_params_set_rate(capture_pcm, params, 48000, 0);
    snd_pcm_hw_params_set_period_size(capture_pcm, params, 1024, 0);
    snd_pcm_hw_params(capture_pcm, params);
​
    // 3. 复制参数到播放设备
    snd_pcm_hw_params_copy(params, capture_pcm);
    snd_pcm_hw_params(playback_pcm, params);
​
    // 4. 准备设备
    snd_pcm_prepare(playback_pcm);
    snd_pcm_prepare(capture_pcm);
​
    // 5. 分配缓冲区
    buffer = malloc(1024 * 4);
​
    // 6. 执行环回
    while (1) {
        // 从麦克风读取
        ret = snd_pcm_readi(capture_pcm, buffer, 1024);
        if (ret < 0) continue;
​
        // 写入喇叭 (播放)
        ret = snd_pcm_writei(playback_pcm, buffer, ret);
        if (ret < 0) {
            // 发生 XRUN,恢复
            snd_pcm_prepare(playback_pcm);
        }
    }
​
    free(buffer);
    snd_pcm_close(capture_pcm);
    snd_pcm_close(playback_pcm);
    return 0;
}

第四章 应用层中间件集成

4.1 统一音频 API 封装

/**
 * @struct audio_app_ctx
 * @brief 音频应用程序上下文。
 */
struct audio_app_ctx {
    struct audio_device *playback_dev; /**< 播放设备 */
    struct audio_device *capture_dev;  /**< 录制设备 */
    struct audio_ringbuffer *ring;     /**< 环型缓冲区 */
    struct noise_reduction *nr;        /**< 降噪处理 */
    pthread_t playback_thread;         /**< 播放线程 */
    pthread_t capture_thread;          /**< 录制线程 */
    int running;                       /**< 运行状态 */
};
​
/**
 * @brief 初始化音频应用上下文。
 *
 * @param device ALSA 设备名称。
 * @param sample_rate 采样率。
 * @param channels 通道数。
 * @return 指向 audio_app_ctx 结构。
 */
static struct audio_app_ctx *audio_app_init(const char *device,
                                            int sample_rate, int channels)
{
    struct audio_app_ctx *ctx = malloc(sizeof(*ctx));
    if (!ctx) return NULL;
​
    // 1. 打开 ALSA 设备
    snd_pcm_t *playback, *capture;
    snd_pcm_open(&playback, device, SND_PCM_STREAM_PLAYBACK, 0);
    snd_pcm_open(&capture, device, SND_PCM_STREAM_CAPTURE, 0);
​
    // 2. 配置设备参数
    snd_pcm_set_params(playback, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED,
                       channels, sample_rate, 1, 50000);
    snd_pcm_set_params(capture, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED,
                       channels, sample_rate, 1, 50000);
​
    // 3. 创建环型缓冲区
    ctx->ring = audio_ringbuffer_create(sample_rate * 2);
​
    // 4. 初始化降噪
    ctx->nr = noise_reduction_init(sample_rate);
​
    ctx->playback_dev = playback;
    ctx->capture_dev = capture;
    ctx->running = 1;
​
    return ctx;
}
​
/**
 * @brief 启动音频处理线程。
 *
 * @param ctx 指向 audio_app_ctx。
 * @return 0 成功。
 */
static int audio_app_start(struct audio_app_ctx *ctx)
{
    pthread_create(&ctx->capture_thread, NULL, capture_thread_func, ctx);
    pthread_create(&ctx->playback_thread, NULL, playback_thread_func, ctx);
    return 0;
}
​
/**
 * @brief 捕获线程函数。
 */
static void *capture_thread_func(void *arg)
{
    struct audio_app_ctx *ctx = arg;
    short buffer[1024 * 2];
​
    while (ctx->running) {
        // 从麦克风读取
        int frames = snd_pcm_readi(ctx->capture_dev, buffer, 1024);
        if (frames < 0) continue;
​
        // 降噪处理
        noise_reduction_process(ctx->nr, buffer, frames * 2);
​
        // 写入环型缓冲区
        audio_ringbuffer_write(ctx->ring, buffer, frames * 2);
    }
    return NULL;
}
​
/**
 * @brief 播放线程函数。
 */
static void *playback_thread_func(void *arg)
{
    struct audio_app_ctx *ctx = arg;
    short buffer[1024 * 2];
​
    while (ctx->running) {
        // 从环型缓冲区读取
        int samples = audio_ringbuffer_read(ctx->ring, buffer, 1024 * 2);
        if (samples == 0) {
            // 缓冲区空,填充静音
            memset(buffer, 0, 1024 * 2);
            samples = 1024 * 2;
        }
​
        // 播放到喇叭
        int frames = samples / 2;
        snd_pcm_writei(ctx->playback_dev, buffer, frames);
    }
    return NULL;
}

4.2 完整应用示例

/**
 * @brief 主函数,实现录音、降噪、播放全流程。
 */
int main(int argc, char **argv)
{
    struct audio_app_ctx *ctx;
    int choice;
​
    printf("Audio Application\n");
    printf("1. Play audio file\n");
    printf("2. Record from microphone\n");
    printf("3. Real-time loopback\n");
    printf("4. Record with noise reduction\n");
    printf("Choose an option: ");
    scanf("%d", &choice);
​
    switch (choice) {
    case 1:
        ffmpeg_play_audio("test.mp3", "hw:0,0");
        break;
    case 2:
        alsa_record_mic("hw:0,0", "record.wav", 10);
        break;
    case 3:
        audio_loopback("hw:0,0", 20);
        break;
    case 4:
        ctx = audio_app_init("hw:0,0", 48000, 2);
        audio_app_start(ctx);
        printf("Recording with noise reduction... Press Enter to stop\n");
        getchar();
        ctx->running = 0;
        pthread_join(ctx->capture_thread, NULL);
        pthread_join(ctx->playback_thread, NULL);
        audio_app_cleanup(ctx);
        break;
    }
​
    return 0;
}

第五章 音频中间件调试核心难点

5.1 麦克风录音无声

现象arecord 录制文件无声音。

原因

  • 麦克风电源未启用。

  • 混音器路由未配置。

  • 偏置电压未提供。

调试方法

  1. 检查音量设置

    amixer sset 'Mic' 80%
    amixer sset 'Mic Boost' 20dB
  2. 检查路由

    amixer sset 'Left Input Mixer' on
    amixer sset 'Right Input Mixer' on
  3. 检查电源

    devmem2 <i2c_addr>+0x09  # WM8960_POWER_MANAGEMENT_2

5.2 降噪效果不佳

现象:背景噪声仍然明显。

原因

  • 降噪阈值设置不当。

  • 噪声模型不匹配。

  • 滤波器参数未优化。

调试方法

  1. 调整噪声等级

    echo "nr=20" > /sys/class/audio/noise_reduction/params
  2. 使用频谱分析

    ffmpeg -i input.wav -af "showspectrum" output.png
  3. 测试不同算法

    # 比较 SpeexDSP 和 FFmpeg afftdn

5.3 环回延迟过高

现象:听到回声,延迟明显。

原因

  • 缓冲区过大。

  • ALSA 设备缓存未调整。

  • 线程调度优先级低。

调试方法

  1. 调整周期大小

    snd_pcm_set_params(..., 256, ...)
  2. 使用 MMAP 模式

    snd_pcm_hw_params_set_access(..., SND_PCM_ACCESS_MMAP_INTERLEAVED)
  3. 设置实时优先级

    pthread_setschedparam(thread, SCHED_RR, &param)

第六章 与其他组件的协同

组件 协同方式 调试关键点
ALSA 驱动 提供 PCM 设备接口 延迟、采样率
FFmpeg 解码/编码音频文件 格式支持、精度
SpeexDSP 实时降噪处理 噪声阈值、帧大小
PulseAudio 高级音频路由 格式转换、缓冲区
ALSA 中间层 管理 ALSA 设备 设备名称、参数
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐