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字节。

我的经验:很多国产触摸屏厂商会在报告描述符里塞一些自定义用法。遇到这种情况,别慌。用USB分析仪抓包看看实际数据,对照描述符慢慢解析就行。我曾经遇到一个设备,报告描述符里写的是8点触控,实际只支持5点。厂商说「兼容性设计」——其实就是偷懒没改描述符。

20.2 Android端USB触摸屏驱动适配

Android系统对USB触摸屏的支持,主要依赖两个层面:

  1. 内核驱动层:USB HID驱动 + 触摸屏驱动
  2. 用户空间层: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-xtouchscreen-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_XABS_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)
我的经验:适配绘图板时,压力值需要做归一化处理。不同品牌的绘图板,压力范围不一样。Wacom是0-8191,Huion是0-4095。我习惯在驱动层统一映射到0-255,这样上层应用不用关心具体硬件。

20.5 知识体系总览

下面这张图总结了USB触摸屏从硬件到应用层的完整数据流:

USB触摸屏数据流与适配流程 触摸屏硬件 USB HID / I2C 内核驱动层 hid-multitouch.ko input子系统 Android框架 InputReader TouchInputMapper 坐标映射与校准 线性/非线性映射 应用层 绘图/触摸应用 校准流程 1. 获取原始坐标 2. 计算映射矩阵 3. 保存校准参数 4. 实时坐标转换 5. 上报给应用层 图:USB触摸屏从硬件到应用层的完整数据流 硬件 驱动 框架 映射 应用

这张图把整个流程串起来了。从触摸屏硬件上报原始数据,到内核驱动解析HID报告,再到Android框架做坐标映射,最后交给应用层使用。每一步都环环相扣。

说实话,USB触摸屏适配这件事,说难不难,说简单也不简单。关键是要理解HID协议的数据格式,以及Android的输入事件处理机制。只要把这两块吃透了,剩下的就是调试和踩坑了。

好了,这一章的内容就到这里。希望你能动手试试,找一块USB触摸屏或者绘图板,按照我讲的步骤走一遍。遇到问题别怕,多看看 dmesggetevent 的输出,问题往往就藏在里面。


公众号:蓝海资料掘金营,微信deep3321