多点电容触摸驱动框架
前面几小节已经详细的讲解了 linux 下多点触摸屏驱动原理,本小节我们来梳理一下 linux 下多点电容触摸驱动的编写框架和步骤。首先确定驱动需要用到哪些知识点,哪些框架?根据前面的分析,我们在编写驱动的时候需要注意一下几点:
① 多点电容触摸芯片的接口,一般都为 I2C 接口,因此驱动主框架肯定是 I2C。
② linux 里面一般都是通过中断来上报触摸点坐标信息,因此需要用到中断框架。
③ 多点电容触摸属于 input 子系统,因此还要用到 input 子系统框架。
④ 在中断处理程序中按照 linux 的 MT 协议上报坐标信息。
根据上面的分析,多点电容触摸驱动编写框架以及步骤如下:
1 、I2C 驱动框架
驱动总体采用 I2C 框架,参考框架代码如下所示:
1 /* 设备树匹配表 */
2 static const struct i2c_device_id xxx_ts_id[] = {
3 { "xxx", 0, },
4 { /* sentinel */ }
5 };
6
7 /* 设备树匹配表 */
8 static const struct of_device_id xxx_of_match[] = {
9 { .compatible = "xxx", },
10 { /* sentinel */ }
11 };
12
13 /* i2c 驱动结构体 */
14 static struct i2c_driver ft5x06_ts_driver = {
15 .driver = {
16 .owner = THIS_MODULE,
17 .name = "edt_ft5x06",
18 .of_match_table = of_match_ptr(xxx_of_match),
19 },
20 .id_table = xxx_ts_id,
21 .probe = xxx_ts_probe,
22 .remove = xxx_ts_remove,
23 };
24
25 /*
26 * @description : 驱动入口函数
27 * @param : 无
28 * @return : 无
29 */
30 static int __init xxx_init(void)
31 {
32 int ret = 0;
33
34 ret = i2c_add_driver(&xxx_ts_driver);
35
36 return ret;
37 }
38
39 /*
40 * @description : 驱动出口函数
41 * @param : 无
42 * @return : 无
43 */
44 static void __exit xxx_exit(void)
45 {
46 i2c_del_driver(&ft5x06_ts_driver);
47 }
48
49 module_init(xxx_init);
50 module_exit(xxx_exit);
51 MODULE_LICENSE("GPL");
52 MODULE_AUTHOR("topeet");
当设备树中触摸 IC 的设备节点和驱动匹配以后,第 21 行的 xxx_ts_probe 函数就会执行,我们可以在此函数中初始化触摸 IC,中断和 input 子系统等。
2摸 、初始化触摸 IC和 、中断和 input 子系统
初始化操作都是在 xxx_ts_probe 函数中完成,参考框架如下所示:
1 static int xxx_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
2 {
3 struct input_dev *input;
4
5 /* 1、初始化 I2C */
6 ......
7
8 /* 2,申请中断, */
9 devm_request_threaded_irq(&client->dev, client->irq, NULL,
10 xxx_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
11 client->name, &xxx);
12 ......
13
14 /* 3,input 设备申请与初始化 */
15 input = devm_input_allocate_device(&client->dev);
16
17 input->name = client->name;
18 input->id.bustype = BUS_I2C;
19 input->dev.parent = &client->dev;
20 ......
21
22 /* 4,初始化 input 和 MT */
23 __set_bit(EV_ABS, input->evbit);
24 __set_bit(BTN_TOUCH, input->keybit);
25
26 input_set_abs_params(input, ABS_X, 0, width, 0, 0);
27 input_set_abs_params(input, ABS_Y, 0, height, 0, 0);
28 input_set_abs_params(input, ABS_MT_POSITION_X,0, width, 0, 0);
29 input_set_abs_params(input, ABS_MT_POSITION_Y,0, height, 0, 0);
30 input_mt_init_slots(input, MAX_SUPPORT_POINTS, 0);
31 ......
32
33 /* 5,注册 input_dev */
34 input_register_device(input);
35 ......
36 }
第 5~7 行,首先肯定是初始化触摸芯片,包括芯片的相关 IO,比如复位、中断等 IO 引脚,然后就是芯片本身的初始化,也就是配置触摸芯片的相关寄存器。
第 9 行,因为一般触摸芯片都是通过中断来向系统上报触摸点坐标信息的,因此我们需要初始化中断。大家可能会发现第 9 行并没有使用 request_irq 函数申请中断,而是采用了 devm_request_threaded_irq 这个函数,为什么使用这个函数呢?是不是 request_irq 函数不能使用?答案肯定不是的,这里用 request_irq函数是绝对没问题的。那为何要用 devm_request_threaded_irq 呢?这里我们就简单的介绍一下这个 API
函数,devm_request_threaded_irq 函数特点如下:
① 用于申请中断,作用和 request_irq 函数类似。
② 此函数的作用是中断线程化,大家如果直接在网上搜索“devm_request_threaded_irq”会发现相关解释很少。但是大家去搜索 request_threaded_irq 函数就会有很多讲解的博客和帖子,这两个函数在名字上的差别就是前者比后者多了个“devm_”前缀,“devm_”前缀稍后讲解。大家应该注意到了
“request_threaded_irq”相比“request_irq”多了个 threaded 函数,也就是线程的意思。那么为什么要中断线程化呢?我们都是知道硬件中断具有最高优先级,不论什么时候只要硬件中断发生,那么内核都会终止当前正在执行的操作,转而去执行中断处理程序(不考虑关闭中断和中断优先级的情况),如果中断非常频
繁的话那么内核将会频繁的执行中断处理程序,导致任务得不到及时的处理。中断线程化以后中断将作为内核线程运行,而且也可以被赋予不同的优先级,任务的优先级可能比中断线程的优先级高,这样做的目的就是保证高优先级的任务能被优先处理。大家可能会疑问,前面不是说可以将比较耗时的中断放到下半
部(bottom half)处理吗?虽然下半部可以被延迟处理,但是依旧先于线程执行,中断线程化可以让这些比较耗时的下半部与进程进行公平竞争。
要注意,并不是所有的中断都可以被线程化,重要的中断就不能这么操作。对于触摸屏而言只要手指放到屏幕上,它可能就会一直产生中断(视具体芯片而定,FT5426 是这样的),中断处理程序里面需要通过 I2C读取触摸信息并上报给内核,I2C 的速度最大只有 400KHz,算是低速外设。不断的产生中断、读取触摸信
息、上报信息会导致处理器在触摸中断上花费大量的时间,但是触摸相对来说不是那么重要的事件,因此可以将触摸中断线程化。如果你觉得触摸中断很重要,那么就可以不将其进行线程化处理。总之,要不要将一个中断进行线程化处理是需要自己根据实际情况去衡量的。
③ 最后来看一下“devm_”前缀,在 linux 内核中有很多的申请资源类的 API 函数都有对应的“devm_”前缀版本。比如 devm_request_irq 和 request_irq 这两个函数,这两个函数都是申请中断的,我们使用request_irq 函数申请中断的时候,如果驱动初始化失败的话就要调用 free_irq 函数对申请成功的 irq 进行
释放,卸载驱动的时候也需要我们手动调用 free_irq 来释放 irq。假如我们的驱动里面申请了很多资源,比如:gpio、irq、input_dev,那么就需要添加很多goto 语句对其做处理,当这样的标签多了以后代码看起来就不整洁了。“devm_”函数就是为了处理这种情况而诞生的,“devm_”函数最大的作用就是:
使用“devm_”前缀的函数申请到的资源可以由系统自动释放,不需要我们手动处理。 如果我们使用devm_request_threaded_irq 函数来申请中断,那么就不需要我们再调用 free_irq 函数对其进行释放。大家可以注意一下,带有“devm_”前缀的都是一些和设备资源管理有关的函数。关于“devm_”函数的实现原理这里就不做详细的讲解了,我们的重点在于学会如何使用这些 API 函数,感兴趣的可以查阅一些其他文档或者帖子来看一下“devm_”函数的实现原理。
第 15 行,接下来就是申请 input_dev,因为多点电容触摸属于 input 子系统。这里同样使用devm_input_allocate_device 函数来申请 input_dev,也就是我们前面讲解的 input_allocate_device 函数加“devm_”前缀版本。申请到 input_dev 以后还需要对其进行初始化操作。
第 23~24 行,设置 input_dev 需要上报的事件为 EV_ABS 和 BTN_TOUCH,因为多点电容屏的触摸坐标为绝对值,因此需要上报 EV_ABS 事件。触摸屏有按下和抬起之分,因此需要上报 BTN_TOUCH 按键。
第 26~29 行,调用 input_set_abs_params 函数设置 EV_ABS 事件需要上报 ABS_X、ABS_Y、ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。单点触摸需要上报 ABS_X 和 ABS_Y,对于多点触摸需要上报 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。
第 30 行,调用 input_mt_init_slots 函数初始化多点电容触摸的 slots。
第 34 行,调用 input_register_device 函数系统注册前面申请到的 input_dev。
3 、上报坐标信息
最后就是在中断服务程序中上报读取到的坐标信息,根据所使用的多点电容触摸设备类型选择使用TypeA 还是 Type B 时序。由于大多数的设备都是 Type B 类型,因此这里就以 Type B 类型为例讲解一下上报过程,参考驱动框架如下所示:
1 static irqreturn_t xxx_handler(int irq, void *dev_id)
2 {
3
4 int num; /* 触摸点数量 */
5 int x[n], y[n]; /* 保存坐标值 */
6
7 /* 1、从触摸芯片获取各个触摸点坐标值 */
8 ......
9
10 /* 2、上报每一个触摸点坐标 */
11 for (i = 0; i < num; i++) {
12 input_mt_slot(input, id);
13 input_mt_report_slot_state(input, MT_TOOL_FINGER, true);
14 input_report_abs(input, ABS_MT_POSITION_X, x);
15 input_report_abs(input, ABS_MT_POSITION_Y, y);
16 }
17 ......
18
19 input_sync(input);
20 ......
21
22 return IRQ_HANDLED;
23 }
进入中断处理程序以后首先肯定是从触摸 IC 里面读取触摸坐标以及触摸点数量,假设触摸点数量保存到 num 变量,触摸点坐标存放到 x,y 数组里面。
第 11~16 行,循环上报每一个触摸点坐标,一定要按照 Type B 类型的时序进行。
第 19 行,每一轮触摸点坐标上报完毕以后就调用一次 input_sync 函数发送一个 SYN_REPORT 事件。
关于多点电容触摸驱动框架就讲解到这里,接下来我们就实际编写一个多点电容触摸驱动程序。
多点电容触摸驱动框架
前面几小节已经详细的讲解了 linux 下多点触摸屏驱动原理,本小节我们来梳理一下 linux 下多点电容触摸驱动的编写框架和步骤。首先确定驱动需要用到哪些知识点,哪些框架?根据前面的分析,我们在编写驱动的时候需要注意一下几点:
① 多点电容触摸芯片的接口,一般都为 I2C 接口,因此驱动主框架肯定是 I2C。
② linux 里面一般都是通过中断来上报触摸点坐标信息,因此需要用到中断框架。
③ 多点电容触摸属于 input 子系统,因此还要用到 input 子系统框架。
④ 在中断处理程序中按照 linux 的 MT 协议上报坐标信息。
根据上面的分析,多点电容触摸驱动编写框架以及步骤如下:
1 、I2C 驱动框架
驱动总体采用 I2C 框架,参考框架代码如下所示:
1 /* 设备树匹配表 */
2 static const struct i2c_device_id xxx_ts_id[] = {
3 { "xxx", 0, },
4 { /* sentinel */ }
5 };
6
7 /* 设备树匹配表 */
8 static const struct of_device_id xxx_of_match[] = {
9 { .compatible = "xxx", },
10 { /* sentinel */ }
11 };
12
13 /* i2c 驱动结构体 */
14 static struct i2c_driver ft5x06_ts_driver = {
15 .driver = {
16 .owner = THIS_MODULE,
17 .name = "edt_ft5x06",
18 .of_match_table = of_match_ptr(xxx_of_match),
19 },
20 .id_table = xxx_ts_id,
21 .probe = xxx_ts_probe,
22 .remove = xxx_ts_remove,
23 };
24
25 /*
26 * @description : 驱动入口函数
27 * @param : 无
28 * @return : 无
29 */
30 static int __init xxx_init(void)
31 {
32 int ret = 0;
33
34 ret = i2c_add_driver(&xxx_ts_driver);
35
36 return ret;
37 }
38
39 /*
40 * @description : 驱动出口函数
41 * @param : 无
42 * @return : 无
43 */
44 static void __exit xxx_exit(void)
45 {
46 i2c_del_driver(&ft5x06_ts_driver);
47 }
48
49 module_init(xxx_init);
50 module_exit(xxx_exit);
51 MODULE_LICENSE("GPL");
52 MODULE_AUTHOR("topeet");
当设备树中触摸 IC 的设备节点和驱动匹配以后,第 21 行的 xxx_ts_probe 函数就会执行,我们可以在此函数中初始化触摸 IC,中断和 input 子系统等。
2摸 、初始化触摸 IC和 、中断和 input 子系统
初始化操作都是在 xxx_ts_probe 函数中完成,参考框架如下所示:
1 static int xxx_ts_probe(struct i2c_client *client, const struct i2c_device_id *id)
2 {
3 struct input_dev *input;
4
5 /* 1、初始化 I2C */
6 ......
7
8 /* 2,申请中断, */
9 devm_request_threaded_irq(&client->dev, client->irq, NULL,
10 xxx_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
11 client->name, &xxx);
12 ......
13
14 /* 3,input 设备申请与初始化 */
15 input = devm_input_allocate_device(&client->dev);
16
17 input->name = client->name;
18 input->id.bustype = BUS_I2C;
19 input->dev.parent = &client->dev;
20 ......
21
22 /* 4,初始化 input 和 MT */
23 __set_bit(EV_ABS, input->evbit);
24 __set_bit(BTN_TOUCH, input->keybit);
25
26 input_set_abs_params(input, ABS_X, 0, width, 0, 0);
27 input_set_abs_params(input, ABS_Y, 0, height, 0, 0);
28 input_set_abs_params(input, ABS_MT_POSITION_X,0, width, 0, 0);
29 input_set_abs_params(input, ABS_MT_POSITION_Y,0, height, 0, 0);
30 input_mt_init_slots(input, MAX_SUPPORT_POINTS, 0);
31 ......
32
33 /* 5,注册 input_dev */
34 input_register_device(input);
35 ......
36 }
第 5~7 行,首先肯定是初始化触摸芯片,包括芯片的相关 IO,比如复位、中断等 IO 引脚,然后就是芯片本身的初始化,也就是配置触摸芯片的相关寄存器。
第 9 行,因为一般触摸芯片都是通过中断来向系统上报触摸点坐标信息的,因此我们需要初始化中断。大家可能会发现第 9 行并没有使用 request_irq 函数申请中断,而是采用了 devm_request_threaded_irq 这个函数,为什么使用这个函数呢?是不是 request_irq 函数不能使用?答案肯定不是的,这里用 request_irq函数是绝对没问题的。那为何要用 devm_request_threaded_irq 呢?这里我们就简单的介绍一下这个 API
函数,devm_request_threaded_irq 函数特点如下:
① 用于申请中断,作用和 request_irq 函数类似。
② 此函数的作用是中断线程化,大家如果直接在网上搜索“devm_request_threaded_irq”会发现相关解释很少。但是大家去搜索 request_threaded_irq 函数就会有很多讲解的博客和帖子,这两个函数在名字上的差别就是前者比后者多了个“devm_”前缀,“devm_”前缀稍后讲解。大家应该注意到了
“request_threaded_irq”相比“request_irq”多了个 threaded 函数,也就是线程的意思。那么为什么要中断线程化呢?我们都是知道硬件中断具有最高优先级,不论什么时候只要硬件中断发生,那么内核都会终止当前正在执行的操作,转而去执行中断处理程序(不考虑关闭中断和中断优先级的情况),如果中断非常频
繁的话那么内核将会频繁的执行中断处理程序,导致任务得不到及时的处理。中断线程化以后中断将作为内核线程运行,而且也可以被赋予不同的优先级,任务的优先级可能比中断线程的优先级高,这样做的目的就是保证高优先级的任务能被优先处理。大家可能会疑问,前面不是说可以将比较耗时的中断放到下半
部(bottom half)处理吗?虽然下半部可以被延迟处理,但是依旧先于线程执行,中断线程化可以让这些比较耗时的下半部与进程进行公平竞争。
要注意,并不是所有的中断都可以被线程化,重要的中断就不能这么操作。对于触摸屏而言只要手指放到屏幕上,它可能就会一直产生中断(视具体芯片而定,FT5426 是这样的),中断处理程序里面需要通过 I2C读取触摸信息并上报给内核,I2C 的速度最大只有 400KHz,算是低速外设。不断的产生中断、读取触摸信
息、上报信息会导致处理器在触摸中断上花费大量的时间,但是触摸相对来说不是那么重要的事件,因此可以将触摸中断线程化。如果你觉得触摸中断很重要,那么就可以不将其进行线程化处理。总之,要不要将一个中断进行线程化处理是需要自己根据实际情况去衡量的。
③ 最后来看一下“devm_”前缀,在 linux 内核中有很多的申请资源类的 API 函数都有对应的“devm_”前缀版本。比如 devm_request_irq 和 request_irq 这两个函数,这两个函数都是申请中断的,我们使用request_irq 函数申请中断的时候,如果驱动初始化失败的话就要调用 free_irq 函数对申请成功的 irq 进行
释放,卸载驱动的时候也需要我们手动调用 free_irq 来释放 irq。假如我们的驱动里面申请了很多资源,比如:gpio、irq、input_dev,那么就需要添加很多goto 语句对其做处理,当这样的标签多了以后代码看起来就不整洁了。“devm_”函数就是为了处理这种情况而诞生的,“devm_”函数最大的作用就是:
使用“devm_”前缀的函数申请到的资源可以由系统自动释放,不需要我们手动处理。 如果我们使用devm_request_threaded_irq 函数来申请中断,那么就不需要我们再调用 free_irq 函数对其进行释放。大家可以注意一下,带有“devm_”前缀的都是一些和设备资源管理有关的函数。关于“devm_”函数的实现原理这里就不做详细的讲解了,我们的重点在于学会如何使用这些 API 函数,感兴趣的可以查阅一些其他文档或者帖子来看一下“devm_”函数的实现原理。
第 15 行,接下来就是申请 input_dev,因为多点电容触摸属于 input 子系统。这里同样使用devm_input_allocate_device 函数来申请 input_dev,也就是我们前面讲解的 input_allocate_device 函数加“devm_”前缀版本。申请到 input_dev 以后还需要对其进行初始化操作。
第 23~24 行,设置 input_dev 需要上报的事件为 EV_ABS 和 BTN_TOUCH,因为多点电容屏的触摸坐标为绝对值,因此需要上报 EV_ABS 事件。触摸屏有按下和抬起之分,因此需要上报 BTN_TOUCH 按键。
第 26~29 行,调用 input_set_abs_params 函数设置 EV_ABS 事件需要上报 ABS_X、ABS_Y、ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。单点触摸需要上报 ABS_X 和 ABS_Y,对于多点触摸需要上报 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y。
第 30 行,调用 input_mt_init_slots 函数初始化多点电容触摸的 slots。
第 34 行,调用 input_register_device 函数系统注册前面申请到的 input_dev。
3 、上报坐标信息
最后就是在中断服务程序中上报读取到的坐标信息,根据所使用的多点电容触摸设备类型选择使用TypeA 还是 Type B 时序。由于大多数的设备都是 Type B 类型,因此这里就以 Type B 类型为例讲解一下上报过程,参考驱动框架如下所示:
1 static irqreturn_t xxx_handler(int irq, void *dev_id)
2 {
3
4 int num; /* 触摸点数量 */
5 int x[n], y[n]; /* 保存坐标值 */
6
7 /* 1、从触摸芯片获取各个触摸点坐标值 */
8 ......
9
10 /* 2、上报每一个触摸点坐标 */
11 for (i = 0; i < num; i++) {
12 input_mt_slot(input, id);
13 input_mt_report_slot_state(input, MT_TOOL_FINGER, true);
14 input_report_abs(input, ABS_MT_POSITION_X, x);
15 input_report_abs(input, ABS_MT_POSITION_Y, y);
16 }
17 ......
18
19 input_sync(input);
20 ......
21
22 return IRQ_HANDLED;
23 }
进入中断处理程序以后首先肯定是从触摸 IC 里面读取触摸坐标以及触摸点数量,假设触摸点数量保存到 num 变量,触摸点坐标存放到 x,y 数组里面。
第 11~16 行,循环上报每一个触摸点坐标,一定要按照 Type B 类型的时序进行。
第 19 行,每一轮触摸点坐标上报完毕以后就调用一次 input_sync 函数发送一个 SYN_REPORT 事件。
关于多点电容触摸驱动框架就讲解到这里,接下来我们就实际编写一个多点电容触摸驱动程序。
举报