Fork me on GitHub
0%

耳机插拔检测

耳机分类

目前市场上的耳机种类繁多每一种耳机都有其较为匹配的应用场景。按照佩戴方式可以划分为:入耳式、外耳式、头戴式等等;按照数据传输方式可以划分为:有线耳机、无线耳机;按照进入耳机的数据类型分类可以划分为:模拟耳机、数字耳机;本文主要是根据嵌入式产品上应用较多的两种不同的接口耳机进行说明:type-c耳机和3.5mm音频接口耳机(type-c耳机不一定是数字耳机,3.5mm圆孔耳机一定是模拟耳机)

3.5mm音频接口耳机

image

  • 3-pole(Headphone)
  • 4-pole(Headset)
    • OMTP(Open Mobile Terminal Platform),对应插头上的塑料环一般是黑色,国标 / 欧标;
    • CITA(Cellular Telecommunications and Internet Association),对应插头上的塑料环一般是白色,美标;

硬件检测原理

一般的耳机检测包含普通的耳机(Headphone)检测和带mic的耳机(Headset)检测两种,这两种耳机统称为Headset。

对于耳机装置的插拔检测,一般通过Jack即耳机插座来完成,大致的原理是使用带检测机械结构的耳机插座,将检测脚连到可GPIO中断上,当耳机插入时,耳机插头的金属会碰到检测脚,使得检测脚的电平产生变化,从而引起中断。这样就可以在中断处理函数中读取GPIO的的值,进一步判断出耳机是插入还是拔出。而对于耳机是否带mic的检测,需要通过codec附加的micbias电流的功能

检测机制

InputEvent

在 Linux 内核中,Input Event 是用于处理输入设备事件的机制,它允许内核捕获和处理各种输入设备的事件。内核中的 Input Event 机制通过 struct input_event 结构体来表示输入事件的信息。但是在音频部分ASOC已经为我们封装好了相应Jack接口函数。

1
2
3
4
5
6
struct input_event {
struct timeval time; // 时间戳,记录事件发生的时间
__u16 type; // 事件类型,如 EV_KEY (键盘按键事件)、EV_REL (相对坐标事件)、EV_ABS (绝对坐标事件) 等
__u16 code; // 事件码,表示事件的具体类型或键码
__s32 value; // 事件的值,表示事件状态,例如按键的按下 (1) 或释放 (0),坐标事件的位置值等
};

ASoc Jack函数

  1. 创建生成jack对象

    1
    2
    3
    4
    int snd_soc_jack_new (struct snd_soc_codec *codec, const char *id, int type, struct snd_soc_jack *jack);

    // id: 名字
    // type: 被检测的类型,即可能插入的设备, SND_JACK_HEADPHONE / SND_JACK_HEADSET或者其他

    函数调用关系:snd_jack_new –> input_allocate_device 申请input device空间,注册成输入设备

  2. 将定义好的pins加入dapm widgets进行电源管理。这一步和InputEvent没有一定联系,也可以不调用。

    1
    2
    3
    4
    5
    6
    7
    8
    int	snd_soc_jack_add_pins (struct snd_soc_jack *jack, int count, struct snd_soc_jack_pin *pins);

    struct snd_soc_jack_pin {
    struct list_head list;
    const char *pin; // 耳机检测引脚的名称或标识符
    int mask; // 需要报告的插拔事件类型
    bool invert;
    };

    这里说之前定义好的pins指的是:使用类似于SND_SOC_DAPM_MIC / SND_SOC_DAPM_HP等等宏声明过的控件名称,否则在add_pins的过程中就会失败。在使用宏声明过后还需要将其定义为snd_soc_jack_pin结构体类型,其中两个重要的两个参数就是pin、mask两个字段需要声明。

    本质上还是对控件的管理,当jack有动作其符合mask所标识的时候,就会将对应的控件进行控制。

  3. 向用户空间上报事件

    1
    void snd_soc_jack_report (struct snd_soc_jack *jack, int status, int mask)
    1. 判断此时的status和mask事件是否对应 使用snd_soc_jack_add_pins函数注册的dapm pin状态,并进行电源管理

    2. 通过调用 –> snd_jack_report –> input_report_key/input_report_switch函数来向上层汇报input event

  4. 将GPIO引脚状态变化与音频时间关联起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int snd_soc_jack_add_gpios(struct snd_soc_jack *jack, int count,  struct snd_soc_jack_gpio *gpios)

    struct snd_soc_jack_gpio {
    unsigned int gpio; //GPIO 引脚的编号
    unsigned int idx;
    struct device *gpiod_dev;
    const char *name; // 这个jack结构体标识符
    int report; // 表示插拔事件的报告类型或标志位。
    int invert;
    int debounce_time; // GPIO 引脚的去抖动时间,用于在检测到状态变化后延迟一段时间再处理事件,以避免抖动现象。
    bool wake; // 用于表示是否在检测到插拔事件后唤醒系统。

    /* private: */
    struct snd_soc_jack *jack;
    struct delayed_work work;
    struct gpio_desc *desc;

    void *data;
    /* public: */
    int (*jack_status_check)(void *data); //检查耳机插拔状态的回调函数。
    };

    该函数通过标准GPIO驱动申请GPIO及GPIO对应中断,并提供了统一的中断处理函数来汇报事件。此函数只适用于耳机中断接至GPIO且GPIO驱动为Linux标准驱动的情况下,并且不支持mic检测。

操作流程

  1. snd_soc_jack_new 创建jack对象

  2. snd_soc_jack_add_pins将其加入到dapm wigets中(可不调用)

  3. 将耳机detect_pin GPIO注册成中断,并在中断处理函数中进一步读取codec相关寄存器判断是headset还是headphone(这一步骤需要注意hp_detect pin需要连接到codec,并且codec是支持该功能的)

  4. 根据判断结果调用snd_soc_jack_report发送InputEvent

Interrupt

对于InputEvent的方式本质上还是Jack和IRQ方式的结合,但是如果codec不支持headset / headphone的检测,就只能使用snd_soc_jack_add_gpios这个函数来进行向应用层通知插拔事件。

当然如果用户层不需要感知HP插拔事件的时候,我们可以直接在中断处理函数中操作codec寄存器进行录放音的切换即可,这样只是用户层将拿不到状态信息。

UEvent

源码位置:andriod-kernel_3.18

UEvent 是 Linux 内核对设备插拔等事件的通知机制,当设备插入或拔出时,内核会生成相应的 UEvent,并发送给用户空间,以便用户空间的应用程序可以对事件作出响应。UEvent是Android系统默认的耳机插拔机制,而UEvent机制比较简单,它基于switch driver。switch driver的目录在Linux kernel的drivers/staging/android/switch目录下,switch_class.c是switch driver的内部实现,它提供了switch driver所需的一些API;switch_gpio.c是一个例子,它实现了一个基于GPIO中断的switch driver。下面着重对switch_class.c进行分析。(只分析kernel层,不关注framework和app Layer)

switch原理

Switch是Android引进的新的驱动,目的是用于检测一些开关量,比如检测耳机插入、检测 USB 设备插入等。Switch在sysfs文件系统中创建相应entry,用户可以通过sysfs与之交互; 此外还可以通过UEvent机制与之交互, 从而检测switch状态。

switch函数实现

Switch class在Android中实现为一个module,可动态加载。switch_class.c文件创建了一个switch_class,实现了内核的switch机制,提供支持函数供其他switch device驱动调用。

  1. 创建switch_class

    1
    static int create_switch_class(void)

    驱动注册入口函数 module_init() 中调用 create_switch_class() 函数。而该函数主要功能是调用 class_create() 函数创建了一个名为 switch 的class

  2. 注册 / 注销switch设备

    1
    2
    int switch_dev_register(struct switch_dev *sdev);
    void switch_dev_unregister(struct switch_dev *sdev);
  3. 显示设备名字和状态

    1
    2
    3
    4
    5
    6
    7
    8
    static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf);
    static ssize_t name_show(struct device *dev, struct device_attribute *attr, char *buf);

    static DEVICE_ATTR(state, S_IRUGO, state_show, NULL);
    static DEVICE_ATTR(name, S_IRUGO, name_show, NULL);

    // #define DEVICE_ATTR(_name, _mode, _show, _store)
    // sysfs(system file system)接口的一部分,允许将设备的状态信息以文件的形式暴露给用户空间,使用户空间可以通过读写这些文件来与设备进行交互和配置。

    这两个sysfs操作函数分别用于输出switch device的name和state。当用户读取sysfs中对应的switch entry(/sys/class/switch//name和/sys/class/switch//state)时候,系统会自动调用这两个函数向用户返回switch设备的名称和状态。

  4. 改变并通知用户swtich状态

    1
    void switch_set_state(struct switch_dev *sdev, int state)
    1. 调用name_show和state_show输出switch设备名称和状态至sysfs文件系统;
    2. 调用 kobject_uevent_env() 函数发送UEvent通知用户switch device的信息(名称和状态)

用户层使用

UEvent机制比较简单,它基于switch driver,switch driver会在Android建立耳机插拔的目录/sys/devices/virtual/switch/h2w,在此目录下有个设备结点名为state,driver通过更新state的值,从而通知Android上层耳机状态的改变。

参考资料