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

68 篇文章 13 订阅
56 篇文章 6 订阅

【概要】

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

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

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值