Linux 5.10 Sound 全流程
第一部分 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 未上电。
调试方法:
-
检查 I2C 地址:
i2cdetect -y <bus>
-
强制设置频率:
echo 100000 > /sys/class/i2c-adapter/i2c-<bus>/power/rate
-
使用逻辑分析仪:观察 SDA/SCL 信号。
4.2 音频无声
现象:aplay 正常,但耳机/扬声器无声。
原因:
-
电源域未完全启用。
-
音量设为 0。
-
音频路由路径未正确配置。
调试方法:
-
检查电源寄存器:
devmem2 <i2c_addr>+0x08 # WM8960_POWER_MANAGEMENT_1 devmem2 <i2c_addr>+0x09 # WM8960_POWER_MANAGEMENT_2
-
检查音量:
amixer sset 'Headphone' 80% amixer sset 'Speaker' 80%
-
检查路由:
# 查看 DAPM 状态 cat /sys/kernel/debug/asoc/wm8960/DAPM/status
4.3 采样率不匹配导致卡顿
现象:音频播放时出现爆音或卡顿。
原因:
-
BCLK/LRCLK 分频计算错误。
-
PLL 未正确锁定。
-
主时钟频率不匹配。
调试方法:
-
检查时钟寄存器:
devmem2 <i2c_addr>+0x0D # WM8960_CLOCKING_1 devmem2 <i2c_addr>+0x0E # WM8960_CLOCKING_2
-
测量 BCLK/LRCLK:使用示波器。
-
强制使用固定分频:
# 在驱动中硬编码分频值
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| 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被过早调用(之前未分配足够资源)。
调试方法:
-
检查日志:
dmesg | grep snd
-
使用
perf跟踪:perf record -e snd_* -a -- modprobe virtual_audio_driver
-
检查约束:确保
snd_pcm_new成功。
5.2 PCM 访问失败
现象:aplay 输出 aplay: main:788: audio open error: No such file or directory。
原因:
-
PCM 设备未创建。
-
设备节点未生成。
调试方法:
-
检查设备节点:
ls -l /dev/snd/pcm*
-
检查
snd_card_register:cat /proc/devices | grep pcm
-
直接查看注册的声卡:
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) 框架,分为三层:
-
Machine 驱动:描述整个音频系统,将 CPU DAI 和 Codec DAI 绑定。
-
Platform 驱动(本篇文章重点):即 I2S 控制器驱动,作为 CPU DAI。
-
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 未正确初始化。
调试方法:
-
检查 I2S 寄存器:
devmem2 <base>+0x00 # I2S_CTRL devmem2 <base>+0x1C # I2S_CLK_CFG
-
检查时钟频率:
cat /sys/kernel/debug/clk/clk_summary | grep i2s
-
测试 Codec:
# 使用 amixer 检查 Codec 音量 amixer scontrol amixer sset 'Headphone' 80%
5.2 音频卡顿或爆音
现象:播放音频时出现周期性卡顿或爆音。
原因:
-
DMA 传输延迟。
-
BCLK 频率不稳定。
-
缓冲区不足。
调试方法:
-
检查 DMA 状态:
devmem2 <base>+0x24 # I2S_DMA_STATUS
-
增加缓冲区大小:
# 在 aplay 中使用 --buffer-size 参数 aplay --buffer-size=4096 test.wav
-
检查 BCLK 精度:
# 测量 BCLK 频率 echo "bclk_rate=1000000" > /sys/kernel/debug/clk/clk_debug
5.3 录音数据错误
现象:录制的音频数据全为零或杂音。
原因:
-
Codec 录音通道未使能。
-
采样率不匹配。
-
数据位宽配置错误。
调试方法:
-
检查 Codec 录音使能:
amixer sset 'Capture' 80% amixer sset 'Mic' on
-
检查采样率:
cat /proc/asound/card0/stream0
-
检查数据格式:
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/ 访问 ↓ [应用层]
关键接入点:
-
I2C 层面:
i2c_driver->wm8960_probe() -
ASoC Codec 层面:
wm8960_probe()->snd_soc_register_codec() -
ASoC DAI 层面:
wm8960_dai描述 Codec 的音频接口 -
控制层面:
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/ 没有 controlC0 或 pcmC0D0p。
原因:
-
Codec 在 ASoC 中未成功注册。
-
Machine 驱动的 DAI 链接名称不匹配。
-
I2C 通信失败导致 Codec 无法被识别。
调试方法:
-
检查 ASoC 注册日志:
dmesg | grep "ASoC" dmesg | grep "wm8960"
-
检查 Codec 是否在 ASoC 列表中:
cat /proc/asound/cards
-
检查 I2C 地址:
i2cdetect -y <bus>
-
使用 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 总线被占用或频率过高。
调试方法:
-
确认硬件地址:
i2cdetect -y <bus> -
检查复位 GPIO:
cat /sys/kernel/debug/gpio | grep reset -
使用逻辑分析仪:观察 SDA/SCL 时序。
4.3 控制控件不生效 (kcontrol)
现象:amixer sset 'Headphone Volume' 80% 无效果,但 regmap 显示寄存器值已被修改。
原因:
-
电源未启用,DAC/混音器处于关闭状态。
-
DAPM 路由未正确配置。
-
音频接口主/从模式不匹配。
调试方法:
-
检查电源寄存器:
devmem2 <i2c_addr>+0x08 devmem2 <i2c_addr>+0x09
-
检查 DAPM 状态:
cat /sys/kernel/debug/asoc/wm8960/DAPM/status
-
检查 I2S 格式:
cat /sys/kernel/debug/asoc/wm8960/registers
4.4 播放/捕获无声
现象:aplay 正常,但耳机无声;arecord 正常,但录制的文件无声音。
原因:
-
音频路径未使能(如 Mixer 关闭)。
-
主时钟 (MCLK) 未提供。
-
采样率不匹配。
调试方法:
-
检查 DAPM 路由:
cat /sys/kernel/debug/asoc/card0/DAPM/status
-
测量 MCLK:使用示波器。
-
强制启用混音器:
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 录制无声。
原因:
-
麦克风电源未启用。
-
混音器路由未配置。
-
偏置电压未提供。
调试方法:
-
检查电源寄存器:
devmem2 <i2c_addr>+0x09 # WM8960_POWER_MANAGEMENT_2
-
检查混音器寄存器:
devmem2 <i2c_addr>+0x21 # WM8960_LEFT_INPUT_MIXER
-
检查偏置电压:
# 使用示波器测量 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(¶ms);
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(¶ms);
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 录制文件无声音。
原因:
-
麦克风电源未启用。
-
混音器路由未配置。
-
偏置电压未提供。
调试方法:
-
检查音量设置:
amixer sset 'Mic' 80% amixer sset 'Mic Boost' 20dB
-
检查路由:
amixer sset 'Left Input Mixer' on amixer sset 'Right Input Mixer' on
-
检查电源:
devmem2 <i2c_addr>+0x09 # WM8960_POWER_MANAGEMENT_2
5.2 降噪效果不佳
现象:背景噪声仍然明显。
原因:
-
降噪阈值设置不当。
-
噪声模型不匹配。
-
滤波器参数未优化。
调试方法:
-
调整噪声等级:
echo "nr=20" > /sys/class/audio/noise_reduction/params
-
使用频谱分析:
ffmpeg -i input.wav -af "showspectrum" output.png
-
测试不同算法:
# 比较 SpeexDSP 和 FFmpeg afftdn
5.3 环回延迟过高
现象:听到回声,延迟明显。
原因:
-
缓冲区过大。
-
ALSA 设备缓存未调整。
-
线程调度优先级低。
调试方法:
-
调整周期大小:
snd_pcm_set_params(..., 256, ...)
-
使用 MMAP 模式:
snd_pcm_hw_params_set_access(..., SND_PCM_ACCESS_MMAP_INTERLEAVED)
-
设置实时优先级:
pthread_setschedparam(thread, SCHED_RR, ¶m)
第六章 与其他组件的协同
| 组件 | 协同方式 | 调试关键点 |
|---|---|---|
| ALSA 驱动 | 提供 PCM 设备接口 | 延迟、采样率 |
| FFmpeg | 解码/编码音频文件 | 格式支持、精度 |
| SpeexDSP | 实时降噪处理 | 噪声阈值、帧大小 |
| PulseAudio | 高级音频路由 | 格式转换、缓冲区 |
| ALSA 中间层 | 管理 ALSA 设备 | 设备名称、参数 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)