关键流程设计
初始化流程
AudioCodec 模块的初始化流程如下:
-
释放 clock 和 reset 信号
-
释放 AudioCodec 模块的全局复位信号
-
配置 playback 和 capture 的 DMA 传输参数
-
注册 codec 端 component driver 和 codec_dai driver
-
注册 platform 端 component driver 和 cpu_dai driver
-
创建声卡设备,初始化声卡的各个参数实例
-
注册声卡
音频通路设置
在 AudioCodec 驱动中,非常重要的环节是音频通路的设置,音频通路是音频数据的流通方向,音频通路设置的正确与否关系到声卡是否能正常工作。Linux 内核中,引入了 DAPM 机制,对音频通路进行动态的上下电管理,降低系统的功耗。AudioCodec 的音频通路设计如下:
如上图所示,在 capture 端,左右声道的音频信号依次经过 DMICI/F、DF、HPF、DVC,输出的 OUT 音频信号可以直接存储到 RXFIFO 中,或是经过混音器 MIX0 或 MIX1 直接输出到 playback 端。capture 通路中,HPF 和 DVC 都可以 bypass,音频信号可以不经过这两个模块的处理,直接旁路到下一个模块。
在 playback 端,MIX0 和 MIX1 的音频输入信号,可以来自 TXFIFO,也可以来自 OUT。MIX0 和 MIX1 的输出信号依次经过 DVC、IF、FADE、SDM、PWM 输出,实现音频的播放功能。playback 通路中,DVC 和 FADE 都可以 bypass,音频信号可以不经过这两个模块的处理,直接旁路到下一个模块。
capture 端的音频通路如下图所示:
playback 端的音频通路如下图所示:
音频通路的注册
在 ALSA 的 DAPM 中,音频通路上的结点用 widget 表示,widget 可以认为是对音频控件的进一步封装,它把音频控件和电源管理进行了结合,同时还具备音频路径的连接功能。实现音频通路的注册,需要以下几个步骤:
-
先把音频路径中的结点初始化为 widget 类型的实例:
static const struct snd_soc_dapm_widget aic_codec_dapm_widgets[] = { SND_SOC_DAPM_ADC("DMICIF", "Capture-dmic", RX_DMIC_IF_CTRL_REG, RX_DMIC_IF_EN, 0), /* MUX */ SND_SOC_DAPM_MUX("PWM ch0", TX_PWM_CTRL_REG, TX_PWM0_EN, 0, &pwm0_output_mux), SND_SOC_DAPM_MUX("PWM ch1", TX_PWM_CTRL_REG, TX_PWM1_EN, 0, &pwm1_output_mux), SND_SOC_DAPM_MUX("HPF", SND_SOC_NOPM, 0, 0, &hpf_mux), SND_SOC_DAPM_MUX("FADE ch0", SND_SOC_NOPM, 0, 0, &fade0_mux), SND_SOC_DAPM_MUX("FADE ch1", SND_SOC_NOPM, 0, 0, &fade1_mux), SND_SOC_DAPM_MUX("ADC HPF", SND_SOC_NOPM, 0, 0, &adc_hpf_mux), /* Sigma-Delta Modulation */ SND_SOC_DAPM_DAC("SDM ch0", "Playback", TX_SDM_CTRL_REG, TX_SDM_CH0_EN, 0), SND_SOC_DAPM_DAC("SDM ch1", "Playback", TX_SDM_CTRL_REG, TX_SDM_CH1_EN, 0), /* PGA */ SND_SOC_DAPM_PGA("DVC 0", ADC_DVC0_CTRL_REG, ADC_DVC0_CTRL_DVC0_EN, 0, NULL, 0), SND_SOC_DAPM_PGA("DVC 1", RX_DVC_1_2_CTRL_REG, RX_DVC1_EN, 0, NULL, 0), SND_SOC_DAPM_PGA("DVC 2", RX_DVC_1_2_CTRL_REG, RX_DVC2_EN, 0, NULL, 0), SND_SOC_DAPM_PGA("DVC 3", TX_DVC_3_4_CTRL_REG, TX_DVC3_EN, 0, NULL, 0), SND_SOC_DAPM_PGA("DVC 4", TX_DVC_3_4_CTRL_REG, TX_DVC4_EN, 0, NULL, 0), SND_SOC_DAPM_PGA("PGA", ADC_CTL1_REG, ADC_CTL1_PGA_EN, 0, NULL, 0), /* Mixer */ SND_SOC_DAPM_MIXER("MIXER0", SND_SOC_NOPM, 0, 0, aic_codec_mixer0_controls, ARRAY_SIZE(aic_codec_mixer0_controls)), SND_SOC_DAPM_MIXER("MIXER1", SND_SOC_NOPM, 0, 0, aic_codec_mixer1_controls, ARRAY_SIZE(aic_codec_mixer1_controls)), /* SUPPLY */ SND_SOC_DAPM_SUPPLY("FADE", FADE_CTRL0_REG, FADE_CTRL0_EN, 0, NULL, 0), SND_SOC_DAPM_SUPPLY("IF", TX_PLAYBACK_CTRL_REG, TX_PLAYBACK_IF_EN, 0, NULL, 0), SND_SOC_DAPM_SUPPLY("Mic Bias", ADC_CTL1_REG, ADC_CTL1_MBIAS_EN, 0, NULL, 0), SND_SOC_DAPM_SUPPLY("TX GLBEN", GLOBE_CTL_REG, GLOBE_TX_GLBEN, 0, NULL, 0), SND_SOC_DAPM_SUPPLY("RX GLBEN", GLOBE_CTL_REG, GLOBE_RX_GLBEN, 0, NULL, 0), SND_SOC_DAPM_DAC("IF ch0", "Playback", TX_PLAYBACK_CTRL_REG, TX_IF_CH0_EN, 0), SND_SOC_DAPM_DAC("IF ch1", "Playback", TX_PLAYBACK_CTRL_REG, TX_IF_CH1_EN, 0), /* AIF OUT */ SND_SOC_DAPM_AIF_OUT("AUDOUTL", "Playback", 0, TXFIFO_CTRL_REG, TXFIFO_CH0_EN, 0), SND_SOC_DAPM_AIF_OUT("AUDOUTR", "Playback", 1, TXFIFO_CTRL_REG, TXFIFO_CH1_EN, 0), /* AIF IN */ SND_SOC_DAPM_AIF_IN("DMICOUTL", "Capture-dmic", 0, DMIC_RXFIFO_CTRL_REG, DMIC_RXFIFO_CH0_EN, 0), SND_SOC_DAPM_AIF_IN("DMICOUTR", "Capture-dmic", 1, DMIC_RXFIFO_CTRL_REG, DMIC_RXFIFO_CH1_EN, 0), SND_SOC_DAPM_AIF_IN("ADCOUT", "Capture-adc", 0, ADC_RXFIFO_CTRL_REG, ADC_RXFIFO_EN, 0), /* ADC */ SND_SOC_DAPM_ADC("ADC", "ADC Capture-adc", ADC_CTL1_REG, ADC_CTL1_ADC_EN, 0), SND_SOC_DAPM_INPUT("AMIC"), SND_SOC_DAPM_INPUT("DMIC"), SND_SOC_DAPM_OUTPUT("SPK_OUT0"), SND_SOC_DAPM_OUTPUT("SPK_OUT1"), };
-
利用实例化的 widget 定义 route 信息。DAPM 中,利用 route 结构体表示两个 widget 的连接关系。struct snd_soc_dapm_route 按照 {“目的 widget”, “控件”, “源 widget”} 的方式进行定义。
static const struct snd_soc_dapm_route aic_codec_dapm_route[] = { {"DMICOUTL", NULL, "RX GLBEN"}, {"DMICOUTR", NULL, "RX GLBEN"}, {"DMICIF", NULL, "DMIC"}, {"HPF", "Bypass", "DMICIF"}, {"HPF", "HPF Enable", "DMICIF"}, {"DVC 1", NULL, "HPF"}, {"DVC 2", NULL, "HPF"}, {"DMICOUTL", NULL, "DVC 1"}, {"DMICOUTR", NULL, "DVC 2"}, #ifdef CONFIG_SND_SOC_AIC_CODEC_V1 {"ADCOUT", NULL, "RX GLBEN"}, {"AMIC", NULL, "Mic Bias"}, {"PGA", NULL, "AMIC"}, {"ADC", NULL, "PGA"}, {"ADC HPF", "Bypass", "ADC"}, {"ADC HPF", "HPF Enable", "ADC"}, {"DVC 0", NULL, "ADC HPF"}, {"ADCOUT", NULL, "DVC 0"}, #endif {"AUDOUTL", NULL, "TX GLBEN"}, {"AUDOUTR", NULL, "TX GLBEN"}, /* MIXER */ {"MIXER0", "audoutl switch", "AUDOUTL"}, {"MIXER0", "audoutr switch", "AUDOUTR"}, {"MIXER0", "dmicoutl switch", "DMICOUTL"}, {"MIXER0", "dmicoutr switch", "DMICOUTR"}, #ifdef CONFIG_SND_SOC_AIC_CODEC_V1 {"MIXER0", "adcout switch", "ADCOUT"}, #endif {"MIXER1", "audoutl switch", "AUDOUTL"}, {"MIXER1", "audoutr switch", "AUDOUTR"}, {"MIXER1", "dmicoutl switch", "DMICOUTL"}, {"MIXER1", "dmicoutr switch", "DMICOUTR"}, #ifdef CONFIG_SND_SOC_AIC_CODEC_V1 {"MIXER1", "adcout switch", "ADCOUT"}, #endif {"FADE ch0", NULL, "FADE"}, {"FADE ch1", NULL, "FADE"}, {"IF ch0", NULL, "IF"}, {"IF ch1", NULL, "IF"}, {"DVC 3", NULL, "MIXER0"}, {"IF ch0", NULL, "DVC 3"}, {"FADE ch0", "Bypass", "IF ch0"}, {"FADE ch0", "Fade Enable", "IF ch0"}, {"SDM ch0", NULL, "FADE ch0"}, {"PWM ch0", "Single_ended", "SDM ch0"}, {"PWM ch0", "Differential", "SDM ch0"}, {"SPK_OUT0", NULL, "PWM ch0"}, {"DVC 4", NULL, "MIXER1"}, {"IF ch1", NULL, "DVC 4"}, {"FADE ch1", "Bypass", "IF ch1"}, {"FADE ch1", "Fade Enable", "IF ch1"}, {"SDM ch1", NULL, "FADE ch1"}, {"PWM ch1", "Single_ended", "SDM ch1"}, {"PWM ch1", "Differential", "SDM ch1"}, {"SPK_OUT1", NULL, "PWM ch1"}, };
-
将上面定义的两个数组赋值给 codec 的 component driver
static const struct snd_soc_component_driver aic_codec_component = { .controls = aic_codec_controls, .num_controls = ARRAY_SIZE(aic_codec_controls), .dapm_widgets = aic_codec_dapm_widgets, .num_dapm_widgets = ARRAY_SIZE(aic_codec_dapm_widgets), .dapm_routes = aic_codec_dapm_route, .num_dapm_routes = ARRAY_SIZE(aic_codec_dapm_route), .idle_bias_on = 1, .use_pmdown_time = 1, .endianness = 1, .non_legacy_dai_naming = 1, };
-
调用 component,完成音频路径的注册
音频控件设置
音频通路是音频数据流通的路径,而音频控件是负责音量大小的调节,以及一些开关的控制等功能。AudioCodec 的音频控件有 DVC 的音量调节,DVC、HPF、FADE 旁路选择,MIX 的增益使能,以及 DMICI/F 数据交换开关等。与音频通路的 widget 不同,音频控件一般是不具有动态上下电控制功能的。音频控件的注册需要三个步骤:
-
定义 new 类型的音频控件
static const struct snd_kcontrol_new aic_codec_controls[] = { SOC_DOUBLE_TLV("DMICIN Capture Volume", RX_DVC_1_2_CTRL_REG, RX_DVC1_GAIN, RX_DVC2_GAIN, 0xFF, 0, aic_codec_dvc_scale), SOC_DOUBLE_TLV("AUDIO Playback Volume", TX_DVC_3_4_CTRL_REG, TX_DVC3_GAIN, TX_DVC4_GAIN, 0xFF, 0, aic_codec_dvc_scale), #ifdef CONFIG_SND_SOC_AIC_CODEC_V1 SOC_SINGLE_TLV("ADC Capture Volume", ADC_DVC0_CTRL_REG, ADC_DVC0_CTRL_DVC0_GAIN, 0xFF, 0, aic_codec_dvc_scale), SOC_SINGLE_TLV("PGA Gain", ADC_CTL2_REG, ADC_CTL2_PGA_GAIN_SEL, 0xF, 0, aic_pga_scale), #endif SOC_SINGLE_TLV("MIX1AUDL Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER1_AUDOUTL_GAIN, 1, 1, aic_mixer_source_gain_scale), SOC_SINGLE_TLV("MIX1AUDR Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER1_AUDOUTR_GAIN, 1, 1, aic_mixer_source_gain_scale), SOC_SINGLE_TLV("MIX1DMICL Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER1_DMICOUTL_GAIN, 1, 1, aic_mixer_source_gain_scale), SOC_SINGLE_TLV("MIX1DMICR Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER1_DMICOUTR_GAIN, 1, 1, aic_mixer_source_gain_scale), #ifdef CONFIG_SND_SOC_AIC_CODEC_V1 SOC_SINGLE_TLV("MIX1ADC Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER1_ADCOUT_GAIN, 1, 1, aic_mixer_source_gain_scale), #endif SOC_SINGLE_TLV("MIX0AUDL Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER0_AUDOUTL_GAIN, 1, 1, aic_mixer_source_gain_scale), SOC_SINGLE_TLV("MIX0AUDR Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER0_AUDOUTR_GAIN, 1, 1, aic_mixer_source_gain_scale), SOC_SINGLE_TLV("MIX0DMICL Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER0_DMICOUTL_GAIN, 1, 1, aic_mixer_source_gain_scale), SOC_SINGLE_TLV("MIX0DMICR Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER0_DMICOUTR_GAIN, 1, 1, aic_mixer_source_gain_scale), #ifdef CONFIG_SND_SOC_AIC_CODEC_V1 SOC_SINGLE_TLV("MIX0ADC Playback Gain", TX_MIXER_CTRL_REG, TX_MIXER0_ADCOUT_GAIN, 1, 1, aic_mixer_source_gain_scale), #endif SOC_ENUM("Data Swap Switch", dmicif_data_enum), SOC_ENUM("RXFIFO Delay Time", dmic_rx_dlt_length_enum), SOC_ENUM("RXFIFO Dealy Switch", dmic_rx_dlt_en_enum), SOC_ENUM("AMIC bias voltage level", mic_bias_voltage_enum), SOC_ENUM("PWM0 mode select", pwm0_mode_enum), SOC_ENUM("PWM1 mode select", pwm1_mode_enum), SOC_ENUM("MIXER0 swicth", mixer0_enable_enum), SOC_ENUM("MIXER1 swicth", mixer1_enable_enum), };
-
将定义的音频控件数组赋值给 codec 的 component driver
static const struct snd_soc_component_driver aic_codec_component = { .controls = aic_codec_controls, .num_controls = ARRAY_SIZE(aic_codec_controls), .dapm_widgets = aic_codec_dapm_widgets, .num_dapm_widgets = ARRAY_SIZE(aic_codec_dapm_widgets), .dapm_routes = aic_codec_dapm_route, .num_dapm_routes = ARRAY_SIZE(aic_codec_dapm_route), .idle_bias_on = 1, .use_pmdown_time = 1, .endianness = 1, .non_legacy_dai_naming = 1, };
-
调用 component 完成音频控件的注册。
dmaengine_pcm 注册
在 d211 中,capture 端新增了 amic 通路。在 DMA 传输时,amic 和 dmic 使用不同的 id。所以为了区分不同的 id,在 DTS 中新增了一个结点,用于注册一个新的 pcm 设备。并新增 c 文件,在该文件中实现对新的 pcm 设备的注册。
period-bytes 对齐
static int aic_codec_startup(struct snd_pcm_substream *substream,
struct snd_soc_dai *dai)
{
int ret;
/* Make sure that the period bytes are 8/128 bytes aligned according to
* the DMA transfer requested.
*/
#ifdef CONFIG_SND_SOC_AIC_CODEC_V1
ret = snd_pcm_hw_constraint_step(substream->runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 8);
if (ret < 0) {
dev_err(dai->dev, "Could not apply period step: %d\n", ret);
return ret;
}
ret = snd_pcm_hw_constraint_step(substream->runtime, 0,
SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 8);
if (ret < 0) {
dev_err(dai->dev, "Could not apply buffer step: %d\n", ret);
return ret;
}
#else
ret = snd_pcm_hw_constraint_step(substream->runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 128);
if (ret < 0) {
dev_err(dai->dev, "Could not apply period step: %d\n", ret);
return ret;
}
ret = snd_pcm_hw_constraint_step(substream->runtime, 0,
SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 128);
if (ret < 0) {
dev_err(dai->dev, "Could not apply buffer step: %d\n", ret);
return ret;
}
#endif
return ret;
}
pop 音消除
在音频的驱动设计中,播放上下电时的 pop 音是经常遇到的一个问题。pop 音产生的主要原因如下:
-
音频通路中的控件还未全部闭合时,功放却已处于工作状态。此时当其它控件闭合形成播放通路时,通路中电流的变化经功放放大,形成 pop 音。
-
在正常播放过程中,播放不同采样率的音频文件时,导致 audio 频率切换,此时也容易产生 pop 音。例如,当前播放的是 48K 采样率的音频文件,下一个音频文件的采样率是 22.05K,需要切换 audio 模块的工作频率,切换频率时有可能产生 pop 音。
基于以上原因,为了消除 pop 音的问题,audio 的驱动设计中做了以下几点设计:
-
在 DTS 中添加 gpio 引脚,用于控制功放的使能和禁用
&codec { pinctrl-names = "default"; pinctrl-0 = <&amic_pins>, <&dmic_pins_a>, <&spk_pins_b>; pa-gpios = <&gpio_f 13 GPIO_ACTIVE_LOW>; status = "okay"; };
-
调整音频通路中的上下电顺序,确保通路最后才使能功放。ALSA 中提供的 widget 宏已经对上下电做好了排序,调用相应的宏定义 widget 即可。
static const struct snd_soc_dapm_widget aic_codec_card_dapm_widgets[] = { SND_SOC_DAPM_SPK("Speaker", aic_codec_spk_event), }; static int aic_codec_spk_event(struct snd_soc_dapm_widget *w, struct snd_kcontrol *k, int event) { if (SND_SOC_DAPM_EVENT_ON(event) && !IS_ERR_OR_NULL(gpiod_pa)) gpiod_set_value(gpiod_pa, 1); else if (SND_SOC_DAPM_EVENT_OFF(event) && !IS_ERR_OR_NULL(gpiod_pa)) gpiod_set_value(gpiod_pa, 0); msleep(100); return 0; } static const struct snd_soc_dapm_widget aic_codec_dapm_widgets[] = { ... SND_SOC_DAPM_SUPPLY("TX GLBEN", GLOBE_CTL_REG, GLOBE_TX_GLBEN, 0, NULL, 0), SND_SOC_DAPM_SUPPLY("RX GLBEN", GLOBE_CTL_REG, GLOBE_RX_GLBEN, 0, NULL, 0), ... }
在使能和禁用功放的 widget 时,通过 event 回调函数使能和禁用功放。soc-dapm.c 文件的 seq 和 dapm_down_seq 数组定义了各个 widget 的上下电顺序。而由 SPK 宏定义的功放 widget,处于上电时最后上电,掉电时较早掉电的位置。将 TX 和 RX 的全局使能加入 widget 链路,确保先于功放使能。
-
audio 需要调整频率时,先关闭功放,频率调整后,再将功放恢复到频率调整前的状态。
创建声卡
创建声卡的一个关键步骤是实现 driver 和 codec driver 的耦合,ALSA 框架中,实现二者耦合是通过 link 实现的。在调用 component 时,会把相应的 platform 和 codec 所对应的 component 注册到链表中。在注册声卡时,会根据 link 中定义的 codecs->dai_name 和 cpus->dai_name 进行查找,如果在链表中可以查找到相应的 dai,则耦合成功,否则会耦合失败。