本文基于Android 4.4和4.2,检测所用codec为wm8994。
Android和Kernel下的mic检测是建立在headset检测的基础上的,具体过程如下:
1) kernel通过Jack检测脚中断检测到有耳机插入
2) 读取codec寄存器判断headset是否带mic
3) 通过InputEvent/UEvent机制通知Android上层
详情可以参看我的前一篇基于耳机插拔检测的文章。本文基于UEvent机制来实现,即 switch driver的方式。
1. mic检测原理
先看看带mic的耳机和不带mic的耳机的差别,如下图,不带mic的耳机为3段,带mic的耳机为4段,比对一下实物可以看出两者左右声道段没有差别,差别之处是不带mic的耳机将GND和MIC两段合并在一起。因而对于不带mic的耳机来说,GND和MIC两段是几乎短路的(有一定电阻),而mic检测就是基于这个原理。
为了实现录音,需要在MIC段施加一定的偏置电压,即micbias,对于没有mic的耳机来说,由于MIC和GND合成为一段,就相当于将micbias接地,因此会产生比较大的电流。一些codec支持电流检测功能,当电流超过某个阈值时,会将相应的寄存器设置为1,从而可以查询得到结果。
2. codec设置
wm8994 codec支持电流检测功能,要使用该功能,需要进行相应设置。
具体的设置可以参考wm8994_mic_detect函数,但我在Linux 3.4.39中调用这个函数后会导致整个播放无声,因此只能手动设置寄存器,以MICBIAS1为例,需要手动设置寄存器代码如下:
/*enable BIAS, MICBIAS1 and VMID in R01h*/ snd_soc_update_bits(codec,WM8994_POWER_MANAGEMENT_1, WM8994_MICB1_ENA_MASK | WM8994_BIAS_ENA |WM8994_VMID_SEL_MASK, WM8994_MICB1_ENA |WM8994_BIAS_ENA | 1 << WM8994_VMID_SEL_SHIFT); /*enable MICBIAS Current Detect*/ snd_soc_update_bits(codec,WM8994_MICBIAS, WM8994_MICD_ENA, reg); /* enable MICDETdeboune */ snd_soc_update_bits(codec,WM8994_IRQ_DEBOUNCE, WM8994_MIC1_DET_DB_MASK, WM8994_MIC1_DET_DB); /* set MICDETthreshold */ snd_soc_update_bits(codec,WM8994_MICBIAS, WM8994_MICD_THR_MASK, 0b100 <<WM8994_MICD_THR_SHIFT);
由于有些寄存器和widget绑定导致dapm会在widget下电时将寄存器disable,因此还需要对codec原有代码做相应修改:
1) vmid_dereference中,对于MICBIAS和VMID的寄存器不应该设置回0,即取消如下行:
snd_soc_update_bits(codec,WM8994_POWER_MANAGEMENT_1, WM8994_BIAS_ENA |WM8994_VMID_SEL_MASK, 0);
2) 对于MICBIAS1对应的widget,将原有的其绑定的寄存器取消,MICBIAS对应widget在wm_hubs.c中,修改如下:
SND_SOC_DAPM_SUPPLY("MICBIAS1",WM8993_POWER_MANAGEMENT_1, 4, 0, NULL, 0),
-> SND_SOC_DAPM_SUPPLY("MICBIAS1", SND_SOC_NOPM, 4, 0, NULL, 0),
3. switch driver中的实现
switch driver的实现在前一篇文章中提到过,这里基于已经实现的switch driver添加关于mic检测的部分即可。
前面提到,对于插拔检测的中断处理函数一般处理成delayed work,防止插拔过程中多次中断,那么在delayed work的回调函数中,流程如下:
1) 读取GPIO电平值,如果为高(低)电平,则耳机插入。具体是高还是低与耳机检测机制有关,大多数为高电平插入。
2) 如果检测到耳机有插入,那么读取wm8994 codec上的寄存器进行进一步判断,代码如下:
mic = wm8994_reg_read(wm8994, WM8994_INTERRUPT_RAW_STATUS_2); mic = (mic & WM8994_MIC2_DET_STS_MASK) >> WM8994_MIC2_DET_STS_SHIFT; if(mic) switch_set_state(&data->sdev, 2); //no mic else switch_set_state(&data->sdev, 1); //with mic
这里的最大问题是,在switch driver中如何访问wm8994的寄存器,即上面wm8994_reg_read的第一个参数wm8994从哪里获得?这里介绍两种方法:
- 将switch device注册为wm8994-core.c的子设备
这里有必要先介绍一下wm8994驱动的结构。由于wm8994的控制部分(寄存器设置)走i2c,所以在linux kernel中,wm8994首先作为一个mfd设备挂载在i2c总线下面,即为wm8994 core(对应文件drivers/mfd/wm8994-core.c)。在wm8994 core下面,有若干个子设备(codec,gpio,regulator)实现不同的功能。
其中codec(对应文件sound/soc/codecs/wm8994.c)就是wm8994 core的一个子设备,这个子设备又注册到了ASOC中。而前面提到的其它子设备(gpio,regulator)也是wm8994 codec硬件的一些其它应用,如可以将codec当作gpio或regulator设备来使用,kernel为这些特殊应用单独实现了driver。
了解了wm8994的大概结构,我这里借鉴了wm8994-regulator.c和gpio-wm8994.c访问wm8994寄存器的方法,将switch device挂载为wm8994-core.c的一个子设备,实现代码如下:
static struct mfd_cell wm8994_devs[] = { { .name= "wm8994-codec", .num_resources= ARRAY_SIZE(wm8994_codec_resources), .resources= wm8994_codec_resources, }, { .name= "wm8994-gpio", .num_resources= ARRAY_SIZE(wm8994_gpio_resources), .resources= wm8994_gpio_resources, .pm_runtime_no_callbacks= true, }, /* hp detect switch driver*/ { .name= "xxx-hp-switch", }, };
这样,通过wm8994_device_init -> mfd_add_devices中就将switch device添加成了wm8994 core的一个子设备。在switch driver的probe函数中,通过如下语句:
structwm8994 *wm8994 = dev_get_drvdata(pdev->dev.parent);
即可得到wm8994结构,从而调用wm8994_reg_read读取寄存器。
- ? 在wm8994 codecdriver中注册switch device
这个方法不需要实现单独的switch driver,整个检测过程中集成在codec driver中,相当于将switch driver嵌入在了codec driver中,大致流程如下:
1)在codec driver(sound/soc/codec/wm8994.c)的probe函数中:
a) 调用switch_dev_register注册一个switch device
b) 申请GPIO中断
2)在中断处理函数中读取GPIO状态和codec寄存器,判断耳机插入和mic是否存在,通过switch_set_state设置当前状态。