1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
第四十三章 耳机插拨驱动实验
耳机插拨在我们日常生活中经常遇到,在某些老款安卓手机还可以看到耳机孔的身影,当我们用耳机插到手机的耳机孔时,手机的外放就停止播放声音。逐渐地,手机上的耳机孔被淘汰了,已经被蓝牙耳机取代,现在主要是一些音响设备需要检测耳机插拨。
Linux 耳机插拨驱动实验是一项涉及到 Linux 内核开发和设备驱动编程的实验。该实验的主要目标是开发一个驱动程序,用于检测和响应耳机插拨事件,以便在插入或拔出耳机时执行相应的操作。Linux耳机插拨驱动不用我们写,内核早已有这个驱动。本章我们分析Linux耳机插拨驱动。
43.1 硬件原理图
正点原子的ATK-DLRK3568开发板底板板载了耳机孔接口,原理图如下所示:
图43.1.1 板载耳机孔接口
在上图图43.1.1中我们可以看到用于耳机插拨使用的引脚HP_DET_L是GPIO1_A4。
ATK-DLRK3568底板的耳机插孔如下图位置。
图43.1.2 底板耳机孔接口
43.2 实验程序编写
本实验对应的例程路径为:开发板光盘01、程序源码03、Linux驱动例程27_hp_det_test。
43.2.1 修改设备树
打开rk3568-linux.dtsi文件,找到如下代码。
示例代码43.2.1耳机检测GPIO配置
1 rk809_sound: rk809-sound {
2 status = "okay";
3 compatible = "simple-audio-card";
4 simple-audio-card,format = "i2s";
5 //simple-audio-card,hp-det-gpio = <&gpio3 RK_PC2 GPIO_ACTIVE_HIGH>;
6 simple-audio-card,hp-det-gpio = <&gpio1 RK_PA4 GPIO_ACTIVE_LOW>;
7 simple-audio-card,name = "rockchip,rk809-codec";
8 simple-audio-card,widgets = "Headphones", "Headphones Jack";
9 simple-audio-card,mclk-fs = <256>;
10 simple-audio-card,cpu {
11 sound-dai = <&i2s1_8ch>;
12 };
13 simple-audio-card,codec {
14 sound-dai = <&rk809_codec>;
15 };
16 };
17
第6行,设置了耳机检测GPIO为GPIO1_A4。
我们现在从设备树开始,先看看“hp-det-gpio”是在哪个驱动文件设置的。我们从源码中找,发现是在内核源码目录中,是在sound/soc/generic/simple-card-utils.c这个驱动文件。那么我们来看看耳机插拨驱动实现的流程。
示例代码45.2.2耳机插拨检测GPIO初始化
423 int asoc_simple_card_init_jack(struct snd_soc_card *card,
424 struct asoc_simple_jack *sjack,
425 int is_hp, char *prefix)
426 {
427 struct device *dev = card->dev;
428 enum of_gpio_flags flags;
429 char prop[128];
430 char *pin_name;
431 char *gpio_name;
432 int mask;
433 int det;
434
435 if (!prefix)
436 prefix = "";
437
438 sjack->gpio.gpio = -ENOENT;
439
440 if (is_hp) {
441 snprintf(prop, sizeof(prop), "%shp-det-gpio", prefix);
442 pin_name = "Headphones";
443 gpio_name = "Headphone detection";
444 mask = SND_JACK_HEADPHONE;
445 } else {
446 snprintf(prop, sizeof(prop), "%smic-det-gpio", prefix);
447 pin_name = "Mic Jack";
448 gpio_name = "Mic detection";
449 mask = SND_JACK_MICROPHONE;
450 }
451
452 det = of_get_named_gpio_flags(dev->of_node, prop, 0, &flags);
453 if (det == -EPROBE_DEFER)
454 return -EPROBE_DEFER;
455
456 if (gpio_is_valid(det)) {
457 sjack->pin.pin = pin_name;
458 sjack->pin.mask = mask;
459
460 sjack->gpio.name = gpio_name;
461 sjack->gpio.report = mask;
462 sjack->gpio.gpio = det;
463 sjack->gpio.invert = !!(flags & OF_GPIO_ACTIVE_LOW);
464 sjack->gpio.debounce_time = 150;
465
466 snd_soc_card_jack_new(card, pin_name, mask,
467 &sjack->jack,
468 &sjack->pin, 1);
469
470 snd_soc_jack_add_gpios(&sjack->jack, 1,
471 &sjack->gpio);
472 }
473
474 return 0;
475 }
476 EXPORT_SYMBOL_GPL(asoc_simple_card_init_jack);
第440~445行,构建用于查找设备树中 GPIO 属性的属性名称 prop。设置 pin_name 为 "Headphones",表示插孔的名称。设置 gpio_name 为 "Headphone detection",表示 GPIO 的名称。设置 mask 为 SND_JACK_HEADPHONE,表示这是一个耳机插孔。
第452行,使用设备树函数 of_get_named_gpio_flags 获取与属性名称 prop 关联的 GPIO 描述符,并存储在 det 中。如果 GPIO 未定义,det 将为负数。
第456~464行,如果设置了检测GPIO,那么设置结构体指针sjack的一些属性。设置插孔的引脚信息,比如sjack->pin.pin引脚名字。设置耳机插孔的一些GPIO关联信息,如sjack->gpio.gpio是表示GPIO 描述符,sjack->gpio.invert表示根据设备树中的属性决定是否反转 GPIO 状态,在上面的设备树中,我们设置了GPIO_ACTIVE_LOW,说明是低电平表示活动。从硬件原理图可知,当耳机插入时,检测脚将被拉低,说明是低有效。debounce_time这个是设置消抖时间,防止误检测。
第466~468行,这里将耳机插孔与声卡绑定。
第470~471行,绑定GPIO,就会触发耳机插拨事件。
打开include/sound/simple_card_utils.h头文件,找到以下内容。
示例代码45.2.3耳机插拨检测GPIO重新定义
13 #define asoc_simple_card_init_hp(card, sjack, prefix) \
14 asoc_simple_card_init_jack(card, sjack, 1, prefix)
15 #define asoc_simple_card_init_mic(card, sjack, prefix) \
16 asoc_simple_card_init_jack(card, sjack, 0, prefix)
从上面的代码可知道,asoc_simple_card_init_jack被重新定义为asoc_simple_card_init_hp。因为有耳机检测也有MIC检测,内核里将它们用不同的函数命名区分,但是用的实际是同一个函数。参数“1”,代表这就是一个耳机设备!
打开sound/soc/generic/simple-card.c文件。找到以下内容。
示例代码45.2.4耳机插拨检测GPIO初始化声卡
342 static int asoc_simple_soc_card_probe(struct snd_soc_card *card)
343 {
344 struct simple_card_data *priv = snd_soc_card_get_drvdata(card);
345 int ret;
346
347 ret = asoc_simple_card_init_hp(card, &priv->hp_jack, PREFIX);
348 if (ret < 0)
349 return ret;
350
351 ret = asoc_simple_card_init_mic(card, &priv->mic_jack, PREFIX);
352 if (ret < 0)
353 return ret;
354
355 return 0;
356 }
这个驱动文件负责声卡的初始化,音频流管理,控制接口等。在第347行,调用了耳机检测IO初始化的代码。
上面的simple-card.c似乎没有上报耳机检测事件数据,所以我们要返回到前面的代码查找是如何实现的。
在示例代码45.2.2中的asoc_simple_card_init_jack这个函数里,查看snd_soc_card_jack_new函数,这个函数定义在sound/soc/soc-jack.c文件中。
示例代码45.2.5 创建一个新的插孔
59 int snd_soc_card_jack_new(struct snd_soc_card *card, const char *id, int type,
60 struct snd_soc_jack *jack, struct snd_soc_jack_pin *pins,
61 unsigned int num_pins)
62 {
63 int ret;
64
65 mutex_init(&jack->mutex);
66 jack->card = card;
67 INIT_LIST_HEAD(&jack->pins);
68 INIT_LIST_HEAD(&jack->jack_zones);
69 BLOCKING_INIT_NOTIFIER_HEAD(&jack->notifier);
70
71 ret = snd_jack_new(card->snd_card, id, type, &jack->jack, false, false);
72 if (ret)
73 return ret;
74
75 if (num_pins)
76 return snd_soc_jack_add_pins(jack, num_pins, pins);
77
78 return 0;
79 }
80 EXPORT_SYMBOL_GPL(snd_soc_card_jack_new);
第76行,我们看到是snd_soc_jack_add_pins函数添加检测管脚pins。
找到snd_soc_jack_add_pins函数定义的地方,也是在sound/soc/soc-jack.c文件中。
200 int snd_soc_jack_add_pins(struct snd_soc_jack *jack, int count,
201 struct snd_soc_jack_pin *pins)
202 {
203 int i;
204
205 for (i = 0; i < count; i++) {
206 if (!pins[i].pin) {
207 dev_err(jack->card->dev, "ASoC: No name for pin %d\n",
208 i);
209 return -EINVAL;
210 }
211 if (!pins[i].mask) {
212 dev_err(jack->card->dev, "ASoC: No mask for pin %d"
213 " (%s)\n", i, pins[i].pin);
214 return -EINVAL;
215 }
216
217 INIT_LIST_HEAD(&pins[i].list);
218 list_add(&(pins[i].list), &jack->pins);
219 snd_jack_add_new_kctl(jack->jack, pins[i].pin, pins[i].mask);
220 }
221
222 /* Update to reflect the last reported status; canned jack
223 * implementations are likely to set their state before the
224 * card has an opportunity to associate pins.
225 */
226 snd_soc_jack_report(jack, 0, 0);
227
228 return 0;
229 }
230 EXPORT_SYMBOL_GPL(snd_soc_jack_add_pins);
第226行,snd_soc_jack_report函数上报声卡关联前的耳机插孔状态。
再找到snd_soc_jack_report函数定义,也是在sound/soc/soc-jack.c文件中。
示例代码45.2.6 上报接口的当前状态
96 void snd_soc_jack_report(struct snd_soc_jack *jack, int status, int mask)
97 {
98 struct snd_soc_dapm_context *dapm;
99 struct snd_soc_jack_pin *pin;
100 unsigned int sync = 0;
101 int enable;
102
103 if (!jack)
104 return;
105 trace_snd_soc_jack_report(jack, mask, status);
106
107 dapm = &jack->card->dapm;
108
109 mutex_lock(&jack->mutex);
110
111 jack->status &= ~mask;
112 jack->status |= status & mask;
113
114 trace_snd_soc_jack_notify(jack, status);
115
116 list_for_each_entry(pin, &jack->pins, list) {
117 enable = pin->mask & jack->status;
118
119 if (pin->invert)
120 enable = !enable;
121
122 if (enable)
123 snd_soc_dapm_enable_pin(dapm, pin->pin);
124 else
125 snd_soc_dapm_disable_pin(dapm, pin->pin);
126
127 /* we need to sync for this case only */
128 sync = 1;
129 }
130
131 /* Report before the DAPM sync to help users updating micbias status */
132 blocking_notifier_call_chain(&jack->notifier, jack->status, jack);
133
134 if (sync)
135 snd_soc_dapm_sync(dapm);
136
137 snd_jack_report(jack->jack, jack->status);
138
139 mutex_unlock(&jack->mutex);
140 }
141 EXPORT_SYMBOL_GPL(snd_soc_jack_report);
第137行,看到上报数据是交给snd_jack_report 处理的。
打开sound/core/jack.c文件,找到snd_jack_report函数如下。
示例代码45.2.7 上报插拨事件功能代码
365 void snd_jack_report(struct snd_jack *jack, int status)
366 {
367 struct snd_jack_kctl *jack_kctl;
368 #ifdef CONFIG_SND_JACK_INPUT_DEV
369 int i;
370 #endif
371
372 if (!jack)
373 return;
374
375 list_for_each_entry(jack_kctl, &jack->kctl_list, list)
376 snd_kctl_jack_report(jack->card, jack_kctl->kctl,
377 status & jack_kctl->mask_bits);
378
379 #ifdef CONFIG_SND_JACK_INPUT_DEV
380 if (!jack->input_dev)
381 return;
382
383 for (i = 0; i < ARRAY_SIZE(jack->key); i++) {
384 int testbit = SND_JACK_BTN_0 >> i;
385
386 if (jack->type & testbit)
387 input_report_key(jack->input_dev, jack->key[i],
388 status & testbit);
389 }
390
391 for (i = 0; i < ARRAY_SIZE(jack_switch_types); i++) {
392 int testbit = 1 << i;
393 if (jack->type & testbit)
394 input_report_switch(jack->input_dev,
395 jack_switch_types[i],
396 status & testbit);
397 }
398
399 input_sync(jack->input_dev);
400 #endif /* CONFIG_SND_JACK_INPUT_DEV */
401 }
402 EXPORT_SYMBOL(snd_jack_report);
第387和388行,上报事件,参数1为耳机事件类型,参数2为耳机事件键值,参数3表示耳机插拨的状态。
第399行,最后同步事件,上报事件与我们前面的触摸屏驱动类似。
耳机插拨是有个中断服务函数的,详细看代码示例代码45.2.2中的snd_soc_jack_add_gpios,最终也会调用上面的snd_jack_report来上报耳机插拨事件。这里我们就不分析了,有兴趣可以自己一步步查找。
43.2.2 编写测试APP
新建hp_det_test.c文件,然后在里面输入如下所示内容:
/*******************************************************************************
Guangzhou Xingyi Electronic Technology Co., Ltd 2021-2030. All rights reserved.
* @brief hp_det_test.c
* @author 正点原子Linux团队
* @date 2023-06-26
* @link http://www.openedv.com/forum.php
********************************************************************************/
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <sys/ioctl.h>
8 #include <linux/input.h>
9 #include <pthread.h>
10 #include <dirent.h>
11
12 int main(void)
13 {
14 struct input_event ev = {0};
15 int fd = -1;
16 DIR *dir = NULL;
17
18 setbuf(stdout, NULL);
19 printf("=== 耳机插拨测试 ===\n");
20
21 dir = opendir("/dev/input");
22 if(dir != NULL) {
23 struct dirent *de = NULL;
24 char name[64];
25
26 while((de = readdir(dir))) {
27 if(strncmp(de->d_name, "event", 5))
28 continue;
29
30 fd = openat(dirfd(dir), de->d_name, O_RDONLY);
31 if(fd < 0)
32 continue;
33
34 if (ioctl(fd, EVIOCGNAME(sizeof(name) - 1), &name) < 1)
35 name[0] = '\0';
36 if (strcmp(name, "rockchip,rk809-codec Headphones")) {
37 close(fd);
38 continue;
39 }
40
41 closedir(dir);
42 goto start_read;
43 }
44
45 closedir(dir);
46 goto err_out;
47 } else
48 goto err_out;
49
50 start_read:
51 for ( ; ; ) {
52
53 if (read(fd, &ev, sizeof(ev)) != sizeof(ev)) {
54 printf("read failed!\n");
55 goto err_out1;
56 }
57
58 if (ev.type == EV_SW && ev.code == SW_HEADPHONE_INSERT) {
59 if (ev.value == 1)
60 printf("耳机插入\n");
61 else
62 printf("耳机拔出\n");
63 }
64 }
65
66 return 0;
67
68 err_out1:
69 close(fd);
70
71 err_out:
72 printf("=== 耳机测试结束 ===\n");
73 return -1;
74 }
App程序也是很简单,耳机插拨也是一种input事件,我们直接在input事件用遍历方法找到耳机插孔事件。
第58~63行,判断事件类型为EV_SW,这是一个开关(Switch)事件,SW_HEADPHONE_INSERT是EV_SW事件类型的一个子事件码,表示耳机插入/拨出,后面再判断事件值,表示耳机的插入状态,1表示插入,0表示拨出。
43.2.3 运行测试
43.2.3.1 编译测试
输入如下命令编译hp_det_test.c:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc hp_det_test.c -o hp_det_test
编译成功以后就会生成hp_det_test。
43.2.3.2 运行测试
在Ubuntu中将上一小节编译出来的hp_det_test通过adb命令发送到开发板的/lib/modules/4.19.232目录下,命令如下:
adb push hp_det_test /lib/modules/4.19.232
本章的驱动默认已经在发布的内核上了,所以我们无需编译驱动,直接在Linux Buildroot根文件系统上测试,我们先测试加速度,输入如下指令。然后使用耳机插头直接插到底板上的耳机孔位。插拨测试会打印“耳机插入”与“耳机拨出”字样。
./hp_det_test