Chinaunix首页 | 论坛 | 博客
  • 博客访问: 335447
  • 博文数量: 41
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 476
  • 用 户 组: 普通用户
  • 注册时间: 2016-09-01 19:08
个人简介

Android/Linux/音频/驱动

文章分类

全部博文(41)

文章存档

2017年(21)

2016年(20)

我的朋友

分类: Android平台

2017-02-08 11:58:01

【概要】

 我们知道,耳机插入/拔出事件肯定是通过中断通知系统进行处理的。有了这个认识之后,我们就可以对这个过程进行逐个击破的分析了:
 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(),它们在后面很快就会被用到。如下所示:

  1. static int ts3a227e_i2c_probe(struct i2c_client *i2c,
  2.                                   const struct i2c_device_id *id)
  3. {
  4.          struct ts3a227e *ts3a227e;
  5.          struct device *dev = &i2c->dev;
  6.          unsigned int i;
  7.          int ret;
  8.          …...
  9.          INIT_DELAYED_WORK(&ts3a227e->hs_det_work, hs_detect_func);
  10.          INIT_DELAYED_WORK(&ts3a227e->enable_key_detect_work, enable_key_detect_func);
  11.          INIT_DELAYED_WORK(&ts3a227e->long_press_work, long_press_func);
  12.          if (i2c->irq != -1) {
  13.                    ret = devm_request_threaded_irq(dev, i2c->irq, NULL, ts3a227e_interrupt,
  14.                                                         IRQF_TRIGGER_LOW | IRQF_ONESHOT,
  15.                                                         "TS3A227E", ts3a227e);
  16.                    if (ret) {
  17.                             dev_err(dev, "Cannot request irq %d (%d)\n", i2c->irq, ret);
  18.                             return ret;
  19.                    }
  20.          }
  21.          …...
  22.          return 0;
  23. }



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

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

  1. static irqreturn_t ts3a227e_interrupt(int irq, void *data)
  2. {
  3.          …...
  4.          /* Check for plug/unplug. */
  5.          regmap_read(regmap, TS3A227E_REG_INTERRUPT, &(ts3a227e->int_reg));
  6.          pr_err("%s ent, int_reg(0x01): %0x\n", __func__, ts3a227e->int_reg);
  7.  
  8.          if (ts3a227e->int_reg & (DETECTION_COMPLETE_EVENT | INS_REM_EVENT)) {
  9.                    regmap_read(regmap, TS3A227E_REG_ACCESSORY_STATUS, &(ts3a227e->acc_reg));
  10.                    pr_err("%s acc_reg(0x0b): %0x\n", __func__, ts3a227e->acc_reg);
  11.          }
  12.          …...
  13.          cancel_delayed_work_sync(&ts3a227e->hs_det_work);
  14.          schedule_delayed_work(&ts3a227e->hs_det_work,
  15.                          msecs_to_jiffies(5)); /* 调度延迟任务hs_detect_func() */
  16.  
  17.          return IRQ_HANDLED;
  18. }

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

  1. static void hs_detect_func(struct work_struct *work)
  2. {
  3.          struct ts3a227e *ts3a227e = container_of(work,
  4.                    struct ts3a227e, hs_det_work.work);
  5.  
  6.          pr_err("%s, id:%x\n", __func__, ts3a227e->id);
  7.          check_jack_status(ts3a227e);
  8. }

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

  1. static void check_jack_status(struct ts3a227e *ts3a227e)
  2. {
  3.          struct regmap *regmap = ts3a227e->regmap;
  4.          unsigned int i;
  5.          bool long_press_det = false;
  6.  
  7.          /* Check for plug/unplug. */
  8.          if (ts3a227e->int_reg & (DETECTION_COMPLETE_EVENT | INS_REM_EVENT)) {
  9.                    //regmap_read(regmap, TS3A227E_REG_ACCESSORY_STATUS, &acc_reg);
  10.                    printk("%s acc_reg(0x0b): %0x\n", __func__, ts3a227e->acc_reg);
  11.                    ts3a227e_new_jack_state(ts3a227e, ts3a227e->acc_reg);
  12.          }
  13.          …...
  14.          input_sync(ts3a227e->button_dev);
  15.          ts3a227e_jack_report(ts3a227e);
  16. }

  分析到这里可以看到,紧接着在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进行相应配置。代码如下:

  1. static void ts3a227e_jack_report(struct ts3a227e *ts3a227e)
  2. {
  3.          pr_err("%s\n", __func__);
  4.  
  5.          if (ts3a227e->plugged) { /* 有耳机插入 */
  6.                    pr_err("%s, HEADPHONE plug\n", __func__);
  7.  
  8.                    if (ts3a227e->mic_present) { /* 插入的耳机带有麦克风 */
  9.                             printk("%s, MICPHONE plug\n", __func__);
  10.                             extcon_set_state(&ts3a227e->edev, BIT_HEADSET);
  11.  
  12.                             rt5677_enable_micbias(true);
  13.                    }
  14.                    else { /* 插入的耳机不带麦克风 */
  15.                             rt5677_enable_micbias(false);
  16.                             extcon_set_state(&ts3a227e->edev, BIT_HEADSET_NO_MIC);
  17.                    }
  18.          }
  19.          else { /* 没有耳机插入 */
  20.                    pr_err("%s, HEADSET unplug\n", __func__);
  21.                    /* disable key press detection. */
  22.         regmap_update_bits(ts3a227e->regmap, TS3A227E_REG_SETTING_2, KP_ENABLE, 0);
  23.                    extcon_set_state(&ts3a227e->edev, BIT_NO_HEADSET);
  24.                    rt5677_enable_micbias(false);
  25.          }
  26. }

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

  1. void rt5677_enable_micbias(bool enable)
  2. {
  3.          struct snd_soc_codec *codec;
  4.  
  5.          pr_err("%s, %d\n", __func__, enable);
  6.          codec = cht_get_codec(&snd_soc_card_cht); /* snd_soc_card_cht是个全局变量 */
  7.          if(codec == NULL){
  8.                    pr_err("%s, codec has not probed yet!\n", __func__);
  9.                    return;
  10.          }
  11.          if (enable) {
  12.                    snd_soc_dapm_force_enable_pin(&codec->dapm, "MICBIAS1");
  13.          }
  14.          else {
  15.                    snd_soc_dapm_disable_pin(&codec->dapm, "MICBIAS1");
  16.          }
  17.          snd_soc_dapm_sync(&codec->dapm);
  18. }

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

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

  1. /* SoC card */
  2. static struct snd_soc_card snd_soc_card_cht = {
  3.          .name = "cherrytrailaud",
  4.          .dai_link = cht_dailink,
  5.          .num_links = ARRAY_SIZE(cht_dailink),
  6.          .set_bias_level = cht_set_bias_level,
  7.          .dapm_widgets = cht_dapm_widgets, /* DAPM widgets */
  8.          .num_dapm_widgets = ARRAY_SIZE(cht_dapm_widgets),
  9.          .dapm_routes = cht_audio_map,
  10.          .num_dapm_routes = ARRAY_SIZE(cht_audio_map),
  11.          .controls = cht_mc_controls,
  12.          .num_controls = ARRAY_SIZE(cht_mc_controls),
  13. };

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


  1. static const struct snd_soc_dapm_widget cht_dapm_widgets[] = {
  2.     SND_SOC_DAPM_HP("Headphone", cht_rt5677_hp_event), /* 耳机事件的DAPM widget */
  3.     SND_SOC_DAPM_SPK("Speaker", cht_rt5677_spk_event),
  4.     SND_SOC_DAPM_MIC("Headset Mic", NULL),
  5.     SND_SOC_DAPM_MIC("Int Mic", NULL),
  6.     SND_SOC_DAPM_SUPPLY("Platform Clock", SND_SOC_NOPM, 0, 0,
  7.             platform_clock_control, SND_SOC_DAPM_PRE_PMU|
  8.             SND_SOC_DAPM_POST_PMD),
  9. };


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

  1. static int cht_rt5677_hp_event(struct snd_soc_dapm_widget *w,
  2.                    struct snd_kcontrol *k, int event)
  3. {
  4.          struct snd_soc_dapm_context *dapm = w->dapm;
  5.          struct snd_soc_card *card = dapm->card;
  6.          struct snd_soc_codec *codec;
  7.  
  8.          pr_err("%s, %d\n", __func__, __LINE__);
  9.          codec = cht_get_codec(card);
  10.          if (!codec) {
  11.                    pr_err("Codec not found; Unable to set platform clock\n");
  12.                    return -EIO;
  13.          }
  14.          if (SND_SOC_DAPM_EVENT_ON(event)) { /* 发生耳机插入事件 */
  15.                    pr_err("%s, %d\n", __func__, __LINE__);
  16.                    msleep(20);
  17.                    snd_soc_update_bits(codec, RT5677_GPIO_CTRL2,
  18.                             RT5677_GPIO5_OUT_MASK, RT5677_GPIO5_OUT_HI); /* 将耳机功放芯片/SHDN引脚的电平拉高 */
  19.                    msleep(50);
  20.          } else {
  21.                    pr_err("%s, %d\n", __func__, __LINE__);
  22.                    snd_soc_update_bits(codec, RT5677_GPIO_CTRL2,
  23.                             RT5677_GPIO5_OUT_MASK, RT5677_GPIO5_OUT_LO);
  24.          }
  25.  
  26.          return 0;
  27. }

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


阅读(6559) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~