赞
踩
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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。