一.Audio Codec的必要性
在理想状况下,对于录音过程,只需要将麦克风获取到的analog信号通过ADC转换为digital信号并存储即可,对于播放音过程,只需要将digital信号通过DAC转换为analog并输出到speaker播放即可。
但在实际的过程中,对于录音过程而言,会受到外界声源的干扰,麦克风自身对信号的衰减以及物理链路接口上引入的杂音等因素的影响,对于放音,可能会受digital数据本身的问题等因素的影响。
举个简单的例子,拿着手机或者固定电话和别人讲话的时候,虽然一边自己说话,一边听电话另外一端的人讲话,但是从听筒中并没有非常明显的听到自己的讲话声音。这中间就是一些Audio Codec在起作用,它们可以实现回音消除,噪音抵消,以及ALC/Limiter等,当然它也实现了最重要的AD和DA功能。
二.RT5651设备树配置
&i2c1 { status = "okay"; i2c-scl-rising-time-ns = <300>; i2c-scl-falling-time-ns = <15>; rt5651: rt5651@1a { #sound-dai-cells = <0>; compatible = "rockchip,rt5651"; reg = <0x1a>; clocks = <&cru SCLK_I2S_8CH_OUT>; clock-names = "mclk"; status = "okay"; }; };
其中:
status :指定设备状态为“正常”,表示该设备状态为正常运行;
i2c-scl-rising-time-ns:定义了SCL信号上升时间的最小值,单位是纳秒;
i2c-scl-falling-time-ns:定义了SCL信号下降时间的最小值,单位是纳秒;
接着定义I2C从设备节点rt5651,即音频编解码器的设备节点,其名称为 rt5651,I2C从设备7位地址为0x1a;
compatible:指定设备驱动程序的兼容性,即告诉内核该设备可以被哪些驱动程序所使用;
reg:指定了rt5651设备在I2C控制器上的设备地址;
clock-names:指定时钟名称,"mclk"表示MCLK时钟;
clocks:mclk时钟来自SCLK_I2S_8CH_OUT;
status :指定设备状态为“正常”,表示该设备状态为正常运行;
三.Codec驱动分析
rt5651驱动路径:
kernel-5.10\sound\soc\codecs\rt5651.c
rt5651_i2c_driver
这里我们需要关注一下i2c_driver结构体变量rt5651_i2c_driver :
static struct i2c_driver rt5651_i2c_driver = { .driver = { .name = "rt5651", .acpi_match_table = ACPI_PTR(rt5651_acpi_match), .of_match_table = of_match_ptr(rt5651_of_match), }, .probe = rt5651_i2c_probe, .id_table = rt5651_i2c_id, };
其成员:
driver.of_match_table:用于设备树匹配;
probe:当I2C驱动和I2C从设备信息匹配成功之后,就会调用probe函数;
id_table:id列表,用于和I2C从设备名称进行匹配;
I2C从设备驱动中的rt5651_i2c_id匹配成功,会执行probe探测函数:
static int rt5651_i2c_probe(struct i2c_client *i2c) // 参数为I2C从设备 { struct rt5651_priv *rt5651; int ret; int err; rt5651 = devm_kzalloc(&i2c->dev, sizeof(*rt5651), // 动态申请内存,数据结构类型为struct rt5651_priv GFP_KERNEL); if (NULL == rt5651) return -ENOMEM; ...... ret = devm_snd_soc_register_component(&i2c->dev, // 注册component &soc_component_dev_rt5651, rt5651_dai, ARRAY_SIZE(rt5651_dai)); return ret; }
执行probe函数后,这里重点看devm_snd_soc_register_component函数,
调用devm_snd_soc_register_component注册的component,该函数会动态申请一个component,并将其添加到全局链表component_list中,同时会建立dai_driver与component的关系。
codec驱动注册流程主要包含一下几个步骤:
(1) 构造一个struct snd_soc_component_driver实例,比如这里的soc_component_dev_rt5651,用于描述codec driver;需要初始化成员name、controls、dapm_widgets、dapm_routes等;
(2) 构造一个struct snd_soc_dai_driver,比如这里的rt5651_dai数组,用于描述dai和 pcm的能力和操作;需要初始化成员name、probe、playback、capture、ops等;
(3) 调用devm_snd_soc_register_component注册component;
devm_snd_soc_register_component函数第二个参数为soc_component_dev_rt5651
soc_component_dev_rt5651:
static const struct snd_soc_component_driver soc_component_dev_rt5651 = { .probe = rt5651_probe, .suspend = rt5651_suspend, .resume = rt5651_resume, .set_bias_level = rt5651_set_bias_level, .set_jack = rt5651_set_jack, .controls = rt5651_snd_controls, // kcontrol定义 .num_controls = ARRAY_SIZE(rt5651_snd_controls), .dapm_widgets = rt5651_dapm_widgets, // widget定义 .num_dapm_widgets = ARRAY_SIZE(rt5651_dapm_widgets), .dapm_routes = rt5651_dapm_routes, // route定义 .num_dapm_routes = ARRAY_SIZE(rt5651_dapm_routes), .use_pmdown_time = 1, .endianness = 1, };
其中controls、dapm_widgets、dapm_routes是与dapm相关的,可以用来表述codec内部的音频路径。
DAPM简介:
DAPM是Dynamic Audio Power Management的缩写,直译过来就是动态音频电源管理的意思,DAPM是为了使基于linux的移动设备上的音频子系统,在任何时候都工作在最小功耗状态下。DAPM对用户空间的应用程序来说是透明的,所有与电源相关的开关都在ASoc core中完成。DAPM根据当前激活的音频流(playback/capture)和声卡中的mixer等的配置来决定那些音频控件的电源开关被打开或关闭。
在datasheel里面描述了音频数据流具体路径细节,如下图:
上图中我们使用箭头标识了一条用于多媒体音频播放右声道的路径,音频通路是:
AIF1 Playback(snd_soc_dapm_dai_in类型的playback dai widget) --> AIF1RX :AIF表示音频数字接口;
AIF1RX --> IF1 DAC;
IF1 DAC --> IF1 DAC1 R;
IF1 DAC1 R --> DAC MIXR:通过rt5651_dac_r_mix(名称为INF1 Switch)控制通断,由MX29寄存器位14来实现静音控制(0非静音,1静音);
DAC MIXR --> Audio DSP;
Audio DSP --> Stereo DAC MIXR:通过rt5651_sto_dac_r_mix(名称DAC R1 Switch)控制通断,由MX2A寄存器位6来实现静音控制(0非静音,1静音);
Stereo DAC MIXR --> DAC R1;
DAC R1 --> OUT MIXR :通过rt5651_out_r_mix(名称为DAC R1 Switch)控制通断,由MX52寄存器位0来实现静音控制(0非静音,1静音);
OUT MIXR --> HPOVOL R:通过hpovol_r_control(名称为Switch)控制通断,由MX02寄存器位6来实现静音控制(0非静音,1静音);
HPOVOL R --> HPOR MIX:通过rt5651_hpo_mix(名称为HPO MIX HPVOL Switch)控制通断,由MX45寄存器位13来实现静音控制(0非静音,1静音);
HPOR MIX --> HP Amp;
HP Amp -> HPO R Playback:通过hpo_r_mute_control(名称为Switch)控制通断,由MX02寄存器位7来实现静音控制(0非静音,1静音);
HPO R Playback --> HPOR ;
HPOR --> Headphones(最后一个path定义在Machine驱动中);
其中红色部分表示有相应的kcontrol,即需要switch打开,在该路径中 HPOL 为SND_SOC_DAPM_EP_SINK类型端点,但是路径中并没有SND_SOC_DAPM_EP_SOURCE类型端点。
kcontrol:
在sound/soc/codecs/rt5651.c定义了大量的kcontrol,包括普通kcontrol和dapm kcontrol:
通常,一个kcontrol代表着一个mixer(混音器),或者是一个mux(多路开关),又或者是一个音量控制器等等。
kcontrol的定义例如:
static const struct snd_kcontrol_new rt5651_sto1_adc_l_mix[] = { SOC_DAPM_SINGLE("ADC1 Switch", RT5651_STO1_ADC_MIXER, RT5651_M_STO1_ADC_L1_SFT, 1, 1), SOC_DAPM_SINGLE("ADC2 Switch", RT5651_STO1_ADC_MIXER, RT5651_M_STO1_ADC_L2_SFT, 1, 1), };
widget:
在sound/soc/codecs/rt5651.c定义了大量的widget:
widget把kcontrol和动态电源管理进行了有机的结合,同时还具备音频路径的连接功能,一个widget可以与他相邻的widge有某种动态的连接关系。
widget的定义例如:
SND_SOC_DAPM_MIXER("Stereo1 ADC MIXL", SND_SOC_NOPM, 0, 0, rt5651_sto1_adc_l_mix, ARRAY_SIZE(rt5651_sto1_adc_l_mix)),
route:
在sound/soc/codecs/rt5651.c定义了widget的链接路径:
系统中注册的各种widget需要互相连接在一起才能协调工作,连接关系通过snd_soc_dapm_route结构来定义.
在rt5651_dapm_widgets中我们可以找到位于多媒体音频播放右声道路径上的route:
{"IF1 DAC", NULL, "AIF1RX"}, {"IF1 DAC1 R", NULL, "IF1 DAC"}, {"DAC MIXR", "INF1 Switch", "IF1 DAC1 R"}, {"Audio DSP", NULL, "DAC MIXR"}, {"Stereo DAC MIXR", "DAC R1 Switch", "Audio DSP"}, {"DAC R1", NULL, "Stereo DAC MIXR"}, {"OUT MIXR", "DAC R1 Switch", "DAC R1"} {"HPOVOL R", "Switch", "OUT MIXR"}, {"HPOR MIX", "HPO MIX HPVOL Switch", "HPOVOL R"}, {"HP Amp", NULL, "HPOR MIX"}, {"HPO R Playback", "Switch", "HP Amp"}, {"HPOR", NULL, "HPO R Playback"},
rt5651_probe:
static int rt5651_probe(struct snd_soc_component *component) { struct rt5651_priv *rt5651 = snd_soc_component_get_drvdata(component); // 取出component->dev设备的driver_data,就是上面介绍的rt5651结构变量 rt5651->component = component; // 设置cpmponent snd_soc_component_update_bits(component, RT5651_PWR_ANLG1, // 向寄存器写入值,RT5651_PWR_ANLG1的值为0x63,寄存器地址0x63用于电源控制寄存器3 RT5651_PWR_LDO_DVO_MASK, RT5651_PWR_LDO_DVO_1_2V); // RT5651_PWR_LDO_DVO_MASK值为0x03 RT5651_PWR_LDO_DVO_1_2V值为2 因此这里向位[1:0]写入10'b // 即配置LDO output电压为1.2V snd_soc_component_force_bias_level(component, SND_SOC_BIAS_OFF); // Set the COMPONENT DAPM bias level,即dapm->bias_level=0 rt5651_apply_properties(component); return 0; }
rt5651_set_bias_level:
set_bias_level用于设置codec域的偏置电压,那什么是偏置电压呢?在电容式麦克风中,为了使麦克风的工作点稳定,需要加一个直流电压,这个直流电压就是偏置电压,偏置电压的作用主要有以下几个方面:
稳定麦克风的工作点;在没有偏置电压的情况下,麦克风的输出信号会受到温度、湿度等环境因素的影响,导致输出信号的偏移,而偏置电压可以保持麦克风的工作不变,保证输出信号的稳定性;
提高麦克风的灵敏度,偏置电压可以使麦克风的灵敏度增加,从而提高声音的捕获能力;
降低麦克风的噪声;偏移电压可以降低麦克风的噪声水平,使得麦克风的输出信号更清晰;
对于ALC5651芯片MICBIAS1引脚会输出一个偏置电压,提供给外置麦克风:
static int rt5651_set_bias_level(struct snd_soc_component *component, enum snd_soc_bias_level level) { switch (level) { case SND_SOC_BIAS_PREPARE: // 准备状态 // 获取dapm->bias_level,待机->准备 if (SND_SOC_BIAS_STANDBY == snd_soc_component_get_bias_level(component)) { // RT5651_PLL_MODE_1的值为0x83,寄存器0x83为ASRC控制寄存器,这里是判断位[15]、[12]、[9]是否为1 // 其中位[15]为I2S1模式选择控制 0:正常模式 1:ASRC模式 // 位[12]为I2S2模式选择控制 0:正常模式 1:ASRC模式 // 位[9] Select Control for ASRC Mode in DMIC1 Function 0:正常模式 1:ASRC模式 if (snd_soc_component_read(component, RT5651_PLL_MODE_1) & 0x9200) // RT5651_D_MISC的值为0xFA,寄存器地址0xFA为基本控制寄存器 snd_soc_component_update_bits(component, RT5651_D_MISC, 0xc00, 0xc00); // 这里向位[11:10]写入11'b,芯片手册中并没有描述这两位有什么作用 } break; case SND_SOC_BIAS_STANDBY: // 待机状态 // 获取dapm->bias_level,关闭状态->待机状态 if (SND_SOC_BIAS_OFF == snd_soc_component_get_bias_level(component)) { // RT5651_PWR_ANLG1的值为0x63,寄存器地址0x63用于电源控制寄存器3 snd_soc_component_update_bits(component, RT5651_PWR_ANLG1, // RT5651_PWR_VREF1位[15],VREF1 Power Control:0下电、1上电 // RT5651_PWR_MB位[13],MBIAS Power Control:0下电、1上电 // RT5651_PWR_BG位[11],MBIAS Bandgap Power Control:0下电、1上电 // RT5651_PWR_VREF2位[4],VREF2 Power Control:0下电、1上电 RT5651_PWR_VREF1 | RT5651_PWR_MB | RT5651_PWR_BG | RT5651_PWR_VREF2, RT5651_PWR_VREF1 | RT5651_PWR_MB | RT5651_PWR_BG | RT5651_PWR_VREF2); usleep_range(10000, 15000); // RT5651_PWR_FV1位[14],VREF1 Fast Mode Control:0 Fast VREF、1 Slow VREF, (For good analog performance) // RT5651_PWR_FV2位[0],VREF2 Fast Mode Control:0 Fast VREF、1 Slow VREF, (For good analog performance) snd_soc_component_update_bits(component, RT5651_PWR_ANLG1, RT5651_PWR_FV1 | RT5651_PWR_FV2, RT5651_PWR_FV1 | RT5651_PWR_FV2); // RT5651_D_MISC的值为0xFA,寄存器地址0xFA为基本控制寄存器,芯片手册中并没有描述位[1]有什么作用 snd_soc_component_update_bits(component, RT5651_D_MISC, 0x1, 0x1); } break; case SND_SOC_BIAS_OFF: // 关闭状态 snd_soc_component_write(component, RT5651_D_MISC, 0x0010); snd_soc_component_write(component, RT5651_PWR_DIG1, 0x0000); snd_soc_component_write(component, RT5651_PWR_DIG2, 0x0000); snd_soc_component_write(component, RT5651_PWR_VOL, 0x0000); snd_soc_component_write(component, RT5651_PWR_MIXER, 0x0000); /* Do not touch the LDO voltage select bits on bias-off */ snd_soc_component_update_bits(component, RT5651_PWR_ANLG1, ~RT5651_PWR_LDO_DVO_MASK, 0); /* Leave PLL1 and jack-detect power as is, all others off */ snd_soc_component_update_bits(component, RT5651_PWR_ANLG2, ~(RT5651_PWR_PLL | RT5651_PWR_JD_M), 0); break; default: break; } return 0; }
rt5651_set_jack:
static int rt5651_set_jack(struct snd_soc_component *component, struct snd_soc_jack *jack, void *data) { if (jack) rt5651_enable_jack_detect(component, jack, data); else rt5651_disable_jack_detect(component); return 0; }
在ASoC中使用struct snd_soc_jack来描述jack,并提供了对其状态、引脚等进行管理和通知的功能。这里如何定义了jack,将会调用rt5651_enable_jack_detect,用于实现麦克风插入/拔出的检测。
rt5651_dai:
数字音频接口DAI,即Digital Audio Interfaces,顾名思义,DAI表示在板级或板间传输数字音频信号的方式。
由于ALC5651有两组I2S接口,可以同时用于耳机输出以及Line output;RK3568与ALC5651连线如下:
* * * <------ * <---- MIC RAM <--------PCM-> * <----------> * <-I2S------------I2S1-> * * * * | * ------> * ----> Line output **************** | * * RK3568 | * * -----I2S2-> * ------> * ----> HEADPHONE *********** ALC5651
codec dai和pcm配置信息通过结构体snd_soc_dai_driver描述,包括了dai的能力描述和操作接口;devm_snd_soc_register_component函数第三个参数为rt5651_dai,数组中长度为2,分别与ALC5651的两组I2S接口一一对应;
static struct snd_soc_dai_driver rt5651_dai[] = { { .name = "rt5651-aif1", // dai的名称,会赋值给与其关联的snd_soc_dai的name成员 // 音频数据链路是通过dai_name到ALC5651的component的dai_list链表中来查找dai的 .id = RT5651_AIF1, // 0,dai的id .playback = { // 声卡注册的时候会为其创建一个类型为snd_soc_dapm_dai_in的playback dai widget,其name以及sname均设置为"AIF1 Playback" .stream_name = "AIF1 Playback", .channels_min = 1, // 最小通道数 .channels_max = 2, // 最大通道数 .rates = RT5651_STEREO_RATES, .formats = RT5651_FORMATS, }, .capture = { // 声卡注册的时候会为其创建一个类型为snd_soc_dapm_dai_out的capture dai widget,其name以及sname均设置为"AIF1 Capture" .stream_name = "AIF1 Capture", .channels_min = 1, .channels_max = 2, .rates = RT5651_STEREO_RATES, // 支持的采样率 SNDRV_PCM_RATE_8000_96000 999~96000之间 .formats = RT5651_FORMATS, // 支持的位深度 (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S8) }, .ops = &rt5651_aif_dai_ops, }, { .name = "rt5651-aif2", .id = RT5651_AIF2, // 1,dai的id .playback = { .stream_name = "AIF2 Playback", .channels_min = 1, .channels_max = 2, .rates = RT5651_STEREO_RATES, .formats = RT5651_FORMATS, }, .capture = { .stream_name = "AIF2 Capture", .channels_min = 1, .channels_max = 2, .rates = RT5651_STEREO_RATES, .formats = RT5651_FORMATS, }, .ops = &rt5651_aif_dai_ops, }, };
其中:
name:codec dai的名称标识,dai_link通过配置codec dai_name来找到对应的codec dai;
capture:描述capture的能力;如回放设备所支持的声道数、采样率、音频格式;非常重要的字段;
playback:描述playback的能力;如录制设备所支持声道数、采样率、音频格式;非常重要的字段;
ops:codec dai的操作函数集,这些函数集非常重要,用于dai的时钟配置、格式配置、硬件参数配置。
rt5651_aif_dai_ops:
static const struct snd_soc_dai_ops rt5651_aif_dai_ops = { .hw_params = rt5651_hw_params, .set_fmt = rt5651_set_dai_fmt, .set_sysclk = rt5651_set_dai_sysclk, .set_pll = rt5651_set_dai_pll, };
其中:
set_sysclk:用于设置系统时钟,对于codec dai来说系统时钟指的是ALC5651 i2s1接口的MCLK信号线输入的时钟,同时也是RK3399 i2s0接口的MCLK信号线输出的时钟(对应的时钟名称为clk_i2sout);当上层打开pcm设备时,需要回调该接口设置ALC5651的系统时钟,ALC5651才能正常工作;
set_pll:用于设置ALC5651的PLL的分频系数,ALC5651一般接了一个MCLK作为ALC5651的PLL输入时钟源,回调该函数基于MCLK来产生ALC5651 PLL时钟;
set_fmt:设置数字音频接口格式,具体见 include/sound/soc-dai.h;
SND_SOC_DAIFMT_I2S:数字音频接口是I2S格式,常用于多媒体音频;
SND_SOC_DAIFMT_RIGHT_J:数字音频接口是I2S右对齐格式;
SND_SOC_DAIFMT_LEFT_J:数字音频接口是I2S左对齐格式;
SND_SOC_DAIFMT_DSP_A:数字音频接口是PCM格式,常用于语音通话;
SND_SOC_DAIFMT_DSP_B:数字音频接口是PCM格式,常用于语音通话;
SND_SOC_DAIFMT_CBM_CFM:ALC5651作为主机,BCLK 和 LRCLK由ALC5651提供;
SND_SOC_DAIFMT_CBS_CFS:ALC5651作为从机,BCLK和LRCLK由SoC/CPU提供;
.......
hw_params:codec dai硬件参数设置,根据上层设定的声道数、采样率、数据格式,来配置codec dai相关寄存器;