Zephyr上添加触屏驱动

Creative Commons
本作品采用知识共享署名

本文简要介绍Zephyr下kscan驱动模型,并说明如何在kscan模型下实现触屏驱动。

Zephyr下kscan(keyboard scan matrix)驱动程序用于检测矩阵键盘或带有按钮的设备中的按键。 由于kscan驱动模型并不定义键值,而是通过按键的行列坐标来标识按键,因此kscan驱动模型也适用于触摸屏。

KSCAN驱动模型

kscan驱动模型定义了一组很简洁的接口和回调,方便应用使用驱动。

外部接口

接口列表

1
int kscan_config(struct device *dev,kscan_callback_t callback);

注册一个callback,当按键发生时通过callback通知应用。

1
int kscan_enable_callback(struct device *dev);

应用注册callback后,通过该API使能callback,使能后有按键发生才会调用callback。

1
int kscan_disable_callback(struct device *dev);

禁止callback,当按键发生时不调用callback,相当于是禁止键盘使用

Callback

当按键发生时,驱动会调用注册的callback来通知应用有按键发生,callback的形式如下

1
typedef void (*kscan_callback_t)(struct device *dev, u32_t row, u32_t column, bool pressed);

row 按键所在行
column 按键所在列
pressed true按键按下, false按键松开
callback的参数不指定固定的按键值二十使用行和列一个非常大的好处就是灵活性很高,几乎可以兼容任何类矩阵输入设备,例如触屏和鼠标。

内部接口

驱动模型内部定义了下面三种函数指针类型,对应于外部的三个接口

1
2
3
4
5
6
7
8
9
10
typedef int (*kscan_config_t)(struct device *dev,
kscan_callback_t callback);
typedef int (*kscan_disable_callback_t)(struct device *dev);
typedef int (*kscan_enable_callback_t)(struct device *dev);

struct kscan_driver_api {
kscan_config_t config;
kscan_disable_callback_t disable_callback;
kscan_enable_callback_t enable_callback;
};

在驱动实现时实现这三个内部函数,并通过kscan_driver_api注册即可完成驱动,基本原理和方法可以参考zephyr驱动模型Zephyr驱动模型实现方式

触屏驱动实现

上一节所列出来的引用文章,是单针对驱动接口的实现,在实际的驱动添加中还有诸如dts,kconfig的添加和修改,本节以gt917s为例一步一步说明如何完成一个kscan驱动的添加。注意,本文是基于Zephyr2.20进行,设备树的宏在代码中体现的还是完整的宏,2.30后Zephyr已经引入了设备树宏生成API,两者之间在DTS宏的体现上不一样。

kscan的代码主要是在下面三个地方:
头文件 include/driver/kscan.h 定义了驱动接口,不需要修改
设备树 dts/binding/kscan/ 放置不同类型驱动的绑定文件,和硬件相关
源代码 drivers/kscan/ 放置不同类型驱动的源代码和kconfig文件

Step1 增加设备树绑定文件

Zephyr设备树原理和使用方法参考Zephyr设备树生成流程Zephyr设备树生成C宏规则。要添加设备树绑定需要先了解硬件,下图为gt917s触摸屏的接口原理图:
touchif
接口驱动方法需要详细阅读gt917s的规格书,本文的重点是如何添加驱动,因此只列出和添加需要相关的内容,这里简要列出接口的使用方法:

  • I2C接口:用于写入和读取gt917s的寄存器,以获得触摸信息
  • INT: 触摸发生时会通过INT通知
  • RST:触摸屏初始化时和INT配合决定触摸屏I2C地址
  • 触摸屏范围可配置:触摸屏的最大范围宽高可根据需求配置

从上面的信息分析可以知道需要使用到I2C接口和两个GPIO,因此在绑定中描述这些硬件信息。触摸屏的宽高作为硬件驱动的配置信息我们也通过绑定来描述。在dts/binding/kscan/下新建绑定文件coodix,gt9x.yaml如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
description: GT9x capacitive touch panel

compatible: "coodix,gt9x"

include: [kscan.yaml, i2c-device.yaml]

properties:
width:
type: int
required: true
description: touch panel width
height:
type: int
required: true
description: touch panel height
int-gpios:
type: phandle-array
required: false
reset-gpios:
type: phandle-array
required: false

其中
include kscan.yaml是所有kscan的属性,目前只有lable
include i2c-device.yaml,是因为包含i2c设备,将其属性include进来
新添加的width和height属性指定触摸屏的宽和高
新添加的int-gpios指定触摸屏的中断gpio
新添加的reset-gpios指定触摸屏的reset gpio

Step2 修改kconfig

kconfig文件用于选择配置kscan驱动和配置kscan驱动纯软件层面的参数。这里添加gt917s驱动,那么就要修改kconfig让其具有gt917s的配置选项,修改方法如下

增加Kconfig.gt9x

在drivers/kscan下新建文件Kconfig.gt9x文件,该文件包含了针对驱动的纯软件配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
menuconfig KSCAN_GT9X
bool "GT91X capacitive touch panel driver"
depends on I2C && HAS_DTS_I2C
help
Enable driver for the GT91X capacitive touch panel controller.

#触摸屏采样周期,默认每10ms读一次触摸屏硬件,看是否有按键发生
config KSCAN_GT9X_PERIOD
int "Sample period (ms)"
default 10
depends on KSCAN_GT9X

#配置是否启用中断,启用后将在中断发生时读取触摸屏硬件,看按键发生情况
config KSCAN_GT9X_INTERRUPT
bool "Enable interrupt"
depends on KSCAN_GT9X

修改Kconfig

修改drivers/kscan/Kconfig, 让其引用Kconfig.gt9x,在Kconfig中添加如下即可

1
source "drivers/kscan/Kconfig.gt9x"

Step3 添加驱动代码

增加源文件

在drivers/kscan/下增加驱动的源代码文件kscan_gt9x.c,在drivers/kscan/CMakefiles.txt中增加下面内容,让编译系统可以编译源文件

1
zephyr_library_sources_ifdef(CONFIG_KSCAN_GT9X		kscan_gt9x.c)

驱动编写

具体驱动gt917s的代码spec有关不是本文说明的内容,本小节罗列驱动的主要框架,说明zephyr下如何实现kscan驱动,省略掉和spec细节相关的内容。
驱动工作的流程如下:

  • 上电时Zephyr调用 gt9x_init,对驱动进行初始化
  • 建立一个work queue,接受按键处理事件
  • 在非中断模式下建立一个timer,安装配置的时间进行触发(例如10ms)
  • 当timer发生时或者中断到来,在timer/中断中发送按键处理事件通知work queue处理按键
  • work queue收到按键处理事件后调用gt9x_read,通过i2c从gt917s中读出数据判断是否有实际按键发生
  • gt9x_read发现有实际按键发生时,呼叫callback将touch点发送给上层

实现的细节说明见注释:

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#include <drivers/kscan.h>
#include <drivers/i2c.h>
#include <drivers/gpio.h>
#include <logging/log.h>

LOG_MODULE_REGISTER(gt9x, CONFIG_KSCAN_LOG_LEVEL);

//定义gt917s使用的硬件结构体,用于管理驱动gt917s使用的硬件
struct gt9x_config {
char *i2c_name; //i2c device name
u8_t i2c_address; //gt917s的i2c地址
char *int_port ; //中断gpio port
u32_t int_pin; //中断gpio pin
u32_t int_flags; //中断gpio配置的flag信息
char *rst_port; //reset gpio port
u32_t rst_pin; //reset gpio pin
u32_t rst_flags; //reset gpio配置的flag
};

//定义gt917s驱动运行时数据信息
struct gt9x_data {
struct device *i2c; //i2c驱动handle
struct device *gpio_int; //int gpio驱动handle
struct device *gpio_rst; //reset gpio驱动handle
kscan_callback_t callback; //保存上层注册的callback
struct k_work work; //处理按键的work queue handle
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
struct gpio_callback int_gpio_cb; //使用中断时需要注册的中断callback
#else
struct k_timer timer; //不使用中断,使用轮询的timer
#endif
struct device *dev; //gt917s设备驱动的handle

};

//gt917s中断处理函数
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
static void gt9x_isr_handler(struct device *dev,
struct gpio_callback *cb, u32_t pins)
{
printk("touch int\n");
const struct gt9x_config *config = dev->config->config_info;
struct gt9x_data *drv_data =
CONTAINER_OF(cb, struct gt9x_data, int_gpio_cb);

gpio_pin_interrupt_configure(dev, config->int_pin,
GPIO_INT_DISABLE);

//中断发生时说明有touch发生,通知work queue处理
k_work_submit(&drv_data->work);
}
#endif

//timer处理函数,每CONFIG_KSCAN_GT9X_PERIOD执行一次
#ifndef CONFIG_KSCAN_GT9X_INTERRUPT
static void gt9x_timer_handler(struct k_timer *timer)
{
struct gt9x_data *data =
CONTAINER_OF(timer, struct gt9x_data, timer);
//通知work queue查询是否有touch发生
k_work_submit(&data->work);
}
#endif

//读gt917s
static void gt9x_read(struct device *dev)
{
const struct gt9x_config *config = dev->config->config_info;
struct gt9x_data *data = dev->driver_data;

/*
省略代码: 从gt917s中读取当前touch点信息
*/

//读到有touch发生时,通过callback通知上层处理,这里的callback是由gt9x_configure注册
data->callback(dev, pre_touch[i].y, pre_touch[i].x, pressed);

#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
gpio_pin_interrupt_configure(data->gpio_int,
config->int_pin, GPIO_INT_EDGE_TO_ACTIVE);
#endif


return ret;
}

//work queue处理函数,work submit的时候执行
static void gt9x_work_handler(struct k_work *work)
{
struct gt9x_data *data =
CONTAINER_OF(work, struct gt9x_data, work);
//work queue中读取当前gt917s信息
gt9x_read(data->dev);
}


//注册callback
static int gt9x_configure(struct device *dev, kscan_callback_t callback)
{
struct gt9x_data *data = dev->driver_data;

if (!callback) {
return -EINVAL;
}
//将传入的callback保存下来
data->callback = callback;

return 0;
}

//使能callback
static int gt9x_enable_callback(struct device *dev)
{
struct gt9x_data *data = dev->driver_data;

#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
//使能中断让驱动开始读取gt917s
gpio_add_callback(data->gpio_int, &data->int_gpio_cb);
#else
//启动timer,让驱动开始轮询
k_timer_start(&data->timer, K_MSEC(CONFIG_KSCAN_GT9X_PERIOD),
K_MSEC(CONFIG_KSCAN_GT9X_PERIOD));
#endif
return 0;
}

//禁用callback
static int gt9x_disable_callback(struct device *dev)
{
struct gt9x_data *data = dev->driver_data;
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT
//禁止中断,驱动停止读取gt917s
gpio_remove_callback(data->gpio_int, &data->int_gpio_cb);
#else
//停止timer,驱动停止轮询
k_timer_stop(&data->timer);
#endif
return 0;
}

//初始化gt917s
static int gt9x_init(struct device *dev)
{
const struct gt9x_config *config = dev->config->config_info;
struct gt9x_data *data = dev->driver_data;
u8_t version[GT9X_VERSION_LEN];
int ret = -1;
u16_t check_sum;
u16_t reg_num;

//初始化中断gpio
#ifdef DT_INST_0_COODIX_GT9X_INT_GPIOS_CONTROLLER
data->gpio_int = device_get_binding(config->int_port);
if (data->gpio_int == NULL) {
LOG_ERR("Could not find INT GPIO device");
return -EINVAL;
}
#endif

//初始化reset gpio
#ifdef DT_INST_0_COODIX_GT9X_RESET_GPIOS_CONTROLLER
data->gpio_rst = device_get_binding(config->rst_port);
if (data->gpio_rst == NULL) {
LOG_ERR("Could not find rst GPIO device");
return -EINVAL;
}
#endif

/*
省略代码:通过int和reset gpio配置gt917s的i2c地址
*/


//配置中断gpio以及gpio中断的callback
if(data->gpio_int != NULL){
gpio_pin_configure(data->gpio_int, config->int_pin, GPIO_INPUT | config->int_flags);
#ifdef CONFIG_KSCAN_GT9X_INTERRUPT

ret = gpio_pin_interrupt_configure(data->gpio_int,
config->int_pin, GPIO_INT_EDGE_TO_ACTIVE);
if (ret != 0) {
LOG_ERR("Error %d: failed to configure pin interrupt %d '%s'\n",
ret, config->int_pin, config->int_port);
return -EINVAL;
}
gpio_init_callback(&data->int_gpio_cb, gt9x_isr_handler, BIT(config->int_pin));
#endif
}

//获取使用i2c的handle
data->i2c = device_get_binding(config->i2c_name);
if (data->i2c == NULL) {
LOG_ERR("Could not find I2C device");
return -EINVAL;
}

/*
省略代码:通过i2c驱动对gt917s进行配置初始化
*/

//保存本驱动的handle
data->dev = dev;


//初始化读取touch的work queue
k_work_init(&data->work, gt9x_work_handler);

//非中断模式,初始化timer用作轮询
#ifndef CONFIG_KSCAN_GT9X_INTERRUPT
k_timer_init(&data->timer, gt9x_timer_handler, NULL);
#endif

return 0;
}


//初始化驱动的API,这些API将注册进Zephyr驱动模型内,之后通过kscan.h中的API呼叫就会对应到这些API
static const struct kscan_driver_api gt9x_driver_api = {
.config = gt9x_configure,
.enable_callback = gt9x_enable_callback,
.disable_callback = gt9x_disable_callback,
};

//初始化驱动的硬件信息,这些信息是DTS根据绑定文件产生,DTS的写法后文会介绍
static const struct gt9x_config gt9x_config = {
.i2c_name = DT_INST_0_COODIX_GT9X_BUS_NAME,
.i2c_address = DT_INST_0_COODIX_GT9X_BASE_ADDRESS,
.int_port = DT_INST_0_COODIX_GT9X_INT_GPIOS_CONTROLLER,
.int_pin = DT_INST_0_COODIX_GT9X_INT_GPIOS_PIN,
.int_flags = DT_INST_0_COODIX_GT9X_INT_GPIOS_FLAGS,
.rst_port = DT_INST_0_COODIX_GT9X_RESET_GPIOS_CONTROLLER,
.rst_pin = DT_INST_0_COODIX_GT9X_RESET_GPIOS_PIN,
.rst_flags = DT_INST_0_COODIX_GT9X_RESET_GPIOS_FLAGS,
};

static struct gt9x_data gt9x_data;

//注册驱动,注册后zephyr在驱动初始化阶段会自动调用gt9x_init对驱动进行初始化,注册后通过DT_INST_0_COODIX_GT9X_LABEL获得gt917s的handle,呼叫kscan.h中的API就会调用到gt9x_driver_api内对应的API
DEVICE_AND_API_INIT(GT9X, DT_INST_0_COODIX_GT9X_LABEL, gt9x_init,
&gt9x_data, &gt9x_config,
POST_KERNEL, CONFIG_KSCAN_INIT_PRIORITY,
&gt9x_driver_api);

KSCAN驱动使用

Kscan使用比较简单,配置好后,注册callback然后enable callback就可以了坐等callback接受按键了

配置

配置分为2部分,一是在dts中指定kscan的硬件信息,二是在prj.conf中启动配置使用kscan

dts

以我使用的mm_swiftio为例,在mm_swiftio.dts中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
&i2c1 {                 //gt917s挂在i2c1下
status = "okay";

gt9x@14 {
compatible = "coodix,gt9x";
reg = <0x14>; //gt917s的i2c地址为0x14
label = "GT9X"; //gt917s的device name为GT9X
width = <800>; //触摸屏宽800
height = <480>; //触摸屏宽480
int-gpios = <&gpio1 19 GPIO_ACTIVE_HIGH>; //中断使用gpio1.19
reset-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>; //复位使用gpio1.2
};
};

dts配置的信息会被zephyr的构建脚本转化为gt9x_config内使用的宏。

prj.conf

使用下面配置选项启用GT917S的KSCAN驱动

1
2
3
4
5
CONFIG_KSCAN=y                  #enable kscan
CONFIG_KSCAN_GT9X=y #使用GT917S驱动
CONFIG_KSCAN_INIT_PRIORITY=55 #驱动初始化优先级
CONFIG_KSCAN_GT9X_PERIOD=10 #轮询GT917S的时间间隔,单位为ms
#CONFIG_KSCAN_GT9X_INTERRUPT=y #配置该项后将只用中断通知接收按键,不再进行轮询

注意:CONFIG_KSCAN_INIT_PRIORITY不配置时默认是40,由于Kscan使用了I2C驱动,I2C驱动使用的是CONFIG_KERNEL_INIT_PRIORITY_DEVICE(50)来作为初始化优先级, 为了避免kscan使用I2C时无效,需要保证I2C驱动先初始化,初始化优先级的数字越大优先级越小。因此要配置CONFIG_KSCAN_INIT_PRIORITY>CONFIG_KERNEL_INIT_PRIORITY_DEVICE.

参考

https://docs.zephyrproject.org/latest/reference/peripherals/kscan.html