基于Android系统的多点触摸屏(MultiTouchScreen)驱动

  1. 云栖社区>
  2. 博客>
  3. 正文

基于Android系统的多点触摸屏(MultiTouchScreen)驱动

morixinguan 2017-10-20 15:27:00 浏览1599
展开阅读全文

理论:

输入子系统由来

  在Linux中, 应用层对于输入设备(鼠标、键盘、触摸屏等)的操作无非都是open、read、write、ioctl,然后调用驱动层的xxx_open、xxx_read、xxx_write、xxx_ioctl去操作具体的硬件输入设备。如果按照传统的思路,每个输入设备都按照这个套路写这些open、read等,是不是太过于累赘了。所以Linux就定义了一套标准,来标准化这些输入设备驱动,这个标准就叫做输入子系统。通过这个标准,写驱动的人不在重复的写xxx_open、xxx_read等通用代码,而只用完成各个输入设备不同的部分(注册不同的输入设备, 上报不同的输入事件,操作不同的硬件等)。

输入子系统框架

  在Linux中,输入子系统是由输入子系统设备驱动层、输入子系统核心层(Input Core)和输入子系统事件处理层(Event Handler)组成。其中设备驱动层提供对硬件各寄存器的读写访问和将底层硬件对用户输入访问的响应转换为标准的输入事件,再通过核心层提交给事件处理层;而核心层对下提供了设备驱动层的编程接口,对上又提供了事件处理层的编程接口;而事件处理层就为我们用户空间的应用程序提供了统一访问设备的接口和驱动层提交来的事件处理。所以这使得我们输入设备的驱动部分不在用关心对设备文件的操作,而是要关心对各硬件寄存器的操作和提交的输入事件。下面用图形来描述一下这三者的关系吧!(转) 
这里写图片描述

Linux输入子系统的结构体:

应用空间: 
 open(“/dev/input/eventx”)


内核空间: 
输入子系统设备驱动 
  向输入系统核心注册一个具体类型的输入设备,检测输入事件的发生,并上报输入事件到输入子系统事件处理层。 
输入子系统核心 
  提供各个输入设备的注册接口,设备驱动上报事件到事件处理层的桥梁。 
输入子系统事件处理层 
  接收设备驱动上报的输入事件,提供应用层访问设备统一的接口(open, read等),将事件通过接口传递到用户空间。


底层硬件: 
鼠标 键盘 触摸屏等


通过输入系统的这个框架发现,我们需要做的事情就是对于不同的输入设备,写出对应的设备驱动,并向输入系统核心注册这个输入设备当检测到有输入事件发生,将输入事件上报到输入子系统事件处理层就OK了。

具体操作:

首先,对触摸屏数据的采集是采用i2c接口或者spi接口,本例子以i2c接口为例,那么首先要熟悉i2c驱动的框架。

1. 注册一个i2c_driver结构体,去匹配i2c_device机构体(i2c_device可以通过添加board_info,设备树等方式创建,这里不做详细介绍)

/* 分配/设置i2c_driver */
static struct i2c_driver mts_driver = {
    .driver = {
        .name   = "my_mts",
        .owner  = THIS_MODULE,
    },
    .id_table   = mts_id_table,
    .probe      = mts_probe,
    .remove     = mts_remove,
};

static const struct i2c_device_id mts_id_table[] = {
    { "my_mts", 0 },
    {}
};

/* 注册i2c_driver */
i2c_add_driver(&mts_driver);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2. 在i2c的probe函数中分配、设置、注册一个input_dev结构体。

static int mts_probe(struct i2c_client *client, const struct i2c_device_id *id) {
ts_client = client;        //记录i2c_client

/* 2.1分配input_dev结构体 */
struct input_dev * ts_dev = input_allocate_device();

/* 2.2设置这个input_dev结构体 */
//设置这个输入设备能产生哪类事件   --  能产生同步事件EV_SYN  和  绝对坐标事件EV_ABS
set_bit(EV_SYN, ts_dev->evbit);
set_bit(EV_ABS, ts_dev->evbit);

//能产生这类事件中的哪些具体事件  -- X轴坐标ABS_MT_POSITION_X  Y轴坐标ABS_MT_POSITION_Y 和触点的ID ABS_MT_TRACKING_ID    
set_bit(ABS_MT_TRACKING_ID, ts_dev->absbit);      //对于多点触摸屏,每一个触点都有一个ABS_MT_TRACKING_ID
set_bit(ABS_MT_POSITION_X,  ts_dev->absbit);
set_bit(ABS_MT_POSITION_Y,  ts_dev->absbit);

//这些事件的范围
//支持多少个触点,具体支持多少个,由具体硬件手册决定
input_set_abs_params(ts_dev, ABS_MT_TRACKING_ID, 0, MTS_MAX_ID, 0, 0);

//X轴和Y轴的最大值,由硬件手册看触摸屏分辨率决定    
input_set_abs_params(ts_dev, ABS_MT_POSITION_X, 0, MTS_MAX_X, 0, 0);      
input_set_abs_params(ts_dev, ABS_MT_POSITION_Y, 0, MTS_MAX_Y, 0, 0);

//Android系统会根据这个name来找相应的配置文件,如果仅仅是Linux驱动则可有可无
//#define  FT5X0X_NAME   ft5x0x_ts
ts_dev->name = FT5X0X_NAME; 

/* 2.3注册这个输入设备 */
input_register_device(ts_dev);
  • 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

3. 硬件相关的操作:

//初始化一个工作队列,中断下半部使用
INIT_WORK(&mts_work, mts_work_func);

//注册触摸屏对应的外部中断,中断号可以通过gpio_to_irq获取,如果是3.x以后的内核需要通过设备树获取
request_irq(irq, mts_interrupt, IRQ_TYPE_EDGE_FALLING, "my_mts", NULL);
  • 1
  • 2
  • 3
  • 4
  • 5

4. 实现中断处理函数,并使用中断的底半部机制去处理中断(上报ABS事件)

//中断处理函数
irqreturn_t mts_interrupt(int irqno, void * dev_id)
{
    /* 通过i2c读取中断数据即坐标
     * 但是i2c是低速设备,所以用中断下半部处理
     * */
    schedule_work(&mts_work);   //调度中断下半部

    return IRQ_HANDLED;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
//中断的底半部,来读取中断数据(坐标)
static void mts_work_func(void * t)
{
    int i;
    int ret;
    /* 读取i2c设备,获得触点数据,并上报 */
    /* 读数据 */
    ret = mts_read_data();
    if (ret < 0)
        return;

    /* 如果没有触点只上报同步事件 */
    if (!ts.ts_points) {
        input_mt_sync(ts.ts_dev);
        input_sync(ts.ts_dev);
        return ;
    }

    for (i = 0; i < ts_points; i++){   /* 将每一个触点进行上报 */
        input_report_abs(ts_dev, ABS_MT_POSITION_X, ts_events[i].x);     //第i个触点的x坐标
        input_report_abs(ts_dev, ABS_MT_POSITION_Y, ts_events[i].y);     //第i个触点的y坐标
        input_report_abs(ts_dev, ABS_MT_TRACKING_ID, ts_events[i].id);   //第i个触点的ID

        /* 表示一个触点上报完成 */
        input_mt_sync(ts_dev);
    }
    /* 表示某一时刻一次上报事件结束 */
    input_sync(ts_dev);
}
  • 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
//通过i2c读取中断数据
static int mts_read_data(void) {
    u8 buf[32] = { 0 };
    int ret;

    ret = i2c_read_data(ts_client, buf, 31);
    if (ret < 0) {
        return ret;
    }

    ts_points= buf[2] & (0xf);       //根据驱动IC手册,读取触点的个数

    if (!ts_points) {                //如果触点个数为0,则只上报同步事件
        input_mt_sync(ts_dev);
        input_sync(ts_dev);
        return 1;
    }

        //读取每一个触点的坐标,id,并保存到ts_event中,具体如何获取buf中数据的格式,参考具体datasheet
    switch (ts_points) {
        case 5:
            ts_events[4].x = (s16)(buf[0x1b] & 0x0F)<<8 | (s16)buf[0x1c];
            ts_events[4].y = (s16)(buf[0x1d] & 0x0F)<<8 | (s16)buf[0x1e];
            ts_events[4].id= (s16)(buf[0x1d] >> 4);
        case 4:
            ts_events[3].x = (s16)(buf[0x15] & 0x0F)<<8 | (s16)buf[0x16];
            ts_events[3].y = (s16)(buf[0x17] & 0x0F)<<8 | (s16)buf[0x18];
            ts_events[3].id= (s16)(buf[0x17] >> 4);
        case 3:
            ts_events[2].x = (s16)(buf[0x0f] & 0x0F)<<8 | (s16)buf[0x10];
            ts_events[2].y = (s16)(buf[0x11] & 0x0F)<<8 | (s16)buf[0x12];
            ts_events[2].id= (s16)(buf[0x11] >> 4);
        case 2:
            ts_events[1].x = (s16)(buf[0x09] & 0x0F)<<8 | (s16)buf[0x0a];
            ts_events[1].y = (s16)(buf[0x0b] & 0x0F)<<8 | (s16)buf[0x0c];
            ts_events[1].id= (s16)(buf[0x0b] >> 4);
        case 1:
            ts_events[0].x = (s16)(buf[0x03] & 0x0F)<<8 | (s16)buf[0x04];
            ts_events[0].y = (s16)(buf[0x05] & 0x0F)<<8 | (s16)buf[0x06];
            ts_events[0].id= (s16)(buf[0x05] >> 4);
            break;
        case 0:   //没有触点
            return 0;
        default:
            return -1;
    }

    return 0;
}

//保存数据的结构体
struct tiny4412_ts_event {
    int x;
    int y;
    int id;
};
struct tiny4412_ts_event ts_events[16];
  • 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
//i2c读取数据
static int i2c_read_data(struct i2c_client * client, char *rxdata, int length) {
    int ret;
    struct i2c_msg msgs[] = {
        {
            .addr   = client->addr,
            .flags  = 0,
            .len    = 1,
            .buf    = rxdata,
        },
        {
            .addr   = client->addr,
            .flags  = I2C_M_RD,
            .len    = length,
            .buf    = rxdata,
        },
    };

    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret < 0)
        pr_err("%s: i2c read error: %d\n", __func__, ret);

    return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

实验:

  1. 为Android创建配置文件(idc文件),配置文件的名字和注册的input_dev的name一致 比如ft5x0x_ts.idc并放入Android文件系统的/system/usr/idc/,里面的内容为:touch.deviceType = touchScreen //表示是触摸屏设备,参考官方文档https://source.android.com/devices/input/touch-devices
  2. 将my_mts.c放入linux-3.0.86/drivers/input/touchscreen/目录,然后修改Makefile,将原有的触摸屏驱动去掉,把新的编译进去,最后make生成zImage
  3. 烧写新的zImage,重启设备,触摸屏幕,OK。

关于触摸屏的配置文件/system/usr/idc/ft5x0x_ts.idc

Android官方文档: 
https://source.android.com/devices/input/touch-devices 
touch.deviceType: 
Definition: touch.deviceType = touchScreen | touchPad | pointer | default

触摸设备的类型:

touchScreen : 触摸屏, 覆盖在显示器上, 可以直接操作各种图标
touchPad    : 触摸板, 不是覆盖在显示器上, 需要在LCD上显示一个光标以便定位
pointer     : 跟touchPad类似, 多一些手势功能("Indirect Multi-touch Pointer Gestures")
default     : 由系统自己确定
  • 1
  • 2
  • 3
  • 4

那么是否可以通过分析Android源码,找到不使用这个idc文件的方法呢?

Android源码中搜索 touch.deviceType

InputReader.cpp (frameworks\native\services\inputflinger)   
//读取idc文件,获取touch.deviceType属性的值
if (getDevice()->getConfiguration().tryGetProperty(String8("touch.deviceType"), deviceTypeString)) {
    if (deviceTypeString == "touchScreen") {                              //如果是触摸屏,则将deviceType赋值为DEVICE_TYPE_TOUCH_SCREEN        
        mParameters.deviceType = Parameters::DEVICE_TYPE_TOUCH_SCREEN;
    } else if (deviceTypeString == "touchPad") {
        mParameters.deviceType = Parameters::DEVICE_TYPE_TOUCH_PAD;
    } else if (deviceTypeString == "touchNavigation") {
        mParameters.deviceType = Parameters::DEVICE_TYPE_TOUCH_NAVIGATION;
    } else if (deviceTypeString == "pointer") {
        mParameters.deviceType = Parameters::DEVICE_TYPE_POINTER;
    } else if (deviceTypeString != "default") {
        ALOGW("Invalid value for touch.deviceType: '%s'", deviceTypeString.string());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

经过分析发现,在文件的这个地方通过getEventHub获取设备的属性,根据设备属性同样也会更改deviceType的值。

if (getEventHub()->hasInputProperty(getDeviceId(), INPUT_PROP_DIRECT)) {      //判断是否有INPUT_PROP_DIRECT这个属性值
    // The device is a touch screen.
    mParameters.deviceType = Parameters::DEVICE_TYPE_TOUCH_SCREEN;
} else if (getEventHub()->hasInputProperty(getDeviceId(), INPUT_PROP_POINTER)) {
    // The device is a pointing device like a track pad.
    mParameters.deviceType = Parameters::DEVICE_TYPE_POINTER;
} else if (getEventHub()->hasRelativeAxis(getDeviceId(), REL_X)
            || getEventHub()->hasRelativeAxis(getDeviceId(), REL_Y)) {
    // The device is a cursor device with a touch pad attached.
    // By default don't use the touch pad to move the pointer.
    mParameters.deviceType = Parameters::DEVICE_TYPE_TOUCH_PAD;
} else {
    // The device is a touch pad of unknown purpose.
    mParameters.deviceType = Parameters::DEVICE_TYPE_POINTER;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在EventHub中去获取这个属性值。

EventHub.cpp (frameworks\native\services\inputflinger)  
bool EventHub::hasInputProperty(int32_t deviceId, int property) const {
    if (property >= 0 && property <= INPUT_PROP_MAX) {
        AutoMutex _l(mLock);

        Device* device = getDeviceLocked(deviceId);
        if (device) {
            return test_bit(property, device->propBitmask);
        }
    }
    return false;
}

ioctl(fd, EVIOCGPROP(sizeof(device->propBitmask)), device->propBitmask);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

去Linux内核中搜索EVIOCGPROP:

Evdev.c (drivers\input) 24713   2017/2/21
case EVIOCGPROP(0):
    return bits_to_user(dev->propbit, INPUT_PROP_MAX, size, p, compat_mode);
发现我们需要修改设备的dev->propbit位,这个位有以下选项:我们需要设置为INPUT_PROP_DIRECT
#define INPUT_PROP_POINTER      0x00    /* needs a pointer */
#define INPUT_PROP_DIRECT       0x01    /* direct input devices */
#define INPUT_PROP_BUTTONPAD    0x02    /* has button(s) under pad */
#define INPUT_PROP_SEMI_MT      0x03    /* touch rectangle only */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

继续搜索,看其他驱动是如何设置这个bit的: 
set_bit(INPUT_PROP_DIRECT, input_dev->propbit); 
所以修改我们的驱动,在设置input_dev结构体的时候,加上这个属性的设置即可。

总结:

只要我们的触摸屏输入设备通过set_bit设置propbit属性值为INPUT_PROP_DIRECT,当没有.idc配置文件时,那么我们的EventHub.cpp就会通过ioctl调用Linux中evdev.c去获取这个属性的值,进行判断,最终给InputReader.cpp中的deviceType赋值为DEVICE_TYPE_TOUCH_SCREEN,从而识别我们的设备,达到了不用创建idc配置文件的目的。

网友评论

登录后评论
0/500
评论
morixinguan
+ 关注