RK3568系列20——GPIO中断通知到安卓应用

  客户需要通过GPIO的按钮来控制安卓应用的行为,一开始做了一个轮询器封装在一个aar库中交付客户使用,即使轮训的间隔设定为100毫秒,在实际的使用过程中还是会发生GPIO电平没有被检测到的情况,因此重新做了一套基于中断方式的aar库,经过测试,可以稳定使用。

整体思路

  GPIO的电平信号发生变化后,RK3568的硬件可以触发中断,在驱动层面可以轻松捕获并处理;然后需要一种方式将中断从内核态通知到用户态程序,这里使用的是发送IO信号的方法;在安卓程序上Java并不能够很好的处理信号,因此需要通过JNI的方式使用C++接受IO信号,然后调用Java方法通知上层安卓程序,整体的流程图如下:

1
2
3
4
5
###########################################################
# #
# GPIO Button -> Kernel Driver -> JNI C++ -> Android Java #
# #
###########################################################

内核驱动

  内核驱动包含一个drivers/gpio/gpio-interrupt.c文件,主要功能为注册一个GPIO设备,并在/dev下新建一个虚拟设备文件,供应用程序注册信号。在DTS中注册后,发生GPIO中断,应该能在内核日志中发现类似的信息:gpio-interrupt: handle, irq=100 value=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/poll.h>

struct gpio_node {
int gpio;
int irq;
int irq_mode;
};

struct gpio_controller {
struct gpio_node *data;
int count;
};

struct fasync_struct *fasync_queue;

// 发生中断时调用该函数
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
int ret, i;
struct gpio_controller *gpio_controller = dev_id;
for (i = 0; i < gpio_controller->count; i++) {
if (gpio_controller->data[i].irq == irq) {
ret = gpio_get_value(gpio_controller->data[i].gpio);
break;
}
}
printk("gpio-interrupt: handle, irq=%d value=%d\n", irq, ret);
// 发送信号给应用程序
kill_fasync(&fasync_queue, SIGIO, POLL_IN);
return IRQ_HANDLED;
}

static int gpio_interrupt_probe(struct platform_device *pdev)
{
int ret ,i;
enum of_gpio_flags flag;
struct gpio_controller *gpio_controller;
struct device_node *device_node = pdev->dev.of_node;

dev_info(&pdev->dev, "start probe gpio-interrupt %s\n", dev_name(&pdev->dev));
gpio_controller = devm_kzalloc(&pdev->dev, sizeof(struct gpio_controller *), GFP_KERNEL);
if (!gpio_controller) {
dev_err(&pdev->dev, "devm_kzalloc failed!\n");
return -ENOMEM;
}

gpio_controller->count = of_gpio_named_count(device_node, "irq-gpios");
if (gpio_controller->count <= 0) {
dev_err(&pdev->dev, "gpio-interrupt: irq-gpios count %d is invalid\n", gpio_controller->count);
return -ENODEV;
}
gpio_controller->data = devm_kzalloc(&pdev->dev, sizeof(struct gpio_node) * gpio_controller->count, GFP_KERNEL);

// 这种方式可以同时注册多个GPIO中断
for (i = 0; i < gpio_controller->count; i++) {
gpio_controller->data[i].gpio = of_get_named_gpio_flags(device_node, "irq-gpios", i, &flag);
if (!gpio_is_valid(gpio_controller->data[i].gpio)) {
dev_err(&pdev->dev, "gpio-interrupt: gpio[%d] %d is invalid\n", i, gpio_controller->data[i].gpio);
ret = -ENODEV;
goto err;
}

gpio_controller->data[i].irq = gpio_to_irq(gpio_controller->data[i].gpio);
if (!gpio_controller->data[i].irq) {
dev_err(&pdev->dev, "gpio-interrupt: gpio[%d] %d irq is invalid\n", i, gpio_controller->data[i].gpio);
ret = -ENODEV;
goto err;
}

gpio_controller->data[i].irq_mode = flag;

if (gpio_request(gpio_controller->data[i].gpio, "gpio-interrupt")) {
dev_err(&pdev->dev, "gpio-interrupt: gpio[%d] %d gpio request failed!\n", i, gpio_controller->data[i].gpio);
ret = -ENODEV;
goto err;
}

ret = request_irq(gpio_controller->data[i].irq, gpio_irq_handler, flag, "gpio-interrupt", gpio_controller);
if (ret != 0) {
dev_err(&pdev->dev, "gpio-interrupt: gpiod[%d] %d irq request failed!\n", i, gpio_controller->data[i].gpio);
ret = IRQ_NONE;
goto err;
}

// 将GPIO导出,应用程序可以通过/sys/class/gpio/读取GPIO值
ret = gpio_export(gpio_controller->data[i].gpio, 0);
if (ret != 0) {
dev_err(&pdev->dev, "gpio-interrupt: gpiod[%d] %d export failed!\n", i, gpio_controller->data[i].gpio);
ret = IRQ_NONE;
goto err;
}
}

dev_info(&pdev->dev, "finish probe gpio-interrupt %s, register %d gpios\n", dev_name(&pdev->dev), gpio_controller->count);
return 0;
err:
for (i = 0; i < gpio_controller->count; i++) {
free_irq(gpio_controller->data[i].irq, gpio_controller);
gpio_free(gpio_controller->data[i].gpio);
}
return ret;
}

static int gpio_interrupt_fasync(int fd, struct file *file, int on)
{
// 应用程序注册时,加入fasync队列
return fasync_helper(fd, file, on, &fasync_queue);
}

static struct file_operations gpio_interrupt_file_operations = {
.owner = THIS_MODULE,
.fasync = gpio_interrupt_fasync,
};

// 在/dev下注册一个gpio-interrupt设备
static struct miscdevice gpio_interrupt_miscdevice = {
.minor = MISC_DYNAMIC_MINOR,
.fops = &gpio_interrupt_file_operations,
.name = "gpio-interrupt",
};

static struct of_device_id match_table[] = {
{ .compatible = "zbqh,gpio-interrupt", },
{},
};

static struct platform_driver gpio_interrupt_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "gpio-interrupt",
.of_match_table = match_table,
},
.probe = gpio_interrupt_probe,
};

static int gpio_interrupt_init(void)
{
misc_register(&gpio_interrupt_miscdevice);
return platform_driver_register(&gpio_interrupt_driver);
}
module_init(gpio_interrupt_init);

static void gpio_interrupt_exit(void)
{
misc_deregister(&gpio_interrupt_miscdevice);
platform_driver_unregister(&gpio_interrupt_driver);
}
module_exit(gpio_interrupt_exit);

MODULE_DESCRIPTION("GPIO interrupt driver");
MODULE_AUTHOR("kuretru <[email protected]");
MODULE_ALIAS("platform:gpio-interrupt");
MODULE_LICENSE("GPL v3");

  不要忘记增加对应的编译选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index e5c9ef4d2096..11f916ebab68 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -277,6 +277,13 @@ config GPIO_INGENIC

If unsure, say N.

+config GPIO_INTERRUPT
+ tristate "Rockchip GPIO interrupt support"
+ depends on GPIO_ROCKCHIP
+ select GPIOLIB_IRQCHIP
+ help
+ driver for GPIO interrupt mode on Rockchip SoCs.
+
config GPIO_IOP
tristate "Intel IOP GPIO"
depends on ARCH_IOP32X || ARCH_IOP33X || COMPILE_TEST
1
2
3
4
5
6
7
8
9
10
11
12
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index 644ce79e87b2..d3c7d9c1d1b3 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -58,6 +58,7 @@ obj-$(CONFIG_GPIO_HLWD) += gpio-hlwd.o
obj-$(CONFIG_HTC_EGPIO) += gpio-htc-egpio.o
obj-$(CONFIG_GPIO_ICH) += gpio-ich.o
obj-$(CONFIG_GPIO_INGENIC) += gpio-ingenic.o
+obj-$(CONFIG_GPIO_INTERRUPT) += gpio-interrupt.o
obj-$(CONFIG_GPIO_IOP) += gpio-iop.o
obj-$(CONFIG_GPIO_IT87) += gpio-it87.o
obj-$(CONFIG_GPIO_JANZ_TTL) += gpio-janz-ttl.o

DTS注册

1
2
3
4
5
6
7
8
9
/ {
gpio_irq {
status = "okay";
compatible = "zbqh,gpio-interrupt";
// 由于开和关闭按钮都要触发,因此设置成EDGE_BOTH
irq-gpios = <&gpio2 RK_PD4 IRQ_TYPE_EDGE_BOTH
&gpio2 RK_PD5 IRQ_TYPE_EDGE_BOTH>;
};
};

安卓程序

  安卓程序首先要编写对应的native方法接口,然后设置一个GPIO中断需要回调的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.kuretru.android.board.sdk;

public class GpioInterrupt {
int fd = -1;

native int open(String path, GpioInterrupt gpioInterrupt);

native void close(int fd);

static {
System.loadLibrary("gpio-interrupt");
}

public void interrupt() {
Log.d(TAG, "Java Interrupted");
Toast.makeText(context, "GPIO中断", Toast.LENGTH_SHORT).show();
}
}

JNI库

Android.mk

1
2
3
4
5
6
7
8
9
10
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := gpio-interrupt
LOCAL_SRC_FILES := gpio-interrupt.cpp

LOCAL_LDLIBS += -llog

include $(BUILD_SHARED_LIBRARY)

GpioInterrupt.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_kuretru_android_board_sdk_GpioInterrupt */

#ifndef _Included_com_kuretru_android_board_sdk_GpioInterrupt
#define _Included_com_kuretru_android_board_sdk_GpioInterrupt
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *);

/*
* Class: com_kuretru_android_board_sdk_GpioInterrupt
* Method: open
* Signature: (Ljava/lang/String;Lcom/kuretru/android/board/sdk/GpioPublisher;)I
*/
JNIEXPORT jint JNICALL Java_com_kuretru_android_board_sdk_GpioInterrupt_open
(JNIEnv *, jobject, jstring, jobject);

/*
* Class: com_kuretru_android_board_sdk_GpioInterrupt
* Method: close
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_com_kuretru_android_board_sdk_GpioInterrupt_close
(JNIEnv *, jobject, jint);

#ifdef __cplusplus
}
#endif
#endif

gpio-interrupt.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <jni.h>

#ifdef __cplusplus
extern "C"
{
#endif

#include <android/log.h>
#define LOG_TAG "gpio-interrupt"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)

#include <fcntl.h>
#include <unistd.h>
#include <GpioInterrupt.h>

static JavaVM *j_vm = nullptr;
static jclass gpio_publisher_class;
static jobject gpio_publisher_object;
static jmethodID interrupt_method;

// 中断调用函数
void interrupt(int signum) {
LOGD("JNI interrupted");
if (j_vm == nullptr) {
LOGE("interrupted, j_vm == nullptr");
return;
}
JNIEnv *env = nullptr;
bool detached = j_vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_EDETACHED;
if (detached) {
LOGE("interrupted, detached thread, attaching");
j_vm->AttachCurrentThread(&env, nullptr);
}

LOGE("interrupted, before CallVoidMethod()");
// 保证方法的同步性,相当于Java中的synchronized
env->MonitorEnter(gpio_publisher_object);
env->CallVoidMethod(gpio_publisher_object, interrupt_method);
env->MonitorExit(gpio_publisher_object);
LOGE("interrupted, after CallVoidMethod()");

if (detached) {
LOGE("interrupted, detached thread, detaching");
j_vm->DetachCurrentThread();
}
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGD("on JNI_OnLoad");

j_vm = vm;
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}

// 载入时,在全局保存一些需要用到的实例
jclass clazz = env->FindClass("com/kuretru/android/board/sdk/GpioInterrupt");
gpio_publisher_class = reinterpret_cast<jclass>(env->NewGlobalRef(clazz));
interrupt_method = env->GetMethodID(gpio_publisher_class, "interrupt", "()V");

return JNI_VERSION_1_6;
}

JNIEXPORT jint JNICALL Java_com_kuretru_android_board_sdk_GpioInterrupt_open
(JNIEnv *env, jobject instance, jstring path, jobject gpio_publisher) {
LOGD("start open device\n");
const char *file_path = env->GetStringUTFChars(path, nullptr);
// 打开/dev/gpio-interrupt设备
int fd = open(file_path, O_RDWR);
if (fd < 0) {
LOGW("open device failed\n");
return fd;
}
env->ReleaseStringUTFChars(path, file_path);

// 保存传入的GpioInterrupt实例
gpio_publisher_object = env->NewGlobalRef(gpio_publisher);

// 注册IO信号,并且保证若有多个IO信号同时到达时,存入一个队列,按序触发
struct sigaction action{};
action.sa_handler = interrupt;
sigemptyset(&action.sa_mask);
sigaddset(&action.sa_mask, SIGIO);
action.sa_flags = SA_RESTART;
sigaction(SIGIO, &action, nullptr);
fcntl(fd, F_SETOWN, getpid());

// 开始接收信号
int oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, oflags | FASYNC);
LOGD("finish open device\n");
return fd;
}

JNIEXPORT void JNICALL Java_com_kuretru_android_board_sdk_GpioInterrupt_close
(JNIEnv *env, jobject instance, jint fd) {
env->DeleteGlobalRef(gpio_publisher_class);
env->DeleteGlobalRef(gpio_publisher_object);
close(fd);
}

#ifdef __cplusplus
}
#endif%

坑点与总结

  最开始JNI收到中断信号后,调用Android代码,死活会报错,导致Java虚拟机直接崩溃,甚至没有任何日志,这个问题困扰了好几天。猜测是反复触发中断导致的,但是在JNI侧在Android侧加同步锁都无法解决。偶然间发现可能需要给信号加锁,也就是保证同一时间只处理一个信号,剩余的信号放到一个队列中等待处理完成后再续触发,即可解决这个问题。

参考文献