首页 > 系统 > Android > 正文

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

2019-11-09 16:04:51
字体:
来源:转载
供稿:网友

【概要】

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

    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更改的过程就分析完毕了。希望这篇文章能让你有所收获。

 


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表