Edit online

关键流程设计

4 Dec 2024
Read time: 12 minute(s)

初始化流程

AudioCodec 模块的初始化流程如下:

  1. 释放 clock 和 reset 信号

  2. 释放 AudioCodec 模块的全局复位信号

  3. 配置 playback 和 capture 的 DMA 传输参数

  4. 注册 codec 端 component driver 和 codec_dai driver

  5. 注册 platform 端 component driver 和 cpu_dai driver

  6. 创建声卡设备,初始化声卡的各个参数实例

  7. 注册声卡

音频通路设置

在 AudioCodec 驱动中,非常重要的环节是音频通路的设置,音频通路是音频数据的流通方向,音频通路设置的正确与否关系到声卡是否能正常工作。Linux 内核中,引入了 DAPM 机制,对音频通路进行动态的上下电管理,降低系统的功耗。AudioCodec 的音频通路设计如下:


data_path

如上图所示,在 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 端的音频通路如下图所示:


capture_data_path

playback 端的音频通路如下图所示:


playback_data_path

音频通路的注册

在 ALSA 的 DAPM 中,音频通路上的结点用 widget 表示,widget 可以认为是对音频控件的进一步封装,它把音频控件和电源管理进行了结合,同时还具备音频路径的连接功能。实现音频通路的注册,需要以下几个步骤:

  1. 先把音频路径中的结点初始化为 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"),
    };
  2. 利用实例化的 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"},
    };
  3. 将上面定义的两个数组赋值给 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,
    };
  4. 调用 component,完成音频路径的注册

音频控件设置

音频通路是音频数据流通的路径,而音频控件是负责音量大小的调节,以及一些开关的控制等功能。AudioCodec 的音频控件有 DVC 的音量调节,DVC、HPF、FADE 旁路选择,MIX 的增益使能,以及 DMICI/F 数据交换开关等。与音频通路的 widget 不同,音频控件一般是不具有动态上下电控制功能的。音频控件的注册需要三个步骤:

  1. 定义 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),
    };
  2. 将定义的音频控件数组赋值给 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,
    };
    
  3. 调用 component 完成音频控件的注册。

dmaengine_pcm 注册

在 d211 中,capture 端新增了 amic 通路。在 DMA 传输时,amic 和 dmic 使用不同的 id。所以为了区分不同的 id,在 DTS 中新增了一个结点,用于注册一个新的 pcm 设备。并新增 c 文件,在该文件中实现对新的 pcm 设备的注册。

period-bytes 对齐

在使用 DMA 传输音频数据时,DMA 要求每次传输的数据长度必须 128bytes/8bytes 对齐。在 ALSA 框架下,音频数据以 period 为周期调用 DMA 传输,每次传输的数据长度为 bytes。所以,必须满足 bytes 按照 128bytes/8bytes 对齐。ALSA 中提供了相应的 API 接口(snd_pcm_hw_constraint_step)来满足这一需求。
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 音产生的主要原因如下:

  1. 音频通路中的控件还未全部闭合时,功放却已处于工作状态。此时当其它控件闭合形成播放通路时,通路中电流的变化经功放放大,形成 pop 音。

  2. 在正常播放过程中,播放不同采样率的音频文件时,导致 audio 频率切换,此时也容易产生 pop 音。例如,当前播放的是 48K 采样率的音频文件,下一个音频文件的采样率是 22.05K,需要切换 audio 模块的工作频率,切换频率时有可能产生 pop 音。

基于以上原因,为了消除 pop 音的问题,audio 的驱动设计中做了以下几点设计:

  1. 在 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";
    };
  2. 调整音频通路中的上下电顺序,确保通路最后才使能功放。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 链路,确保先于功放使能。

  3. audio 需要调整频率时,先关闭功放,频率调整后,再将功放恢复到频率调整前的状态。

创建声卡

创建声卡的一个关键步骤是实现 driver 和 codec driver 的耦合,ALSA 框架中,实现二者耦合是通过 link 实现的。在调用 component 时,会把相应的 platform 和 codec 所对应的 component 注册到链表中。在注册声卡时,会根据 link 中定义的 codecs->dai_name 和 cpus->dai_name 进行查找,如果在链表中可以查找到相应的 dai,则耦合成功,否则会耦合失败。