文章目录
- 前言
- 电容触摸屏特点
- MT触摸消息
-
- 电容触摸屏协议
- 电容屏触摸时序
-
- [Type A 触摸点信息上报时序](#Type A 触摸点信息上报时序)
- [Type B 触摸点信息上报时序](#Type B 触摸点信息上报时序)
- 多点触摸所使用到的API函数
- 驱动部分
- 参考文献
前言
随着智能手机的发展,电容触摸屏也得到了飞速的发展。相比电阻触摸屏,电容触摸屏有 很多的优势,比如支持多点触控、不需要按压,只需要轻轻触摸就有反应。
电容触摸屏特点
- 电容触摸屏是 IIC 接口的,需要触摸IC,以正点原子的 ATK7016 为例,其所使用的触摸屏控制IC为 FT5426,因此所谓的电容触摸驱动就是 IIC 设备驱动。
- 触摸 IC 提供了中断信号引脚(INT),可以通过中断来获取触摸信息。
- 电容触摸屏得到的是触摸位置绝对信息以及触摸屏是否有按下。
- 电容触摸屏不需要校准,当然了,这只是理论上的,如果电容触摸屏质量比较差,或者触摸玻璃和 TFT 之间没有完全对齐,那么也是需要校准的。
因此对应到驱动开发上,我们可以得出电容触摸屏驱动其实就是以下几种linux驱动框架的组合:
- IIC 设备驱动,因为电容触摸 IC 基本都是 IIC 接口的,因此大框架就是 IIC 设备驱动。
- 通过中断引脚(INT)向 linux 内核上报触摸信息,因此需要用到 linux 中断驱动框架。坐 标的上报在中断服务函数中完成。
- 触摸屏的坐标信息、屏幕按下和抬起信息都属于 linux 的 input 子系统,因此向 linux 内 核上报触摸屏坐标信息就得使用 input 子系统。只是,我们得按照 linux 内核规定的规则来上报 坐标信息。
MT触摸消息
电容触摸屏协议
老版本的 linux 内核是不支持多点电容触摸的(Multi-touch,简称 MT),MT 协议是后面加入 的,因此如果使用 2.x 版本 linux 内核的话可能找不到 MT 协议。MT 协议被分为两种类型,Type A 和 TypeB,这两种类型的区别如下:
Type A :适用于触摸点不能被区分或者追踪,此类型的设备上报原始数据(此类型在实际使 用中非常少!)。
Type B:适用于有硬件追踪并能区分触摸点的触摸设备,此类型设备通过 slot 更新某一个 触摸点的信息,FT5426 就属于此类型,一般的多点电容触摸屏 IC 都有此能力。
触摸点的信息通过一系列的 ABS_MT 事件(有的资料也叫消息)上报给 linux 内核,只有 ABS_MT 事件是用于多点触摸的,ABS_MT 事件定义在文件 include/uapi/linux/input.h 中
c
#define ABS_MT_SLOT 0x2f /* MT slot being modified */
#define ABS_MT_TOUCH_MAJOR 0x30 /* Major axis of touching ellipse */
#define ABS_MT_TOUCH_MINOR 0x31 /* Minor axis (omit if circular) */
#define ABS_MT_WIDTH_MAJOR 0x32 /* Major axis of approaching ellipse */
#define ABS_MT_WIDTH_MINOR 0x33 /* Minor axis (omit if circular) */
#define ABS_MT_ORIENTATION 0x34 /* Ellipse orientation */
#define ABS_MT_POSITION_X 0x35 /* Center X touch position */
#define ABS_MT_POSITION_Y 0x36 /* Center Y touch position */
#define ABS_MT_TOOL_TYPE 0x37 /* Type of touching device */
#define ABS_MT_BLOB_ID 0x38 /* Group a set of packets as a blob */
#define ABS_MT_TRACKING_ID 0x39 /* Unique ID of initiated contact */
#define ABS_MT_PRESSURE 0x3a /* Pressure on contact area */
#define ABS_MT_DISTANCE 0x3b /* Contact hover distance */
#define ABS_MT_TOOL_X 0x3c /* Center X tool position */
#define ABS_MT_TOOL_Y 0x3d /* Center Y tool position */
其 中 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 用 来 上 报 触 摸 点 的 (X,Y) 坐 标 信 息 ,ABS_MT_SLOT 用来上报触摸点 ID , 对 于 Type B 类型的设备,需要用到 ABS_MT_TRACKING_ID 事件来区分触摸点。
如果设备支持的话,还可以使用 ABS_MT_TOUCH_MAJOR 和 ABS_MT_WIDTH_MAJOR 这两个消息上报触摸面积信息,关于 其他 ABS_MT 事件的具体含义大家可以查看 Linux 内核中的 multi-touch-protocol.txt 文档。
电容屏触摸时序
Type A 触摸点信息上报时序
下面这个是发送两个触摸点的例子:
c
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
SYN_MT_REPORT
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_MT_REPORT
SYN_REPORT
Linux 内核里面也有 Type A 类型的多点触摸驱动,找到 st2332.c 这个驱动文件,具体实例如下:
c
static irqreturn_t st1232_ts_irq_handler(int irq, void *dev_id)
{
struct st1232_ts_data *ts = dev_id;
struct st1232_ts_finger *finger = ts->finger;
struct input_dev *input_dev = ts->input_dev;
int count = 0;
int i, ret;
ret = st1232_ts_read_data(ts);
if (ret < 0)
goto end;
/* multi touch protocol */
for (i = 0; i < MAX_FINGERS; i++) {
if (!finger[i].is_valid)
continue;
input_report_abs(input_dev, ABS_MT_TOUCH_MAJOR, finger[i].t);
input_report_abs(input_dev, ABS_MT_POSITION_X, finger[i].x);
input_report_abs(input_dev, ABS_MT_POSITION_Y, finger[i].y);
input_mt_sync(input_dev);
count++;
}
/* SYN_MT_REPORT only if no contact */
if (!count) {
input_mt_sync(input_dev);
if (ts->low_latency_req.dev) {
dev_pm_qos_remove_request(&ts->low_latency_req);
ts->low_latency_req.dev = NULL;
}
} else if (!ts->low_latency_req.dev) {
/* First contact, request 100 us latency. */
dev_pm_qos_add_ancestor_request(&ts->client->dev,
&ts->low_latency_req,
DEV_PM_QOS_RESUME_LATENCY, 100);
}
/* SYN_REPORT */
input_sync(input_dev);
end:
return IRQ_HANDLED;
}
其中input_mt_sync和input_sync对应着SYN_MT_REPORT和SYN_REPORT,每 上报完一个触摸点坐标,都要调用 input_mt_sync 函数上报一个 SYN_MT_REPORT 信息。 每上报完一轮触摸点信息就调用一次 input_sync 函数,也就是发送一个 SYN_REPORT 事件。
Type B 触摸点信息上报时序
下面这个是发送两个触摸点的例子:
c
ABS_MT_SLOT 0
ABS_MT_TRACKING_ID 45
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 46
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_REPORT
这里就以 ili210x 这个触摸驱动 IC 为例,看看是 Type B 类型 是 如 何 上 报 触 摸 点 坐 标 信 息 的 。 找 到 ili210x.c 这 个 驱 动 文 件,具体实力2机器人
c
static void ili210x_report_events(struct input_dev *input,
const struct touchdata *touchdata)
{
int i;
bool touch;
unsigned int x, y;
const struct finger *finger;
for (i = 0; i < MAX_TOUCHES; i++) {
input_mt_slot(input, i);
finger = &touchdata->finger[i];
touch = touchdata->status & (1 << i);
input_mt_report_slot_state(input, MT_TOOL_FINGER, touch);
if (touch) {
x = finger->x_low | (finger->x_high << 8);
y = finger->y_low | (finger->y_high << 8);
input_report_abs(input, ABS_MT_POSITION_X, x);
input_report_abs(input, ABS_MT_POSITION_Y, y);
}
}
input_mt_report_pointer_emulation(input, false);
input_sync(input);
}
调用 input_mt_slot 函数 上报 ABS_MT_SLOT 事 件 。 调用 input_mt_report_slot_state 函 数 上 报 ABS_MT_TRACKING_ID 事件,也就是给 SLOT 关联一个 ABS_MT_TRACKING_ID。使用 input_report_abs 函数上报触摸点对应的(X,Y)坐标值。 使用 input_sync 函数上报 SYN_REPORT 事件。
多点触摸所使用到的API函数
- input_mt_init_slots:用于初始化 MT 的输入 slots,编写 MT 驱动的时候必须先调用此函 数初始化 slots
- input_mt_slot:用于产生 ABS_MT_SLOT 事件,告诉内核当前上报的是哪个触摸点的坐标数据
- input_mt_report_slot_state:用于 Type B 类型,用于产生 ABS_MT_TRACKING_ID 和 ABS_MT_TOOL_TYPE 事 件 , ABS_MT_TRACKING_ID 事 件 给 slot 关 联 一 个 ABS_MT_TRACKING_ID , ABS_MT_TOOL_TYPE 事件指定触摸类型(是笔还是手指等)。
- input_report_abs:ype A 和 Type B 类型都使用此函数上报触摸点坐标信息,通过 ABS_MT_POSITION_X 和 ABS_MT_POSITION_Y 事件实现 X 和 Y 轴坐标信息上报
- input_mt_report_pointer_emulation:如果追踪到的触摸点数量多于当前上报的数量,驱动程序使用 BTN_TOOL_TAP 事件来通 知用户空间当前追踪到的触摸点总数量,然后调用 input_mt_report_pointer_emulation 函数将 use_count 参数设置为 false。否则的话将 use_count 参数设置为 true,表示当前的触摸点数量
驱动部分
驱动框图
编写驱动的框架如下:
①、多点电容触摸芯片的接口,一般都为 I2C 接口,因此驱动主框架肯定是 I2C。
②、linux 里面一般都是通过中断来上报触摸点坐标信息,因此需要用到中断框架。
③、多点电容触摸属于 input 子系统,因此还要用到 input 子系统框架。
④、在中断处理程序中按照 linux 的 MT 协议上报坐标信息。
具体见下图:
注意:中断没有使用 request_irq 函数申请中断,而是采用了 devm_request_threaded_irq 这个函数,并不是因为前者不能用,而是因为后者有更多的特点,比较适用于此场景。devm_request_threaded_irq具有如下特点:
- 用于申请中断,作用和 request_irq 函数类似。
- threaded的作用是中断线程化,硬件中断具有最高优先级,只要硬件中断发生,那么内核都会终止当前正在执行的操作,转而去执行中断处理程序。中断线程化以后任务的优先级可能比中断线程的优先级高,这样做的目的就是保证高优先级的任务能被优先处理。大家可能会疑问,不是说可以将比较耗时的中断放到下半部(bottom half) 处理吗?虽然下半部可以被延迟处理,但是依旧先于线程执行,中断线程化可以让这些比较耗时的下半部与进程进行公平竞争。
- 使用devm_前缀的函数申请到的资源可以由系统自动释放,不需要我们手动处理。
设备树节点修改
设备树节点的修改主要分为两个部分,一个是引脚的配置,一个是设备节点的配置
设备树引脚配置
如上图所示,该电容屏,主要用到四个引脚,其中两个是I2C的信号引脚,一个是触摸屏的中断引脚,还有一个是电容屏的复位引脚。
复位引脚:复位引脚用的是SNVS_TAMPER9,因此我们需要在&iomuxc_snvs下写入相关信息,具体如下:
c
&iomuxc_snvs {
pinctrl-names = "default_snvs";
pinctrl-0 = <&pinctrl_hog_2>;
imx6ul-evk {
pinctrl_tsc_reset: tsc_reset {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x10B0
>;
};
};
};
中断引脚:此处使用的是GPIO1_IO09,因此我们需要在节点&iomuxc下写入相关信息,具体如下:
c
&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
pinctrl_tsc: tscgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x79
>;
};
};
};
I2C引脚:此处使用的是I2C2,UART5_TX和UART5_RX,因此需要同中断引脚一样,在节点&iomuxc下写入相关信息,具体如下:
c
&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
imx6ul-evk {
pinctrl_i2c2: i2c2grp {
fsl,pins = <
MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0
MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0
>;
};
};
};
这里需要注意一个很容易犯的错误,我们在配置对应的引脚时一定要先看看这个引脚有没有被配置成其他的功能,如有有的话需要把其他的功能给屏蔽掉,不然会发生冲突。
设备节点配置
引脚配置好之后,需要我们配置相应的设备树节点信息,因为这个设备是挂载在I2C2上的,因此需要把对应的节点信息放在&i2c2上面,对应的注释我都写入代码里了,如下所示:
c
&i2c2 {
clock_frequency = <100000>; //时钟频率
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>; //I2C引脚
status = "okay"; //打开I2C2
gt9147:gt9147@14 { //名称@设备地址
compatible = "hbb,gt9147"; //熟悉值,用于匹配驱动
reg = <0x14>; //寄存器地址
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_tsc //另外两个引脚
&pinctrl_tsc_reset>;
interrupt-parent = <&gpio1>; //配置中断
interrupts = <9 0>; //配置中断IO序号,以及中断触发形式
reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>; //配置复位引脚,便于驱动直接从节点读取
interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>; //配置复位引脚,便于驱动直接从节点读取
status = "okay";
};
};
这里跟上面引脚定义一样,我们也需要避免出现冲突,防止同一个引脚被不同的驱动拿去使用,我们需要查看pinctrl-0、reset-gpios、interrupt-gpios也就是对应的四个引脚有没有被其他地方使用。
具体驱动开发
具体驱动开发本质上就是I2C,中断,input子系统的综合例程,=接下来我将分析如何搭建出一个框架
在驱动开发之前,需要注意以下两点:
- I2C框架中i2c_add_driver函数创建的驱动节点里面就包含了设备的相关信息,并且能实现I2C通信
- input子系统是在I2C总线上面开发的,之前开发ap3216设备时,我们直接用dev、class、device三步走的方式创建设备节点,而这次是需要开发一个input子系统的设备节点,因此我们需要借助input_register_device这个函数创建对应的设备节点。
I2C驱动框架
I2C驱动框架如下:
c
static const struct of_device_id gt9147_of_match[] = {
{.compatible = "hbb,gt9147"},
{ }
};
static const struct i2c_device_id gt9147_id[] = {
{"gt9147",0},/* 我们用的是of_device_id,i2c_device_id可以写的不对,但是必须要有*/
};
static struct i2c_driver gt9147_driver = {
.probe = gt9147_probe,
.remove = gt9147_remove,
.driver = {
.owner = THIS_MODULE,
.name = "gt9147",
.of_match_table = gt9147_of_match,
},
.id_table = gt9147_id,
};
static int __init gt9147_init(void)
{
return i2c_add_driver(>9147_driver);
}
static void __exit gt9147_exit(void)
{
i2c_del_driver(>9147_driver);
}
I2C框架内部实现
在I2C设备驱动节点创建好之后就可以进行I2C通信了,但是我们还需要input子系统和中断的配合,因此我们需要丰富一下.probe函数,下面是probe函数的具体内容,紧接着按照probe函数的顺序逐一讲解:
c
static int gt9147_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
u8 data;
printk("gt9147_probe!!!\r\n");
gt9147dev.client = client;
// 1,获取设备树中的中断和复位引脚
gt9147dev.irq_pin = of_get_named_gpio(client->dev.of_node, "interrupt-gpios", 0);
gt9147dev.reset_pin = of_get_named_gpio(client->dev.of_node, "reset-gpios", 0);
// 2,复位GT9147
ret = gt9147_ts_reset(client, >9147dev);
if(ret < 0) {
goto fail;
}
// 3,初始化GT9147
data = 0x02;
gt9147_write_regs(>9147dev, GT_CTRL_REG, &data, 1); //软复位
mdelay(100);
data = 0x0;
gt9147_write_regs(>9147dev, GT_CTRL_REG, &data, 1); //停止软复位
mdelay(100);
// 4,初始化GT9147,读取固件
ret = gt9147_read_firmware(client, >9147dev);
if(ret != 0) {
printk("Fail !!! check !!\r\n");
goto fail;
}
// 5,input设备注册
gt9147dev.input = devm_input_allocate_device(&client->dev);
if (!gt9147dev.input) {
ret = -ENOMEM;
goto fail;
}
gt9147dev.input->name = client->name;
gt9147dev.input->id.bustype = BUS_I2C;
gt9147dev.input->dev.parent = &client->dev;
__set_bit(EV_KEY, gt9147dev.input->evbit);
__set_bit(EV_ABS, gt9147dev.input->evbit);
__set_bit(BTN_TOUCH, gt9147dev.input->keybit);
input_set_abs_params(gt9147dev.input, ABS_X, 0, gt9147dev.max_x, 0, 0);
input_set_abs_params(gt9147dev.input, ABS_Y, 0, gt9147dev.max_y, 0, 0);
input_set_abs_params(gt9147dev.input, ABS_MT_POSITION_X,0, gt9147dev.max_x, 0, 0);
input_set_abs_params(gt9147dev.input, ABS_MT_POSITION_Y,0, gt9147dev.max_y, 0, 0);
ret = input_mt_init_slots(gt9147dev.input, MAX_SUPPORT_POINTS, 0);
if (ret) {
goto fail;
}
ret = input_register_device(gt9147dev.input);
if (ret)
goto fail;
// 6,最后初始化中断
ret = gt9147_ts_irq(client, >9147dev);
if(ret < 0) {
goto fail;
}
return 0;
fail:
return ret;
}
-
由于设备不是通电就能使用,还需要进对复位引脚进行读写操作才行,因此需要先获取设备对应的IO引脚号后执行函数gt9147_ts_reset来实现复位GT9147电容屏幕
-
接着利用i2c通信来初始化GT9147,这里用到了gt9147_write_regs 函数,后续读取数据会用到对应的gt9147_read_regs函数,重点注意这个地方的寄存器地址是16位的,因此需要把地址写成两个字节分两次传输。利用函数gt9147_read_firmware来读取固件信息,配置对应的像素点等信息。
-
devm_input_allocate_device是input设备注册,方便我们上传电容屏数据给用户端,同时也需要配置好与input有关的初始化配置,具体见probe函数如何配置的。
-
配置引脚中断,利用函数gt9147_ts_irq配置引脚中断,这里我们用的是devm_request_threaded_irq函数,为啥不用request_irq前面已经讲解了,最后只需要在中断中上报input的MT数据即可
上面所有的子函数以及中断处理函数等直接参考最后的参考文献链接地址即可,代码都放入了对应的github仓库里面,这里不在一一展示。
需要特别说明一下的是正点原子提供的驱动只能实现单点触摸,本实验在网上找到了多点触摸的实现方法,通过touch_index和id配合实现,具体多点触摸代码如下:
c
static irqreturn_t gt9147_irq_handler(int irq, void *dev_id)
{
int touch_num = 0;
int input_x, input_y;
int id = 0;
int ret = 0;
u8 data;
u8 touch_data[BUFFER_SIZE];
u16 touch_index = 0;
int pos = 0;
int report_num = 0;
int i;
static u16 last_index = 0;
struct gt9147_dev *dev = dev_id;
ret = gt9147_read_regs(dev, GT_GSTID_REG, &data, 1);
if (data == 0x00) { // 没有触摸数据,直接返回
goto fail;
} else { //统计触摸点数据
touch_num = data & 0x0f;
}
if(touch_num) { //有触摸按下
//读取具体的触摸寄存器
gt9147_read_regs(dev, GT_TP1_REG, touch_data, BUFFER_SIZE);
id = touch_data[0];
touch_index |= (0x01<<id);
for(i = 0;i < 5; i++){
if(touch_index |= (0x01 << i)){
input_x = touch_data[pos + 1] | (touch_data[pos + 2] << 8);
input_y = touch_data[pos + 3] | (touch_data[pos + 4] << 8);
input_mt_slot(dev->input, id); //产生ABS_MT_SLOT 事件 报告是哪个触摸点的坐标
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, true); // 指定手指触摸 连续触摸
input_report_abs(dev->input, ABS_MT_POSITION_X, input_x); // 上报触摸点坐标信息
input_report_abs(dev->input, ABS_MT_POSITION_Y, input_y); // 上报触摸点坐标信息
report_num++;
if(report_num < touch_num){
pos += 8;
id = touch_data[pos];
touch_index |= (0x01<<id);
}
}
else{
input_mt_slot(dev->input, i);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false); // 关闭手指触摸
}
}
} else if(last_index){ // 触摸释放
for(i = 0;i < 5; i++){
if(last_index & (0x01 << i)){
input_mt_slot(dev->input, i);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false);
}
}
}
last_index = touch_index;
input_mt_report_pointer_emulation(dev->input, true);
input_sync(dev->input);
data = 0x00; //向0X814E寄存器写0
gt9147_write_regs(dev, GT_GSTID_REG, &data, 1);
fail:
return IRQ_HANDLED;
}
下面这个是单点触摸实验代码,可以对比学习一下:
c
static irqreturn_t gt9147_irq_handler(int irq, void *dev_id)
{
int touch_num = 0;
int input_x, input_y;
int id = 0;
int ret = 0;
u8 data;
u8 touch_data[5];
struct gt9147_dev *dev = dev_id;
ret = gt9147_read_regs(dev, GT_GSTID_REG, &data, 1);
if (data == 0x00) { // 没有触摸数据,直接返回
goto fail;
} else { //统计触摸点数据
touch_num = data & 0x0f;
}
// 由于GT9147没有硬件检测每个触摸点按下和抬起,因此每个触摸点的抬起和按
// 下不好处理,尝试过一些方法,但是效果都不好,因此这里暂时使用单点触摸
if(touch_num) { //单点触摸按下
gt9147_read_regs(dev, GT_TP1_REG, touch_data, 5);
id = touch_data[0] & 0x0F;
if(id == 0) {
input_x = touch_data[1] | (touch_data[2] << 8);
input_y = touch_data[3] | (touch_data[4] << 8);
input_mt_slot(dev->input, id);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, true);
input_report_abs(dev->input, ABS_MT_POSITION_X, input_x);
input_report_abs(dev->input, ABS_MT_POSITION_Y, input_y);
}
} else if(touch_num == 0){ // 单点触摸释放
input_mt_slot(dev->input, id);
input_mt_report_slot_state(dev->input, MT_TOOL_FINGER, false);
}
input_mt_report_pointer_emulation(dev->input, true);
input_sync(dev->input);
data = 0x00; //向0X814E寄存器写0
gt9147_write_regs(dev, GT_GSTID_REG, &data, 1);
fail:
return IRQ_HANDLED;
}
参考文献
- 个人专栏系列文章
- 正点原子嵌入式驱动开发指南
- 对代码有兴趣的同学可以查看链接https://github.com/NUAATRY/imx6ull_dev