您的位置:首页 > 移动开发 > Android开发

Android系统中从发生耳机插拔事件到音频Route切换过程分析

2017-02-08 11:45 489 查看
【概要】

    我们知道,耳机插入/拔出事件肯定是通过中断通知系统进行处理的。有了这个认识之后,我们就可以对这个过程进行逐个击破的分析了:

    1、  谁为耳机事件产生中断?

    2、  中断处理函数是哪个?

    3、  中断处理函数中执行了什么操作来改变音频Route?

 

【备注】

    本文基于我所使用的硬件环境进行分析,虽然可能与你现在使用的芯片不同,但思路是一样的。重在方法,不在结果。

 

 

【谁产生中断】

    要回答这个问题,需要查看耳机部分的电路原理图:



图1  耳机检测电路
    可以看到,耳机插口(headset-jack)是和ts3a227e这款耳机检测芯片相连接的。芯片上的/INT和/MIC_PRESENT应该就是中断输出引脚。查看芯片datasheet中的说明:



图2  ts3a227e芯片引脚说明
    至此我们就清楚了:当耳机插入/拔出时,ts3a227e能够检测到这一动作,同时向SoC发送中断。SoC接收到中断后,相应的中断处理函数会对硬件配置进行修改。

 

 

【找到中断处理函数】

    回忆一下《Linux设备驱动程序》这本书“中断处理”章节的内容:设备需要为自己要使用的中断线向系统申请中断号,并在系统中注册相应的中断处理函数。

    查看ts3a227e芯片的驱动源码文件ts3a227e.c可以发现该芯片是作为i2c设备进行注册的。在probe函数中不仅申请了设备中断号、绑定了中断处理函数ts3a227e_interrupt(),还向工作队列中添加了3个延迟任务,分别是hs_detect_func()、enable_key_detect_func()和long_press_func(),它们在后面很快就会被用到。如下所示:

static int ts3a227e_i2c_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
struct ts3a227e *ts3a227e;
struct device *dev = &i2c->dev;
unsigned int i;
int ret;
…...
INIT_DELAYED_WORK(&ts3a227e->hs_det_work,hs_detect_func);
INIT_DELAYED_WORK(&ts3a227e->enable_key_detect_work,enable_key_detect_func);
INIT_DELAYED_WORK(&ts3a227e->long_press_work,long_press_func);
if(i2c->irq != -1) {
ret = devm_request_threaded_irq(dev, i2c->irq, NULL, ts3a227e_interrupt,
IRQF_TRIGGER_LOW| IRQF_ONESHOT,
"TS3A227E",ts3a227e);
if(ret) {
dev_err(dev,"Cannot request irq %d (%d)\n", i2c->irq, ret);
return ret;
}
}
…...
return 0;
}


 

【中断处理函数执行了什么操作】

    经过上面分析,我们可以判断当ts3a227e芯片发出的中断被SoC接收到时,函数ts3a227e_interrupt()会被立即调用。这个函数的功能有2个:读取寄存器以更新相关变量的值,以及调用之前添加到工作队列中的延迟任务hs_detect_func()。关键代码如下:

static irqreturn_t ts3a227e_interrupt(int irq, void *data)
{
…...
/* Check for plug/unplug. */
regmap_read(regmap,TS3A227E_REG_INTERRUPT, &(ts3a227e->int_reg));
pr_err("%sent, int_reg(0x01): %0x\n", __func__, ts3a227e->int_reg);

if(ts3a227e->int_reg & (DETECTION_COMPLETE_EVENT | INS_REM_EVENT)) {
regmap_read(regmap,TS3A227E_REG_ACCESSORY_STATUS, &(ts3a227e->acc_reg));
pr_err("%sacc_reg(0x0b): %0x\n", __func__, ts3a227e->acc_reg);
}
…...
cancel_delayed_work_sync(&ts3a227e->hs_det_work);
schedule_delayed_work(&ts3a227e->hs_det_work,
msecs_to_jiffies(5));    /* 调度延迟任务hs_detect_func()*/

return IRQ_HANDLED;
}


    被调用的hs_detect_func()函数代码如下:

static void hs_detect_func(struct work_struct *work)
{
struct ts3a227e *ts3a227e = container_of(work,
structts3a227e, hs_det_work.work);

pr_err("%s,id:%x\n", __func__, ts3a227e->id);
check_jack_status(ts3a227e);
}


    被hs_detect_func()调用的check_jack_status()函数主要功能是:检测耳机是否完全插入/拔出,并完成耳机按键状态检测,最终调用ts3a227e_jack_report()函数根据检测结果上报用于执行音频Route切换的操作。关键代码如下:

static void check_jack_status(struct ts3a227e *ts3a227e)
{
struct regmap *regmap = ts3a227e->regmap;
unsigned int i;
bool long_press_det = false;

/* Check for plug/unplug. */
if(ts3a227e->int_reg & (DETECTION_COMPLETE_EVENT | INS_REM_EVENT)) {
//regmap_read(regmap,TS3A227E_REG_ACCESSORY_STATUS, &acc_reg);
printk("%sacc_reg(0x0b): %0x\n", __func__, ts3a227e->acc_reg);
ts3a227e_new_jack_state(ts3a227e,ts3a227e->acc_reg);
}
…...
input_sync(ts3a227e->button_dev);
ts3a227e_jack_report(ts3a227e);
}


    分析到这里可以看到,紧接着在ts3a227e_jack_report()函数中要执行的就是操作硬件实现音频Route切换了。但为了看懂这个函数中的操作,我们需要先来看一看更宏观一点的电路关系,分别是耳机功放芯片电路和Codec芯片的部分电路:



图3  耳机功放芯片电路



图4  Codec芯片局部电路
    很明显,耳机功放芯片的片选引脚/SHDN连接到Codec芯片的GPIO5引脚上,由Codec芯片进行控制,低电平有效(就是说当/SHDN引脚上的电平为低时,耳机功放芯片将被关闭)。左右声道的音频信号分别从Codec芯片的LOUT1P、LOUT1N和LOUT2P、LOUT2N引脚输出到耳机功放芯片的INL+、INL-和INR+、INR-引脚上。

    所以如果想从耳机里听到声音,需要先将/SHDN引脚上的电平拉高。由于这个引脚上的电平是Codec芯片进行控制的,所以需要修改Codec芯片中的寄存器配置。

    我这里使用的Codec芯片为Realtek5677。

    电路分析到这里就可以继续阅读ts3a227e_jack_report()函数的实现细节了。它根据之前的耳机检测结果(是否插入耳机,插入的耳机是否带有麦克风),分别调用rt5677_enable_micbias()函数对Codec芯片Realtek5677进行相应配置。代码如下:

static void ts3a227e_jack_report(struct ts3a227e *ts3a227e)
{
pr_err("%s\n",__func__);

if(ts3a227e->plugged) {    /* 有耳机插入 */
pr_err("%s,HEADPHONE plug\n", __func__);

if(ts3a227e->mic_present) {    /* 插入的耳机带有麦克风 */
printk("%s,MICPHONE plug\n", __func__);
extcon_set_state(&ts3a227e->edev,BIT_HEADSET);

rt5677_enable_micbias(true);
}
else{    /* 插入的耳机不带麦克风 */
rt5677_enable_micbias(false);
extcon_set_state(&ts3a227e->edev,BIT_HEADSET_NO_MIC);
}
}
else{    /* 没有耳机插入 */
pr_err("%s,HEADSET unplug\n", __func__);
/*disable key press detection. */
regmap_update_bits(ts3a227e->regmap, TS3A227E_REG_SETTING_2, KP_ENABLE,0);
extcon_set_state(&ts3a227e->edev,BIT_NO_HEADSET);
rt5677_enable_micbias(false);
}
}


    再查看rt5677_enable_micbias()函数的实现细节,可以发现其使用了ASoC架构中的DAPM相关的函数。如下:

void rt5677_enable_micbias(bool enable)
{
struct snd_soc_codec *codec;

pr_err("%s,%d\n", __func__, enable);
codec = cht_get_codec(&snd_soc_card_cht);   /* snd_soc_card_cht是个全局变量*/
if(codec== NULL){
pr_err("%s,codec has not probed yet!\n", __func__);
return;
}
if(enable) {
snd_soc_dapm_force_enable_pin(&codec->dapm,"MICBIAS1");
}
else{
snd_soc_dapm_disable_pin(&codec->dapm,"MICBIAS1");
}
snd_soc_dapm_sync(&codec->dapm);
}


    这些DAPM函数经过一系列既定的调用流程后最终会落实到DAPM widgets上。关于DAPM的相关知识本篇文章不进行介绍(因为要写的话实在太多了),如果想了解这方面内容,我推荐你阅读Linux内核官方文档《Dynamic Audio Power Management for Portable Devices》、ALSA官方文档《DAPM》,或sepnic的博客,或我自己曾经也翻译过的一篇官方文档《DAPM概述(中文翻译)/
dapm.txt》。

    总之,最终要用到的DAPM widgets在ASoC Machine驱动的源文件cht_bl_dpcm_rt5677.c中进行了定义。Machine驱动中的声卡结构体定义如下:

/* SoC card */
static struct snd_soc_card snd_soc_card_cht= {
.name= "cherrytrailaud",
.dai_link= cht_dailink,
.num_links= ARRAY_SIZE(cht_dailink),
.set_bias_level= cht_set_bias_level,
.dapm_widgets= cht_dapm_widgets,    /* DAPM widgets */
.num_dapm_widgets= ARRAY_SIZE(cht_dapm_widgets),
.dapm_routes= cht_audio_map,
.num_dapm_routes= ARRAY_SIZE(cht_audio_map),
.controls= cht_mc_controls,
.num_controls= ARRAY_SIZE(cht_mc_controls),
};


    上述声卡所使用的DAPMwidgets定义如下,其中第一个对应的就是耳机插拔事件。其中cht_rt5677_hp_event()则是耳机插拔事件发生时要被执行的函数:

static const struct snd_soc_dapm_widgetcht_dapm_widgets[] = {
SND_SOC_DAPM_HP("Headphone",cht_rt5677_hp_event),    /* 耳机事件的DAPMwidget */
SND_SOC_DAPM_SPK("Speaker",cht_rt5677_spk_event),
SND_SOC_DAPM_MIC("HeadsetMic", NULL),
SND_SOC_DAPM_MIC("IntMic", NULL),
SND_SOC_DAPM_SUPPLY("PlatformClock", SND_SOC_NOPM, 0, 0,
platform_clock_control,SND_SOC_DAPM_PRE_PMU|
SND_SOC_DAPM_POST_PMD),
};


    在cht_rt5677_hp_event()函数中就可以看到修改Codec芯片Realtek5677寄存器的相关代码了。通过拉高Codec芯片的GPIO5引脚上的电平,相应的耳机功放芯片上的/SHDN引脚电平也被拉高,耳机功放芯片开始工作。代码如下:

static int cht_rt5677_hp_event(structsnd_soc_dapm_widget *w,
struct snd_kcontrol *k, int  event)
{
struct snd_soc_dapm_context *dapm = w->dapm;
struct snd_soc_card *card = dapm->card;
struct snd_soc_codec *codec;

pr_err("%s,%d\n", __func__, __LINE__);
codec = cht_get_codec(card);
if(!codec) {
pr_err("Codecnot found; Unable to set platform clock\n");
return-EIO;
}
if(SND_SOC_DAPM_EVENT_ON(event)) {    /* 发生耳机插入事件 */
pr_err("%s,%d\n", __func__, __LINE__);
msleep(20);
snd_soc_update_bits(codec,RT5677_GPIO_CTRL2,
RT5677_GPIO5_OUT_MASK,RT5677_GPIO5_OUT_HI);    /* 将耳机功放芯片/SHDN引脚的电平拉高 */
msleep(50);
}else {
pr_err("%s,%d\n", __func__, __LINE__);
snd_soc_update_bits(codec,RT5677_GPIO_CTRL2,
RT5677_GPIO5_OUT_MASK,RT5677_GPIO5_OUT_LO);
}

return 0;
}


    至此,从耳机插入/拔出到音频Route更改的过程就分析完毕了。希望这篇文章能让你有所收获。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: