Platform: RK3399

OS: Android 7.1

Kernel: v4.4.103

Board: Firefly-AIO-3399C

TSC2007 的 Datasheet 给出了它的一个典型应用电路:

android 摄像头页面_触摸屏

我们自己打了几块调试的小板,大概如下图这样:

android 摄像头页面_RK3399_02

然后是接到开发板上准备调试 :

android 摄像头页面_RK3399_03

android 摄像头页面_设备驱动_04

一、内核驱动

tsc2007的驱动源码在内核中是已经存在的,其路径如下:

kernel/drivers/input/touchscreen/tsc2007.c

所以,我们只需要在kernel目录下,通过 make menuconfig 命令来配置内核并重新编译即可;或者,在调试阶段,也可以用一种简单粗暴的方式——直接改Makefile(kernel/drivers/input/touchscreen/Makefile)

#
# Makefile for the touchscreen drivers.
#
...
#obj-$(CONFIG_TOUCHSCREEN_TSC2007)	+= tsc2007.o
obj-y	+= tsc2007.o
...

这里可以简单看一下 tsc2007 的设备结构体在驱动文件中的定义:

struct tsc2007 {
	struct input_dev	*input;
	char			phys[32];

	struct i2c_client	*client;

	u16			model;
	u16			x_plate_ohms;
	u16			max_rt;
	unsigned long		poll_period; /* in jiffies */
	int			fuzzx;
	int			fuzzy;
	int			fuzzz;

	unsigned		gpio;
	int			irq;

	wait_queue_head_t	wait;
	bool			stopped;

	int			(*get_pendown_state)(struct device *);
	void			(*clear_penirq)(void);
};

二、设备树

内核的配置搞定了,接下来就是修改设备树文件,来把相关的硬件信息传给驱动。之前在Firefly的开发板上接了一LVDS接口的LCD屏幕,使用的是以下的dts:

kernel/arch/arm64/boot/dts/rockchip/rk3399-firefly-aioc-lvds-HSX101H40C.dts

在此基础上做修改。

那么,tsc2007的设备树节点应该怎么写呢?

首先,去看bindings。

打开 kernel/Documentation/devicetree/bindings/input/touchscreen/tsc2007.txt,可看到它给出了一个example:

Example:
	&i2c1 {
		/* ... */
		tsc2007@49 {
			compatible = "ti,tsc2007";
			reg = <0x49>;
			interrupt-parent = <&gpio4>;
			interrupts = <0x0 0x8>;
			gpios = <&gpio4 0 0>;
			ti,x-plate-ohms = <180>;
		};

		/* ... */
	};

对设备节点中各个属性的解释如下:

Required properties:

  • compatible: must be “ti,tsc2007”.
  • reg: I2C address of the chip.
  • ti,x-plate-ohms: X-plate resistance in ohms.

Optional properties:

  • gpios: the interrupt gpio the chip is connected to (trough the penirq pin).
    The penirq pin goes to low when the panel is touched. (see GPIO binding[1] for more details).
  • interrupt-parent: the phandle for the gpio controller (see interrupt binding[0]).
  • interrupts: (gpio) interrupt to which the chip is connected (see interrupt binding[0]).
  • ti,max-rt: maximum pressure.
  • ti,fuzzx: specifies the absolute input fuzz x value. If set, it will permit noise in the data up to ± the value given to the fuzz parameter, that is used to filter noise from the event stream.
  • ti,fuzzy: specifies the absolute input fuzz y value.
  • ti,fuzzz: specifies the absolute input fuzz z value.
  • ti,poll-period: how much time to wait (in milliseconds) before reading again the values from the tsc2007.

– [0]: Documentation/devicetree/bindings/interrupt-controller/interrupts.txt
–[1]: Documentation/devicetree/bindings/gpio/gpio.txt

这里,列举几个主要的:

  1. I2C从设备地址 —— reg
    tsc2007的7位设备地址是由A0, A1两个引脚的连接方式确定的,在 Datasheet 有给出说明。

7位地址中,D7~D5为高3位,所以地址高位为100,即为4。

由于我把A1, A0两个引脚接地,所以 D4~D1为1000,即为8。

因此,这7位地址应该是0x48:

reg = <0x48>;
  1. 中断引脚 —— gpios
    tsc2007的中断引脚(PENIRQ)在触摸屏被触摸时会拉至低电平,以此来产生一个中断信号给主机,这个引脚我把它接到了AIO-3399C引出来的一个GPIO——GPIO2_B4上,这个GPIO的位置,可以参看这里:http://wiki.t-firefly.com/AIO-3399C/driver_gpio.html 。
gpios = <&gpio2 12 GPIO_ACTIVE_LOW>;
  1. 中断控制器 —— interrupt-parent
    指定中断引脚对应的中断控制器。在设备树中可以作为 interrupt-parent 的节点,都带有 interrupt-controller这个属性,由于我们使用GPIO2_B4这个IO,所以这个 interrupt-parent 设为 gpio2。
interrupt-parent = <&gpio2>;
  1. 中断号及其触发方式 —— interrupts
    我们在 gpio2 下申请一个中断,interrupts这个属性有2个cell:第一个cell就是引脚号,这里GPIO_B4的number就是12;第二个cell,用来表示中断的触发方式,可以使用诸如 IRQ_TYPE_LEVEL_LOW,IRQ_TYPE_EDGE_FALLING,IRQ_TYPE_EDGE_BOTH 这样的宏来表示。这里我们设置为下降沿触发。
interrupts = <12 IRQ_TYPE_EDGE_FALLING>;

在AIO-3399C 开发板上,引出了 I2C4 这个I2C接口,这个接口也是开发板原本的电容触摸屏(gslx680)的接口,AIO-3399C 开发板的I2C接口可参看这里:http://wiki.t-firefly.com/AIO-3399C/driver_i2c.html 。现在我们在 I2C4 这个接口下使用tsc2007,最终,dts文件的修改如下:

...
&i2c4 {
        status = "okay";
		
        gslx680: gslx680@41 {                         
                compatible = "gslX680";
                reg = <0x41>;
                screen_max_x = <800>;
                screen_max_y = <1280>;
                touch-gpio = <&gpio4 28 IRQ_TYPE_LEVEL_LOW>;
                reset-gpio = <&gpio4 21 GPIO_ACTIVE_HIGH>;
                flip-x = <1>;
                flip-y = <0>;
                swap-xy = <0>;
                gsl,fw = <1>;
				status = "disabled";
        };
        
        // tsc2007 - Yumin
        tsc2007@48 {
			compatible = "ti,tsc2007";
			reg = <0x48>;
			interrupt-parent = <&gpio2>;
			interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
			gpios = <&gpio2 12 GPIO_ACTIVE_LOW>;   //GPIO2_B4
			ti,x-plate-ohms = <180>;
            status = "okay";
		};
};
...

三、重新编译内核

做完上述的工作之后,应该就可以重新编译内核了,但是这里有一点需要说明一下,就是需要把RK的 camsys 驱动从内核中去掉,具体是修改 kernel/drivers/media/Makefile,把对应的行注释掉。

...
	# Do not compile camsys - Yumin
	# obj-$(CONFIG_CAMSYS_DRV) += video/rk_camsys/
	...

这么做的原因是:之前在调试一个电池模块的充电插入检测的时候,有使用 GPIO2_B4 这个IO口,结果发现 Android 系统需要延迟几分钟才广播充电状态,并且,在使用USB摄像头热拔插的时候,会出现系统崩溃。最终排查的结果是和 camsys 这个驱动有关系,于是把它从内核中直接去掉,终于可以正常使用。所以在这里为了避免踩坑,就把它先去掉吧。
好了,重新编译内核吧,在开发板的SDK根目录下执行相关的命令即可:

$ . ./build/envsetup.sh
$ lunch
$ ./FFTools/make.sh -k -j32

再把所有的镜像打包到 rockdev/Image-rk3399_firefly_aioc_lvds_box 目录去:

$ ./mkimage.sh

按理说,完成了上面工作之后,应该就可以正常使用触摸屏了。但是,当我把镜像烧录到机器中,启动之后,发现我在触摸屏上滑动手指,屏幕上有鼠标的光标跟着移动,或者,可以这样表述:“触摸屏操作类似鼠标一样”,“触摸屏操作类似触摸板”。百思不得其解,在网上查找了很多资料,都没有得到正确的答案,最早的甚至能看到2013年的帖子(居然没有答案!):
后来最终在android的官网上找到了答案。

四、Android 输入设备配置文件

对于 输入设备配置文件(input device configuration files),安卓的官方文档为:https://source.android.com/devices/input/input-device-configuration-files

这里摘抄一段吧:

输入设备配置文件(.idc 文件)包含特定设备的配置属性,这些属性会影响输入设备的行为。

输入设备配置文件通常并非标准外围设备(例如 HID 键盘和鼠标)所必需的,因为默认的系统行为通常可确保它们开箱即用。另一方面,内置的嵌入式设备(尤其是触摸屏)几乎总是需要输入设备配置文件来指定其行为。

基本原理

根据关联的 Linux 内核输入设备驱动程序报告的事件类型和属性,Android 会自动检测和配置大多数输入设备功能。

例如,如果输入设备支持 EV_REL 事件类型、代码 REL_XREL_Y 以及 EV_KEY 事件类型和 BTN_MOUSE,则 Android 会将输入设备归类为鼠标。鼠标的默认行为是在屏幕上显示光标,光标跟踪鼠标的移动并在鼠标被点击时模拟触摸操作。虽然可以通过不同的方式配置鼠标,但是默认行为通常足以用于标准的鼠标外围设备。

某些类别的输入设备更加模糊。例如,多点触摸屏和触摸板都至少支持 EV_ABS 事件类型以及代码 ABS_MT_POSITION_XABS_MT_POSITION_Y。不过,这些设备的主要用途千差万别,无法总能自动确定。此外,还需要其他信息才能了解触摸设备报告的压力和大小信息。因此,触摸设备(尤其是内置触摸屏)通常都需要 IDC 文件

位置

输入设备配置文件由 USB 供应商、产品(及可选版本)ID 或输入设备名称定位。

按顺序查阅以下路径。

  • /odm/usr/idc/Vendor_XXXX_Product_XXXX_Version_XXXX.idc
  • /vendor/usr/idc/Vendor_XXXX_Product_XXXX_Version_XXXX.idc
  • /system/usr/idc/Vendor_XXXX_Product_XXXX_Version_XXXX.idc
  • /data/system/devices/idc/Vendor_XXXX_Product_XXXX_Version_XXXX.idc
  • /odm/usr/idc/Vendor_XXXX_Product_XXXX.idc
  • /vendor/usr/idc/Vendor_XXXX_Product_XXXX.idc
  • /system/usr/idc/Vendor_XXXX_Product_XXXX.idc
  • /data/system/devices/idc/Vendor_XXXX_Product_XXXX.idc
  • /odm/usr/idc/DEVICE_NAME.idc
  • /vendor/usr/idc/DEVICE_NAME.idc
  • /system/usr/idc/DEVICE_NAME.idc
  • /data/system/devices/idc/DEVICE_NAME.idc

当构建包含设备名称的文件路径时,设备名称中除“0-9”、“a-z”、“A-Z”、“-”或“_" 之外的所有字符将替换为“_”。

“触摸设备(尤其是内置触摸屏)通常都需要 IDC 文件”,嗯,这句话说得好!

这里我打算在android文件系统以 /system/usr/idc/DEVICE_NAME.idc 这种形式来作为 TSC2007 的 IDC 文件。

那么这个DEVICE_NAME又是什么呢?回过头看一看我们的tsc2007.c这个驱动文件吧:

static int tsc2007_probe(struct i2c_client *client,
			 const struct i2c_device_id *id)
{
...
	input_dev->name = "TSC2007 Touchscreen";//输入设备名称
	input_dev->phys = ts->phys;
	input_dev->id.bustype = BUS_I2C;

	input_dev->open = tsc2007_open;
	input_dev->close = tsc2007_close;
...
	err = input_register_device(input_dev);	//注册输入设备
	if (err) {
		dev_err(&client->dev,
			"Failed to register input device: %d\n", err);
		return err;
	}
...
}

可见在probe的过程中,向输入子系统注册输入设备的时候,设备名称为 “TSC2007 Touchscreen” ,所以最终我们的IDC在android文件系统下应该是:/system/usr/idc/TSC2007_Touchscreen.idc

1. 我们应该在源码的哪里创建这个IDC文件,并拷贝到设备的文件系统呢?

如果你对安卓系统的编译过程有所了解的话,应该知道,通常一个厂商的某个具体产品的makefile,都会放在device目录下,并且被命名为 xxxx.mk,它利用编译系统中已有的全局变量、函数等来完成所需要的功能,如:编译结束后,复制指定文件到设备系统中、设置系统属性等。

于是找到device/rockchip/rk3399/rk3399_firefly_aioc_lvds_box.mk,在这个 makefile 中,可以通过指定 PRODUCT_COPY_FILES 这个变量来实现在编译过程中拷贝文件到设备系统。可以参考这篇文章:

rk3399_firefly_aioc_lvds_box.mk 如下:

$(call inherit-product, $(LOCAL_PATH)/rk3399.mk)
$(call inherit-product, device/rockchip/common/tv/tv_base.mk)

PRODUCT_CHARACTERISTICS := box

PRODUCT_BRAND := rockchip
PRODUCT_MANUFACTURER := rockchip
PRODUCT_NAME := rk3399_firefly_aioc_lvds_box
PRODUCT_DEVICE := rk3399_firefly_aioc_lvds_box
PRODUCT_MODEL := AIO-3399C

PRODUCT_FIREFLY_NAME := LVDS

PRODUCT_AAPT_CONFIG := normal tvdpi hdpi
PRODUCT_AAPT_PREF_CONFIG := tvdpi

# enable mtp default
BOARD_USB_ALLOW_DEFAULT_MTP := true

# debug-logs
ifneq ($(TARGET_BUILD_VARIANT),user)
MIXIN_DEBUG_LOGS := true
endif

#for drm widevine
BUILD_WITH_WIDEVINE := true

看到 device/rockchip/rk3399/rk3399_firefly_aioc_lvds_box/ 这个目录,那么就在这里创建TSC2007_Touchscreen.idc 吧!

因此在 rk3399_firefly_aioc_lvds_box.mk 的末尾加上 PRODUCT_COPY_FILES 的配置:

PRODUCT_COPY_FILES += \      device/rockchip/rk3399/rk3399_firefly_aioc_lvds_box/TSC2007_Touchscreen.idc:system/usr/idc/TSC2007_Touchscreen.idc

完成这个修改之后,则在下次编译系统的时候会先把 device/rockchip/rk3399/rk3399_firefly_aioc_lvds_box中的TSC2007_Touchscreen.idc 复制到 out/target/product/rk3399_firefly_aioc_lvds_box/system/usr/idc/ 目录中,然后再从这里打包到 system.img 镜像。

2. 完成TSC2007_Touchscreen.idc

接下来,就是把 TSC2007_Touchscreen.idc 写出来了。在网上找了一些触摸屏的IDC文件的写法,直接copy了一个过来用,如下:

# Basic Parameters
touch.deviceType = touchScreen
#touch.orientationAware = 1

# Touch Size
touch.touchSize.calibration = default

# Tool Size
touch.toolSize.calibration = default

# Pressure
touch.pressure.calibration = default

# Size
touch.size.calibration = default

# Orientation
touch.orientation.calibration = none

这里最重要的当然应该是这一句啦:

touch.deviceType = touchScreen

当然,Android的官方文档里,也有对于触摸设备的详细的说明:https://source.android.com/devices/input/touch-devices

好了,到这里总算把所有的东西都搞定了,前面说过,配置完 rk3399_firefly_aioc_lvds_box.mk 之后只有在下一次编译的时候,才会在 system.img 中放入 TSC2007_Touchscreen.idc 文件,可是,我不想等上几分钟的编译过程,怎么办呢?

很简单,直接手动复制 TSC2007_Touchscreen.idc 到 out/target/product/rk3399_firefly_aioc_lvds_box/system/usr/idc/ 目录中,然后回到SDK的根目录,执行打包命令打包所有镜像:

$ ./mkimage.sh

或者可以直接打包 system.img (在 out/target/product/rk3399_firefly_aioc_lvds_box/ 下生成 system.img ):

$ make snod

把新的 system.img 烧录到机器上,重启,再试一下在触摸屏上滑动手指,nice,终于不再出现鼠标光标了!