20、USB触摸屏与数字化仪:HID Touch协议
触摸屏这东西,大家每天都在用。但你真的了解它在Android系统里是怎么工作的吗?
我记得刚入行那会儿,接到一个外接USB触摸屏的适配任务。当时觉得不就是个触摸屏嘛,插上就能用。结果折腾了整整一周,才把坐标映射搞清楚。今天我就把这些经验掰开揉碎了讲给你听。
20.1 HID Touch协议:多点触控报告描述符
USB触摸屏本质上是一个HID设备。它通过报告描述符告诉主机:「我能干什么,我的数据长什么样」。
说白了,报告描述符就是一份数据格式说明书。主机读懂了它,才能正确解析触摸屏发来的数据包。
20.1.1 多点触控报告描述符结构
一个标准的HID Touch报告描述符,通常包含以下关键元素:
- Usage Page (0x0D):声明这是Digitizer(数字化仪)设备
- Usage (Touch Screen):具体是触摸屏
- Collection (Application):应用集合,表示一个完整的触摸设备
- Report ID:报告ID,用于区分不同数据包
- Finger Count:最大触点数
- X/Y坐标:每个触摸点的位置数据
- Tip Switch:触摸状态(按下/抬起)
- Contact ID:触摸点ID,用于跟踪多点
来看一个实际的多点触控报告描述符示例:
// 5点触控HID报告描述符(简化版)
0x05, 0x0D, // Usage Page (Digitizer)
0x09, 0x04, // Usage (Touch Screen)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x09, 0x55, // Usage (Contact Count Maximum)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x05, // Logical Maximum (5)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0xB1, 0x02, // Feature (Data,Var,Abs)
// 每个触摸点的数据
0x09, 0x54, // Usage (Contact Count)
0x81, 0x02, // Input (Data,Var,Abs)
// 5个触摸点,每个点包含:
// Tip Switch (1 bit) + Contact ID (7 bits) + X (16 bits) + Y (16 bits)
0x09, 0x32, // Usage (In Range)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x05, // Report Count (5)
0x81, 0x02, // Input (Data,Var,Abs)
0x09, 0x51, // Usage (Contact Identifier)
0x75, 0x07, // Report Size (7)
0x95, 0x05, // Report Count (5)
0x81, 0x02, // Input (Data,Var,Abs)
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x05, // Report Count (5)
0x81, 0x02, // Input (Data,Var,Abs)
0x09, 0x31, // Usage (Y)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x05, // Report Count (5)
0x81, 0x02, // Input (Data,Var,Abs)
0xC0 // End Collection
这段描述符定义了5点触控。每个触摸点包含:1位触摸状态、7位ID、16位X坐标、16位Y坐标。总共5个点,数据包大小是 (1+7+16+16)*5 = 200位 = 25字节。
20.2 Android端USB触摸屏驱动适配
Android系统对USB触摸屏的支持,主要依赖两个层面:
- 内核驱动层:USB HID驱动 + 触摸屏驱动
- 用户空间层:InputReader + InputDispatcher
20.2.1 内核驱动适配
Android内核默认支持HID触摸屏。但有些设备需要额外配置:
// 内核配置选项
CONFIG_HID_MULTITOUCH=y
CONFIG_HIDRAW=y
CONFIG_USB_HID=y
CONFIG_INPUT_TOUCHSCREEN=y
如果触摸屏是I2C接口的,还需要配置对应的I2C驱动:
// 设备树配置示例(i2c触摸屏)
&i2c3 {
touchscreen@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&gpio1>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
touchscreen-size-x = <1024>;
touchscreen-size-y = <600>;
touchscreen-inverted-x;
touchscreen-swapped-x-y;
};
};
嗯,这里要注意。设备树里的 touchscreen-size-x 和 touchscreen-size-y 必须和触摸屏硬件分辨率一致。我曾经见过有人把这两个值写反了,结果触摸点满屏乱飞。
20.2.2 用户空间适配
内核驱动上报的是原始坐标。Android的InputReader会读取这些坐标,然后进行转换。
关键文件在 frameworks/native/services/inputflinger/ 目录下:
- InputReader.cpp:读取输入事件
- TouchInputMapper.cpp:触摸事件映射
- MultiTouchInputMapper.cpp:多点触控映射
Android通过 getevent 命令可以查看原始事件:
# 查看触摸事件
adb shell getevent -lt /dev/input/event2
# 输出示例
[ 12345.678901] EV_ABS ABS_MT_POSITION_X 00000320
[ 12345.678901] EV_ABS ABS_MT_POSITION_Y 00000258
[ 12345.678901] EV_ABS ABS_MT_TRACKING_ID 00000001
[ 12345.678901] EV_SYN SYN_REPORT 00000000
20.3 校准与坐标映射
触摸屏的坐标映射,是让触摸点准确对应到屏幕显示位置的关键。
20.3.1 坐标映射原理
触摸屏的原始坐标范围,和屏幕的显示分辨率通常不一样。需要做线性映射:
// 坐标映射公式
display_x = (touch_x - touch_min_x) * (display_width - 1) / (touch_max_x - touch_min_x)
display_y = (touch_y - touch_min_y) * (display_height - 1) / (touch_max_y - touch_min_y)
举个例子:触摸屏分辨率是4096x4096,屏幕分辨率是1080x1920。触摸点在(2048, 2048),映射到屏幕就是:
display_x = (2048 - 0) * 1079 / 4095 ≈ 540
display_y = (2048 - 0) * 1919 / 4095 ≈ 960
20.3.2 校准方法
Android系统支持两种校准方式:
| 校准方式 | 原理 | 适用场景 |
|---|---|---|
| 线性校准 | 通过4个角点计算映射矩阵 | 大多数电阻屏、部分电容屏 |
| 非线性校准 | 多点采样,插值计算 | 曲面屏、大尺寸触摸屏 |
线性校准的代码实现:
// 校准矩阵计算
// 已知4个校准点的屏幕坐标和触摸坐标
struct CalPoint {
int screen_x, screen_y;
int touch_x, touch_y;
};
CalPoint points[4] = {
{0, 0, 100, 100}, // 左上角
{1079, 0, 3900, 100}, // 右上角
{0, 1919, 100, 3800}, // 左下角
{1079, 1919, 3900, 3800} // 右下角
};
// 计算缩放因子
float scale_x = (float)(points[1].screen_x - points[0].screen_x) /
(float)(points[1].touch_x - points[0].touch_x);
float scale_y = (float)(points[2].screen_y - points[0].screen_y) /
(float)(points[2].touch_y - points[0].touch_y);
// 应用校准
int map_touch_to_screen(int touch_x, int touch_y, int *screen_x, int *screen_y) {
*screen_x = (int)((touch_x - points[0].touch_x) * scale_x + points[0].screen_x);
*screen_y = (int)((touch_y - points[0].touch_y) * scale_y + points[0].screen_y);
return 0;
}
/data/misc/touchscreen/ 目录下,或者写入内核的校准参数节点。
20.4 实战:外接触摸屏/绘图板
好了,理论讲完了。咱们来点实际的。
20.4.1 硬件连接
外接USB触摸屏或绘图板,通常有两种连接方式:
- USB HID直连:插上就能用,系统自动识别
- USB转I2C:需要额外的桥接芯片,驱动要单独适配
我建议优先选择USB HID设备。插上后,用 lsusb 查看设备信息:
# 查看USB设备
adb shell lsusb
Bus 001 Device 003: ID 0eef:0001 D-WAV Scientific Co., Ltd
# 查看输入设备
adb shell getevent -p
...
/dev/input/event2: 0001 0001 00000000 (TouchScreen)
...
20.4.2 驱动适配步骤
如果系统没有自动识别,需要手动加载驱动:
# 加载HID多点触控驱动
insmod /system/lib/modules/hid-multitouch.ko
# 查看驱动是否加载成功
lsmod | grep hid_multitouch
hid_multitouch 16384 0
# 查看设备节点
ls -l /dev/input/
crw-rw---- 1 root input 13, 64 2024-01-01 10:00 event0
crw-rw---- 1 root input 13, 65 2024-01-01 10:00 event1
crw-rw---- 1 root input 13, 66 2024-01-01 10:00 event2 # 触摸屏设备
20.4.3 坐标校准实战
写一个简单的校准工具:
// touch_calibrator.c
#include <stdio.h>
#include <fcntl.h>
#include <linux/input.h>
int main() {
int fd = open("/dev/input/event2", O_RDONLY);
if (fd < 0) {
printf("无法打开触摸设备\n");
return -1;
}
struct input_event ev;
int touch_x = 0, touch_y = 0;
int cal_points[4][2] = {0}; // 存储4个校准点
printf("请点击屏幕左上角...\n");
for (int i = 0; i < 4; i++) {
while (1) {
read(fd, &ev, sizeof(ev));
if (ev.type == EV_ABS) {
if (ev.code == ABS_MT_POSITION_X)
touch_x = ev.value;
else if (ev.code == ABS_MT_POSITION_Y)
touch_y = ev.value;
} else if (ev.type == EV_SYN && ev.code == SYN_REPORT) {
// 触摸点已就绪
cal_points[i][0] = touch_x;
cal_points[i][1] = touch_y;
printf("校准点 %d: (%d, %d)\n", i+1, touch_x, touch_y);
break;
}
}
if (i < 3) printf("请点击屏幕下一个角...\n");
}
// 计算校准参数并保存
// ... 省略具体计算代码
close(fd);
return 0;
}
20.4.4 绘图板特殊处理
绘图板和普通触摸屏的区别在于:
- 压力感应:需要处理
ABS_MT_PRESSURE事件 - 倾斜角度:需要处理
ABS_MT_TILT_X和ABS_MT_TILT_Y - 笔身旋转:需要处理
ABS_MT_ORIENTATION
绘图板的HID报告描述符会多出这些用法:
// 压力感应
0x0D, 0x30, // Usage (Tip Pressure)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs)
// 倾斜角度
0x0D, 0x3D, // Usage (X Tilt)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs)
20.5 知识体系总览
下面这张图总结了USB触摸屏从硬件到应用层的完整数据流:
这张图把整个流程串起来了。从触摸屏硬件上报原始数据,到内核驱动解析HID报告,再到Android框架做坐标映射,最后交给应用层使用。每一步都环环相扣。
说实话,USB触摸屏适配这件事,说难不难,说简单也不简单。关键是要理解HID协议的数据格式,以及Android的输入事件处理机制。只要把这两块吃透了,剩下的就是调试和踩坑了。
好了,这一章的内容就到这里。希望你能动手试试,找一块USB触摸屏或者绘图板,按照我讲的步骤走一遍。遇到问题别怕,多看看 dmesg 和 getevent 的输出,问题往往就藏在里面。
公众号:蓝海资料掘金营,微信deep3321