程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Rockchip RK3399 - linux通过libusb读取usb数据包

----------------------------------------------------------------------------------------------------------------------------

开发板 :SOM-RK3399核心板+定制底板
eMMC16GB
LPDDR34GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot2017.09
linux4.19
----------------------------------------------------------------------------------------------------------------------------

注意:本节介绍的内容基于《Rockchip RK3399 - 移植uboot 2017.09 & linux 4.19(友善之家脚本方式)》移植的运行环境:内核版本4.19.193以及debian 11根文件系统。

在《Rockchip RK3399 - linux通过usbmon抓取usb数据包》中我们分析了我们所使用的USB触摸屏的HID报告描述符,并通过usbmon工具进行USB触摸屏数据包的抓取,最终结合HID报告描述符分析抓取到的数据包中数据的含义。

在《Rockchip RK3399 - 移植uboot 2017.09 & linux 4.19(友善之家脚本方式)》中我们介绍了在SOM-RK3399核心板上,通过USB3.0 Type-C接口将开发板模拟成USB设备。

这一节我们将通过libusb来实现USB触摸屏数据包的捕获,并将捕获的数据通过模拟的USB触摸屏发送到PC

一、模拟USB设备回顾

我们首先回顾一下模拟USB设备是如何实现的,我们编写了一个hid_keyboard_mouse.sh脚本,由于USB3.0 Type-C接口被配置为了从设备,因此可以通过USB3.0 Type-C接口将开发板模拟成USB设备(同时模拟鼠标、键盘、触摸屏功能)。

1.1 hid_keyboard_mouse.sh

/opt/hid下新建hid_keyboard_mouse.sh脚本:

#!/bin/bash

gadget=g1

do_start(){
    has_mount=$(mount -l | grep /sys/kernel/config)
    if [[ -z  $has_mount ]];then
        mount -t configfs none /sys/kernel/config
    fi
    cd /sys/kernel/config/usb_gadget

    # 当我们创建完这个文件夹之后,系统自动的在这个文件夹中创建usb相关的内容 ,这些内容需要由创建者自己填写
    if [[ ! -d ${gadget} ]]; then
        mkdir ${gadget}
    else
    	exit 0
    fi
    cd ${gadget}

    #设置USB协议版本USB2.0
    echo 0x0200 > bcdUSB

    #定义产品的VendorID和ProductID
    echo "0x0525"  > idVendor
    echo "0xa4ac" > idProduct

    #实例化"英语"ID:
    mkdir strings/0x409

    #将开发商、产品和序列号字符串写入内核
    echo "76543210" > strings/0x409/serialnumber
    echo "mkelehk"  > strings/0x409/manufacturer
    echo "keyboard_mouse"  > strings/0x409/product

    #创建一个USB配置实例
    if [[ ! -d configs/c.1 ]]; then
        mkdir configs/c.1
    fi

    #定义配置描述符使用的字符串
    if [[ ! -d configs/c.1/strings/0x409 ]]; then
        mkdir configs/c.1/strings/0x409
    fi

    echo "hid" > configs/c.1/strings/0x409/configuration

    #创建接口
    mkdir functions/hid.0   #键盘
    mkdir functions/hid.1   #鼠标
    mkdir functions/hid.2   #触摸屏

    #接口0,模拟键盘  
    echo 1 > functions/hid.0/subclass   #启动设备符
    echo 1 > functions/hid.0/protocol   #标识键盘设备
    echo 8 > functions/hid.0/report_length  #标识该hid设备每次发送的报表长度为8字节
    echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.0/report_desc  #配置hid描述符

	#接口1,模拟键盘鼠标
    echo 1 > functions/hid.1/subclass  #启动设备符
    echo 2 > functions/hid.1/protocol  #鼠标协议
    echo 4 > functions/hid.1/report_length  # 相对值是4
    echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x03\\x15\\x00\\x25\\x01\\x95\\x03\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x05\\x81\\x03\\x05\\x01\\x09\\x30\\x09\\x31\\x09\\x38\\x15\\x81\\x25\\x7f\\x75\\x08\\x95\\x03\\x81\\x06\\xc0\\xc0 > functions/hid.1/report_desc

    #接口2,模拟触摸屏
    echo 0 > functions/hid.2/subclass
    echo 0 > functions/hid.2/protocol
    echo 5 > functions/hid.2/report_length  #标识该hid设备每次发送的报表长度为5字节
    echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x05\\x15\\x00\\x25\\x01\\x95\\x05\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x03\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\xc0\\xc0 > functions/hid.2/report_desc

    #捆绑接口到配置config.1
    ln -s functions/hid.0 configs/c.1
    ln -s functions/hid.1 configs/c.1
    ln -s functions/hid.2 configs/c.1

    #配置USB3.0 OTG0的工作模式为Device(设备):
    #echo peripheral > /sys/devices/platform/ff770000.syscon/ff770000.syscon:usb2-phy@e460/otg_mode

    echo "sleep 3s"
    sleep 3s

    #将gadget驱动注册到UDC上,插上USB线到电脑上,电脑就会枚举USB设备。
    echo fe900000.dwc3 > UDC
}

do_stop() {
    cd /sys/kernel/config/usb_gadget/${gadaget}
    echo "" > UDC
}

case $1 in
    start)
        echo "Start hid gadget "
        do_start
        ;;
    stop)
        echo "Stop hid gadget"
        do_stop
        ;;
    *)
        echo "Usage: $0 (stop | start)"
        ;;
esac

/opt/profile.d下新建hid.sh脚本:

#!/bin/bash

/opt/hid/hid_keyboard_mouse.sh start

我们将这个脚本配置为开机自动执行,重新开机,开机会自动执行/etc/profile.d的所有shell脚本。查看hid设备;

root@SOM-RK3399v2:/home/pi# ls /dev/hidg* -nR
crw------- 1 0 0 236, 0 Sep 26 14:25 /dev/hidg0    # 键盘
crw------- 1 0 0 236, 1 Sep 26 14:25 /dev/hidg1    # 鼠标
crw------- 1 0 0 236, 2 Sep 26 14:25 /dev/hidg2    # 触摸屏 
root@SOM-RK3399v2:/home/pi# ls /sys/kernel/config/usb_gadget/g1/
UDC           bDeviceProtocol  bMaxPacketSize0  bcdUSB   functions  idVendor  strings
bDeviceClass  bDeviceSubClass  bcdDevice        configs  idProduct  os_desc

1.2 HID报告描述符

既然我们想通过模拟的USB触摸屏向PC发送数据,自然而然的我们要搞懂模拟的USB触摸屏的HID报告描述符:

echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x05\\x15\\x00\\x25\\x01\\x95\\x05\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x03\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\xc0\\xc0 > functions/hid.2/report_desc

根据《Rockchip RK3399 - linux通过usbmon抓取usb数据包》介绍的HID报告描述符的规则描述,我们得到;

0x05,0x01,   // USAGE_PAGE (Generic Desktop)
0x09,0x02,   // USAGE (Mouse)
0xa1,0x01,   // COLLECTION (Application)
	0x09,0x01,   //   USAGE (Pointer)
	0xa1,0x00,   //   COLLECTION (Physical)
		/* 下面定义的是用途Button.1~Button.5,加上3个无效位,一共占用1个字节 */
		0x05,0x09,  //     USAGE_PAGE (Button)
		0x19,0x01,  //     USAGE_MINIMUM (Button 1)
		0x29,0x05,  //     USAGE_MAXIMUM (Button 5)
		0x15,0x00,  //     LOGICAL_MINIMUM (0)
		0x25,0x01,  //     LOGICAL_MAXIMUM (1)
		0x95,0x05,  //     REPORT_COUNT (5)
		0x75,0x01,  //     REPORT_SIZE (1)
		0x81,0x02,  //     INPUT (Data,Var,Abs)
		0x95,0x01,  //     REPORT_COUNT (1)
		0x75,0x03,  //     REPORT_SIZE (3)
		0x81,0x01,  //     INPUT (Data,Var,Abs)
		
		/* 下面定义的是用途GenericDesktop.X、GenericDesktop.Y,各占用2个字节 */
		0x05,0x01,  //     USAGE_PAGE (Generic Desktop)
		0x09,0x30,  //     USAGE (X)
		0x09,0x31,  //     USAGE (Y)
		0x15,0x00,  //     LOGICAL_MINIMUM (0) 
		0x26,0xff,0x7f,  //     LOGICAL_MAXIMUM (0x7fff)
		0x35,0x00,  // PHYSICAL_MINIMUM (0)
		0x46,0xff,0x7f,  // PHYSICAL_MAXIMUM (0x7fff)
		0x75,0x10,  //     REPORT_SIZE (16)
		0x95,0x02,  //     REPORT_COUNT (2)
		0x81,0x02,  //     INPUT (Data,Var,Abs)
	0xc0,      // End COLLECTION
0xc0       // End COLLECTION

我们需要关注一下HID报告描述符中X、Y坐标的逻辑最小值和最大值,这个后面应用程序中有使用到;

 Logical Minimum(0)
 Logical Maximum(32767)

由于HID报告描述符只有1个报告,因此在USB数据包中就不用指定报告ID(需要注意的是:如果HID报告描述符有多个报告,那么存在一个USB数据包中有包含多个报告的场景),通过分析,我们可以知道一个USB数据包包含5个字节:

字节 描述
BYTE0 bit [7~5]:未使用
bit[4] 1:表示EXTRA键按下 0:表示松开
bit[3] 1:表示SIDE键按下 0:表示松开
bit[2] 1:表示中键按下 0:表示松开
bit[1] 1:表示右键按下 0:表示松开
bit[0] 1:表示左键按下 0:表示松开
BYTE1 X坐标低8位(绝对值)
BYTE2 X坐标高8位(绝对值)
BYTE3 Y坐标低8位(绝对值)
BYTE4 Y坐标高8位(绝对值)

我们只需要按照上面表格的描述构建USB数据包,并将数据写入/dev/hidg2设备中,即可通过USB OTG线将数据发送到PC

二、USB触摸屏回顾

在《Rockchip RK3399 - linux通过usbmon抓取usb数据包》中我们分析了我们所使用的USB触摸屏(PID=e5e3,VID=1a86)的HID报告描述符;

root@SOM-RK3399v2:/#  cat /sys/kernel/debug/hid/0003\:1A86\:E5E3.0007/rdesc
05 0d 09 04 a1 01 85 01 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 95 07 81 01 75 08 09 51 95 01 81 02 05 01 26 00 10 75 10 55 0e 65 11 09 30 35 00 46 79 08 81 02 26 00 10 46 4c 05 09 31 81 02 05 0d 09 48 81 02 c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 95 07 81 01 75 08 09 51 95 01 81 02 05 01 26 00 10 75 10 55 0e 65 11 09 30 35 00 46 79 08 81 02 26 00 10 46 4c 05 09 31 81 02 05 0d 09 48 81 02 c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 95 07 81 01 75 08 09 51 95 01 81 02 05 01 26 00 10 75 10 55 0e 65 11 09 30 35 00 46 79 08 81 02 26 00 10 46 4c 05 09 31 81 02 05 0d 09 48 81 02 c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 95 07 81 01 75 08 09 51 95 01 81 02 05 01 26 00 10 75 10 55 0e 65 11 09 30 35 00 46 79 08 81 02 26 00 10 46 4c 05 09 31 81 02 05 0d 09 48 81 02 c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 95 07 81 01 75 08 09 51 95 01 81 02 05 01 26 00 10 75 10 55 0e 65 11 09 30 35 00 46 79 08 81 02 26 00 10 46 4c 05 09 31 81 02 05 0d 09 48 81 02 c0 09 22 a1 02 09 42 15 00 25 01 75 01 95 01 81 02 95 07 81 01 75 08 09 51 95 01 81 02 05 01 26 00 10 75 10 55 0e 65 11 09 30 35 00 46 79 08 81 02 26 00 10 46 4c 05 09 31 81 02 05 0d 09 48 81 02 c0 05 0d 27 ff ff 00 00 75 10 95 01 09 56 81 02 09 54 15 00 25 7f 95 01 75 08 81 02 85 02 09 55 95 01 25 0a b1 02 85 03 06 00 ff 09 c5 15 00 26 ff 00 75 08 96 00 01 b1 02 c0

同时对HID报告描述符进行了解析,得知第一个触摸点的描述数据位于捕获到的数据包的字节1~字节8中。

位7 位6 位5 位4 位3 位2 位1 位0
字节0 报告编号
字节1 第一个触摸点状态
字节2 第一个触摸点标识符
字节3 X坐标低8位(绝对值)
字节4 X坐标高8位(绝对值)
字节5 Y坐标低8位(绝对值)
字节6 Y坐标高8位(绝对值)
字节7 触摸笔宽度低8位
字节8 触摸笔宽度高8位

我们需要关注一下报告描述符中X、Y坐标的逻辑最小值和最大值;

 Logical Minimum(0)
 Logical Maximum(4096)  // 0x1000

实际上这个就是移动触摸点时捕获到的USB数据包中X、Y坐标的区间范围;

  • 当触摸点移动到屏幕的最左侧,X->0;
  • 当触摸点移动到屏幕的最右侧,X->4096;
  • 当触摸点移动到屏幕的最上边,Y->0;
  • 当触摸点移动到屏幕的最下边,Y->4096;

三、应用程序

这里我们编写一个应用程序通过libusb来实现USB触摸屏数据包的捕获,并将捕获的数据通过模拟的USB触摸屏设备发送到PC。具体思路如下:

  • 通过libusb库读取USB触摸屏的数据包;
  • 结合USB触摸屏的HID报告描述符,获取到第一个触摸点的信息(按键状态、X、Y坐标);
  • 结合模拟的USB触摸屏的HID报告描述符,将按键状态、X、Y坐标转换成模拟的USB触摸屏的所需的报告格式;
  • 向模拟的USB触摸屏写入报告数据;

3.1 应用程序

3.1.1 usb_test.c

/opt/libusb目录下编写测试应用程序usb_test.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <getopt.h>
#include <ctype.h>
#include <signal.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <stdbool.h>
#include "libusb.h"

/* Developers will wish to consult the API documentation: http://api.libusb.info  */
/* https://github.com/libusb/libusb */

static int verbose = 1;

static volatile sig_atomic_t rcv_exit;

/* 触摸点ID */
static int touch_id = 100;

/* 模拟的usb触摸屏设备 x、y坐标逻辑最大值 */
static int gadget_x_logical_maximum = 0x7fff;
static int gadget_y_logical_maximum = 0x7fff;

/* 存放hid报告描述符解析后的信息 */
struct hid_descriptor_t
{
    /* usb触摸屏描述信息 */
    uint16_t vid;               /* 生产厂家ID */
    uint16_t pid;                /* 产品ID */

    /* */
    uint8_t endpoint_address;   /* 端点地址 */
    uint8_t report_length;      /* 报告长度,标识该hid设备每次发送的报表长度 */

    /* 第一个触摸点描述信息 */
    uint16_t btn_offset;        /* 按键状态偏移位,单位为位 */
    uint16_t index_x_low;       /* x坐标低8位索引,单位为字节 */
    uint16_t index_x_high;      /* x坐标高8位索引,单位为字节 */
    uint16_t index_y_low ;      /* y坐标低8位索引,单位为字节 */
    uint16_t index_y_high;      /* y坐标高8位索引,单位为字节 */
    int x_logical_minimum;      /* x坐标逻辑最小值 */
    int x_logical_maximum;      /* x坐标逻辑最大值 */
    int y_logical_minimum;      /* y坐标逻辑最小值 */
    int y_logical_maximum;      /* y坐标逻辑最大值 */
    int interface_number;       /* bInterfaceNumber */
};

/* usb触摸屏报告 */
struct touch_screen_report_t
{
    uint8_t buttons;        /* 按键按下状态 */
    uint16_t x;             /* x坐标 */
    uint16_t y;             /* y坐标 */
};

/* 存放不同usb触摸屏hid报告描述符解析后的信息 */
static const struct hid_descriptor_t hids[] = {
    [0] = { 0x1a86, 0xe5e3, 0x82, 52, 8, 3, 4, 5, 6, 0, 0x1000, 0, 0x1000, 0 },
    [1] = { 0x222a, 0x0001, 0x81, 52, 14, 2, 3, 4, 5, 0, 0x0780, 0, 0x0438, 0 },
    [2] = { 0x03eb, 0x8a6e, 0x83, 59, 8, 2, 3, 6, 7, 0, 0x0fff, 0, 0x0fff,1 },
};

/**
 * 上报事件
 * @param fd:文件描述符
 * @param type:事件类型
 * @param code:事件code
 * @param value:事件值
 */
static int report_key(int fd, int type, int code, int32_t value)
{
    struct input_event event;
    event.type = type;
    event.code = code;
    event.value = value;
    gettimeofday(&event.time, 0);

    if (write(fd, &event, sizeof(struct input_event)) < 0)
    {
        perror("report key error!\n");
        return -1;
    }

    return 0;
}

/**
 * 上报slot,对于协议B,内核驱动应该把每一个识别出的触摸点和一个slot相关联
 * @param fd:文件描述符
 * @param slot: 当前发送的是哪个slot的坐标信息,也就是哪个触摸点  
 *              比如10点触摸,最多可以同时支持10个触摸点,slot可以理解为是第几个触摸点
 */
static void input_mt_slot(int fd, int slot)
{
    report_key(fd, EV_ABS, ABS_MT_SLOT, slot);
}

/**
 * 使用当前slot来传播触摸状态的改变,通过修改关联slot的ABS_MT_TRACKING_ID来达到对触摸点的创建,替换和销毁。
 * 1. ABS_MT_TRACKING_ID用来跟踪触摸点属于哪一条线,如果触摸点的ID值与上一次事件中ID值相等,那么他们就属于同一条线,
 *    ID值并不是随便赋值的,而是硬件上跟踪了触摸点的轨迹,比如按下一个点硬件会跟踪这个点的ID,只要不抬起上报的点都会和这个ID相关
 * 2. 上报ABS_MT_TRACKING_ID -1系统会清除对应的ID和slot
 * @param fd:文件描述符
 * @param active: 1连续触摸(表示触摸点一直按下),0抬起(表示触摸点无效了)
 */
static void input_mt_report_slot_state(int fd, bool active)
{
    // 上报ABS_MT_TRACKING_ID -1系统会清除对应的slot和ID,表示触摸点抬起
    if (!active) {
        report_key(fd, EV_ABS, ABS_MT_TRACKING_ID, -1);
        return;
    }

    // ABS_MT_TRACKING_ID大于0,表示触摸点按下   如果ABS_MT_TRACKING_ID本次并没有改变表示触摸点还在一个轨迹上
    report_key(fd, EV_ABS, ABS_MT_TRACKING_ID, touch_id);
}

/**
 * 上报触摸点坐标
 * @param fd:文件描述符
 * code: 要上报的是什么数据
 * value: 要上报的数据值
 * return: 无
 */
static void input_report_abs(int fd, unsigned int code,int value)
{
    report_key(fd, EV_ABS, code, value);
}

/**
 * 触摸点按下或者抬起
 * @param fd:文件描述符
 * @param x:x坐标
 * @param y:y坐标
 * @param down:true 按下状态,false抬起状态
 */
static void touch_screen_pressed(int fd, int x, int y, bool down)
{
    // 上报触摸点序号
    input_mt_slot(fd, 0);

    if(down){
        // 为触摸点分配ID
        input_mt_report_slot_state(fd, 1);
         // 上报触摸点X轴坐标信息
        input_report_abs(fd, ABS_MT_POSITION_X, x);
        // 上报触摸点Y轴坐标信息
        input_report_abs(fd, ABS_MT_POSITION_Y, y);
    }else{
        input_mt_report_slot_state(fd, 0);
    }

    // 同步事件
    report_key(fd, EV_SYN, SYN_REPORT, 0);
}

/**
 * 创建一个类似于触摸屏的虚拟设备,返回文件描述符
 */
static int create_touch_screen()
{
     int fd,err;
     struct uinput_user_dev dev;

     fd = open("/dev/uinput", O_WRONLY|O_NONBLOCK);
     if (fd < 0) {
         printf("Error could not open /dev/uinput device: %s\n", strerror(errno));
         return -1;
     }

     // configure touch device event properties
     memset(&dev, 0, sizeof(dev));

     // 设备的别名
     strncpy(dev.name, "TouchScreen", UINPUT_MAX_NAME_SIZE);
     dev.id.version = 1;
     dev.id.bustype = BUS_USB;

    // 支持10点触摸
     dev.absmin[ABS_MT_SLOT] = 0;
     dev.absmax[ABS_MT_SLOT] = 9;


     dev.absmin[ABS_MT_TRACKING_ID] = 0;
     dev.absmax[ABS_MT_TRACKING_ID] = 65535;

     dev.absmin[ABS_MT_TOUCH_MAJOR] = 0;
     dev.absmax[ABS_MT_TOUCH_MAJOR] = 0xff;

	 // 屏幕最小/大的X尺寸
     dev.absmin[ABS_MT_POSITION_X] = 0;      
     dev.absmax[ABS_MT_POSITION_X] = gadget_x_logical_maximum;  

	 // 屏幕最小/大的Y尺寸
     dev.absmin[ABS_MT_POSITION_Y] = 0;     
     dev.absmax[ABS_MT_POSITION_Y] = gadget_y_logical_maximum;  

	 //屏幕按下的压力值
     dev.absmin[ABS_MT_PRESSURE] = 0;
     dev.absmax[ABS_MT_PRESSURE] = 0xff;     

     // Setup the uinput device
     err = ioctl(fd, UI_SET_EVBIT, EV_KEY);
     if (err < 0){
        goto err;
     }
     err = ioctl(fd, UI_SET_EVBIT, EV_REL);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_EVBIT,  EV_ABS);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_ABSBIT, ABS_MT_SLOT);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_ABSBIT, ABS_MT_POSITION_X);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
     if (err < 0){
        goto err;
     }
     err = ioctl (fd, UI_SET_KEYBIT, BTN_TOUCH);
     if (err < 0){
        goto err;
     }

     /* Create input device into input sub-system */
     err = write(fd, &dev, sizeof(dev));
     if (err < 0){
        goto err;
     }
     err = ioctl(fd, UI_DEV_CREATE);
     if (err < 0){
        goto err;
     }

     return fd;
err:
     printf("(%s) Failed to initialise\n",__func__);
     close(fd);
}

/**
 * 输出usb端点额外信息
 * @param config:usb端点伴随描述符
 */
static void print_endpoint_comp(const struct libusb_ss_endpoint_companion_descriptor *ep_comp)
{
    printf("      USB 3.0 Endpoint Companion:\n");
    printf("        bMaxBurst:           %u\n", ep_comp->bMaxBurst);
    printf("        bmAttributes:        %02xh\n", ep_comp->bmAttributes);
    printf("        wBytesPerInterval:   %u\n", ep_comp->wBytesPerInterval);
}

/**
 * 输出usb端点信息
 * @param config:usb端点描述符
 */
static void print_endpoint(const struct libusb_endpoint_descriptor *endpoint)
{
    int i, ret;

    printf("      Endpoint:\n");
    // 端点地址:位[3:0]表示端点号,第7位是方向:0位输出、1为输入
    printf("        bEndpointAddress:    %02xh\n", endpoint->bEndpointAddress);
    // 端点属性:位[1:0]值00表示控制、01表示等时、10表示批量、11表示中断
    printf("        bmAttributes:        %02xh\n", endpoint->bmAttributes);
    // 本端点接受或发送的最大信息包的大小
    printf("        wMaxPacketSize:      %u\n", endpoint->wMaxPacketSize);
    // 轮询数据传输端点的时间间隔,用在中断传输上,比如间隔时间查询鼠标的数据;
    printf("        bInterval:           %u\n", endpoint->bInterval);
    printf("        bRefresh:            %u\n", endpoint->bRefresh);
    printf("        bSynchAddress:       %u\n", endpoint->bSynchAddress);

    // 遍历usb端点额外信息
    for (i = 0; i < endpoint->extra_length;) {
        if (LIBUSB_DT_SS_ENDPOINT_COMPANION == endpoint->extra[i + 1]) {
            struct libusb_ss_endpoint_companion_descriptor *ep_comp;

            // 获取端点伴随描述符的详细信息
            ret = libusb_get_ss_endpoint_companion_descriptor(NULL, endpoint, &ep_comp);
            if (LIBUSB_SUCCESS != ret)
            continue;

            // 输出usb端点信息
            print_endpoint_comp(ep_comp);

            // 释放usb端点伴随描述符
            libusb_free_ss_endpoint_companion_descriptor(ep_comp);
        }

        i += endpoint->extra[i];
    }
}

/**
 * 输出usb接口信息
 * @param config:usb接口描述符
 */
static void print_altsetting(const struct libusb_interface_descriptor *interface)
{
    uint8_t i;

    printf("    Interface:\n");
    // 接口的编号
    printf("      bInterfaceNumber:      %u\n", interface->bInterfaceNumber);
    // 接口的设置的编号
    printf("      bAlternateSetting:     %u\n", interface->bAlternateSetting);
    // 使用的端点个数(不包括端点0), 表示有多少个端点描述符
    printf("      bNumEndpoints:         %u\n", interface->bNumEndpoints);
    // 接口类型
    printf("      bInterfaceClass:       %u\n", interface->bInterfaceClass);
    // 接口子类型
    printf("      bInterfaceSubClass:    %u\n", interface->bInterfaceSubClass);
    // 接口所遵循的协议
    printf("      bInterfaceProtocol:    %u\n", interface->bInterfaceProtocol);
    // 描述该接口的字符串索引值
    printf("      iInterface:            %u\n", interface->iInterface);

    // 遍历usb端点描述符,输出usb端点信息
    for (i = 0; i < interface->bNumEndpoints; i++)
        print_endpoint(&interface->endpoint[i]);
}

static void print_2_0_ext_cap(struct libusb_usb_2_0_extension_descriptor *usb_2_0_ext_cap)
{
    printf("    USB 2.0 Extension Capabilities:\n");
    printf("      bDevCapabilityType:    %u\n", usb_2_0_ext_cap->bDevCapabilityType);
    printf("      bmAttributes:          %08xh\n", usb_2_0_ext_cap->bmAttributes);
}

static void print_ss_usb_cap(struct libusb_ss_usb_device_capability_descriptor *ss_usb_cap)
{
    printf("    USB 3.0 Capabilities:\n");
    printf("      bDevCapabilityType:    %u\n", ss_usb_cap->bDevCapabilityType);
    printf("      bmAttributes:          %02xh\n", ss_usb_cap->bmAttributes);
    printf("      wSpeedSupported:       %u\n", ss_usb_cap->wSpeedSupported);
    printf("      bFunctionalitySupport: %u\n", ss_usb_cap->bFunctionalitySupport);
    printf("      bU1devExitLat:         %u\n", ss_usb_cap->bU1DevExitLat);
    printf("      bU2devExitLat:         %u\n", ss_usb_cap->bU2DevExitLat);
}

static void print_bos(libusb_device_handle *handle)
{
    struct libusb_bos_descriptor *bos;
    uint8_t i;
    int ret;

    ret = libusb_get_bos_descriptor(handle, &bos);
    if (ret < 0)
    return;

    printf("  Binary Object Store (BOS):\n");
    printf("    wTotalLength:            %u\n", bos->wTotalLength);
    printf("    bNumDeviceCaps:          %u\n", bos->bNumDeviceCaps);

    for (i = 0; i < bos->bNumDeviceCaps; i++) {
        struct libusb_bos_dev_capability_descriptor *dev_cap = bos->dev_capability[i];

        if (dev_cap->bDevCapabilityType == LIBUSB_BT_USB_2_0_EXTENSION) {
            struct libusb_usb_2_0_extension_descriptor *usb_2_0_extension;

            ret = libusb_get_usb_2_0_extension_descriptor(NULL, dev_cap, &usb_2_0_extension);
            if (ret < 0)
            return;

            print_2_0_ext_cap(usb_2_0_extension);
            libusb_free_usb_2_0_extension_descriptor(usb_2_0_extension);
        } else if (dev_cap->bDevCapabilityType == LIBUSB_BT_SS_USB_DEVICE_CAPABILITY) {
            struct libusb_ss_usb_device_capability_descriptor *ss_dev_cap;

            ret = libusb_get_ss_usb_device_capability_descriptor(NULL, dev_cap, &ss_dev_cap);
            if (ret < 0)
            return;

            print_ss_usb_cap(ss_dev_cap);
            libusb_free_ss_usb_device_capability_descriptor(ss_dev_cap);
        }
    }

    libusb_free_bos_descriptor(bos);
}

/**
 * 输出usb接口信息
 * @param config:usb接口
 */
static void print_interface(const struct libusb_interface *interface)
{
    int i;

    // 遍历接口所支持的可选设置
    for (i = 0; i < interface->num_altsetting; i++)
        print_altsetting(&interface->altsetting[i]);
}

/**
 * 输出usb配置信息
 * @param config:usb配置描述符
 */
static void print_configuration(struct libusb_config_descriptor *config)
{
    uint8_t i;

    printf("  Configuration:\n");
    // 配置描述符的总长度,以字节为单位
    printf("    wTotalLength:            %u\n", config->wTotalLength);
    // 配置所支持的接口数量
    printf("    bNumInterfaces:          %u\n", config->bNumInterfaces);
    // 配置值,用于在设置或选择配置时标识该配置
    printf("    bConfigurationValue:     %u\n", config->bConfigurationValue);
    // 配置描述符字符串的索引。可以使用该索引在设备的字符串描述符中找到配置描述符的字符串表示
    printf("    iConfiguration:          %u\n", config->iConfiguration);
    // 供电模式的选择
    printf("    bmAttributes:            %02xh\n", config->bmAttributes);
    // 设备从总线提取的最大电流
    printf("    MaxPower:                %u\n", config->MaxPower);

    // 遍历usb接口描述符,输出usb接口信息
    for (i = 0; i < config->bNumInterfaces; i++)
        print_interface(&config->interface[i]);
}

/**
 * 判断当前设备的vid和pid是否和指定的匹配
 * @param vid:生产厂商ID
 * @param pid:产品ID
 */
bool device_match_vid_pid(libusb_device *dev,uint16_t vid, uint16_t pid)
{
    struct libusb_device_descriptor desc;
    int ret;
    // 获取usb设备描述符
    ret = libusb_get_device_descriptor(dev, &desc);
    if (ret < 0) {
        fprintf(stderr, "failed to get device descriptor");
        return false;
    }
    return desc.idVendor == vid && desc.idProduct == pid;
}

/**
 * 输出usb设备信息
 *
 * @param dev:usb设备
 * @param handle:该变量是libusb库中的设备句柄,用于操作被封装的USB设备
 */
void print_device(libusb_device *dev, libusb_device_handle *handle)
{
    struct libusb_device_descriptor desc;
    unsigned char string[256];
    const char *speed;
    int ret;
    uint8_t i;

    // 获取usb设备的速度
    switch (libusb_get_device_speed(dev)) {
        case LIBUSB_SPEED_LOW: speed = "1.5M"; break;
        case LIBUSB_SPEED_FULL: speed = "12M"; break;
        case LIBUSB_SPEED_HIGH: speed = "480M"; break;
        case LIBUSB_SPEED_SUPER: speed = "5G"; break;
        case LIBUSB_SPEED_SUPER_PLUS: speed = "10G"; break;
        default: speed = "Unknown";
    }

    // 获取usb设备描述符
    ret = libusb_get_device_descriptor(dev, &desc);
    if (ret < 0) {
        fprintf(stderr, "failed to get device descriptor");
        return;
    }

    if (!handle)
        libusb_open(dev, &handle);

    // 输出usb总线编号、设备地址、生产厂商ID、产品ID、速度信息
    printf("Dev (bus %u, device %u): %04X - %04X speed: %s\n",
           libusb_get_bus_number(dev), libusb_get_device_address(dev),
           desc.idVendor, desc.idProduct, speed);

    if (handle){
        // 输出生产厂商信息
        if (desc.iManufacturer) {
            // 获取string描述符
            ret = libusb_get_string_descriptor_ascii(handle, desc.iManufacturer, string, sizeof(string));
            if (ret > 0)
                printf("  Manufacturer:              %s\n", (char *)string);
        }

        // 输出产品信息
        if (desc.iProduct) {
            // 获取string描述符
            ret = libusb_get_string_descriptor_ascii(handle, desc.iProduct, string, sizeof(string));
            if (ret > 0)
                printf("  Product:                   %s\n", (char *)string);
        }

        // 输出序列号信息
        if (desc.iSerialNumber && verbose) {
            ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, string, sizeof(string));
            if (ret > 0)
                printf("  Serial Number:             %s\n", (char *)string);
        }
    }

    if (verbose) {
        // 遍历配置描述符
        for (i = 0; i < desc.bNumConfigurations; i++) {
            struct libusb_config_descriptor *config;

            // 获取配置描述符
            ret = libusb_get_config_descriptor(dev, i, &config);
            if (LIBUSB_SUCCESS != ret) {
                printf("  Couldn't retrieve descriptors\n");
                continue;
            }

            // 输出配置描述符信息
            print_configuration(config);

            // 释放通过libusb_get_config_descriptor函数获取到的配置描述符
            libusb_free_config_descriptor(config);
        }

        // USB2.0以上协议  USB 2.0 的版本号是 0x0200
        if (handle && desc.bcdUSB >= 0x0201)
            print_bos(handle);
    }

    if(handle){
        libusb_close(handle);
    }
}

/**
 * 用于测试指定名称的usb设备,设备存在返回0,设备不存在返回1
 * @param device_name:usb设备名称(必须指定)
 */
int test_wrapped_device(const char *device_name)
{
    libusb_device_handle *handle;
    int r, fd;

    // 打开设备文件
    fd = open(device_name, O_RDWR);
    if (fd < 0) {
        printf("Error could not open %s: %s\n", device_name, strerror(errno));
        return 1;
    }

    // 使用libusb提供的函数对设备进行封装、第三个参数是用于保存设备句柄的指针handle
    r = libusb_wrap_sys_device(NULL, fd, &handle);
    if (r) {
        printf("Error wrapping device: %s: %s\n", device_name, libusb_strerror(r));
        close(fd);
        return 1;
    }

    // 输出设备信息
    print_device(libusb_get_device(handle), handle);
    close(fd);
    return 0;
}

/**
 * 信号处理函数
 * @param signum:信号值
 */
static void sig_handler(int signum)
{
    switch (signum) {
        case SIGTERM:
            rcv_exit = 1;
            break;
        case SIGINT:
            rcv_exit = 1;
            break;
        case SIGUSR1:
            break;
    }
}

static void usage(char *program)
{
    printf("%s - test usb data transfers to/from usb device\n",program);
    printf("Usage:\n");
    printf("  %s [options]\n", program);
    printf("options are:\n");
    printf("Common:\n");
    printf("  --help (or -h)\n");
    printf("  -v vendor_id\n");
    printf("  -p product_id\n");
    printf("  -d usb device name\n");
    printf("  -o usb gadget device name\n");
}


/**
 * 向模拟的usb触摸屏设备发送数据,每次发送5字节报告
 * [0]的D0就是左键,D1就是右键,D2就是中键
 * [1]为X轴低字节
 * [2]为X轴高字节
 * [3]为Y轴低字节
 * [4]为Y轴高字节

 * @param fd:usb gadget device的文件描述符,文件所指向的hid设备的报告描述符需要按照发送的数据格式配置
 * @param touch_screen_report_t: 报告内容
 *
 * 写入成功返回0,否则返回-1
 */
static int send_usb_packet(int fd, struct touch_screen_report_t *report)
{
    char data[5] = {0,0,0,0,0};
    data[0] = report->buttons;
    data[1] = report->x & 0xFF;
    data[2] = (report->x >> 8) & 0xFF;
    data[3] = report->y & 0xFF;
    data[4] = (report->y >> 8) & 0xFF;

//    printf("send data:\n");
//    for(int i=0;i<5;i++){
//       printf("0x%x ",data[i]);
//    }
//    printf("\n");

    // 写入失败
    if (write(fd, data, 5) != 5) {
        printf("Error write usb gadget device\n");
        return -1;
    }

    /* 伪造双击效果,鼠标点击间隔小于系统设置的鼠标双击的间隔时间 */
//    data[0] = 0;
//    if (write(fd, data, 5) != 5) {
//        printf("Error write usb gadget device\n");
//        return -1;
//    }

    return 0;
}

/**
 * 解析读取到的usb数据包,获取按键状态、x、y坐标,并转换为模拟的usb触摸屏的数据格式;
 * @param hid:hid报告描述符信息,用于解析读取到的usb数据包
 * @param data:读取的usb数据包
 * @param report:写入解析后的按键状态、x、y坐标
 *
 * 解析成功返回0,否则返回-1
 */
static int parse_usb_package(const struct hid_descriptor_t *hid,const char *data, struct touch_screen_report_t *report)
{
    if(!hid){
        printf("Error parse usb package, invalid hid descriptor.\n");
        return -1;
    }

    // logical_minimum-logical_maximum 映射到 0~0x7fff
    double x_scale =  1.0 * (gadget_x_logical_maximum-0) / (hid->x_logical_maximum - hid->x_logical_minimum);
    double y_scale =  1.0 * (gadget_y_logical_maximum-0) / (hid->y_logical_maximum - hid->y_logical_minimum);
    report->buttons = (data[hid->btn_offset/8] &  (1<< hid->btn_offset%8)) >> (hid->btn_offset%8);
    report->x = (data[hid->index_x_low] | data[hid->index_x_high] << 8) * x_scale ;
    report->y = (data[hid->index_y_low] | data[hid->index_y_high] << 8) * y_scale;
    return 0;
}

/**
 * 读取usb数据包
 * @param vendor_id:生产厂商ID
 * @param product_id:产品ID
 * @param gadget_device_name:模拟的usb触摸屏设备
 */
static int interrupt_data_rw(uint16_t vendor_id, uint16_t product_id,const char * gadget_device_name)
{
    int kernelDriverDetached = 0;
    unsigned char data[64]={0};
    int length = 0;
    int r,j,fd_gadget = 0,fd_event = 0;
    libusb_device_handle *handle;
    struct touch_screen_report_t report;
    const struct hid_descriptor_t *hid = NULL;

    // 查找匹配的hid报告描述符信息
    for(int i=0; i< sizeof(hids) / sizeof(hids[0]); i++){
        if((hids[i].vid == vendor_id) && (hids[i].pid == product_id)){
            hid = &hids[i];
        }
    }

    if(!hid){
        printf("Error please config hid descriptor info for usb device vid:0x%x pid:0x%x \n", vendor_id, product_id);
        return -1;;
    }

    // 打开指定Vendor ID和Product ID的USB设备,并返回设备句柄
    handle = libusb_open_device_with_vid_pid(NULL, vendor_id, product_id);
    if (handle == NULL) {
        printf("libusb_open() failed\n");
        return -1;;
    }

    /* 驱动必须解绑定,否则数据由驱动程序处理 */
    if(libusb_kernel_driver_active(handle, hid->interface_number)){
        printf("Kernel Driver Active\n");
        r = libusb_detach_kernel_driver(handle, hid->interface_number);
        if (r == 0) {
            printf("Detach Kernel Driver\n");
            kernelDriverDetached = 1;
        }else{
            fprintf(stderr, "Error detaching kernel driver.\n");
            return -1;;
        }
    }

    /* 指定当前接口 */
    r = libusb_claim_interface(handle, hid->interface_number);
    if (r != 0){
        fprintf(stderr, "Error claiming interface.\n");
        goto exit;
    }

    // 打开模拟的usb触摸屏设备
    if(gadget_device_name){
        // 打开设备文件
        fd_gadget = open(gadget_device_name, O_RDWR, 0666);
        if (fd_gadget < 0) {
            printf("Error could not open usb gadget device %s: %s\n", gadget_device_name, strerror(errno));
            return 1;
        }
    }

    // 创建一个类似于触摸屏的虚拟设备
    fd_event = create_touch_screen();

    // 退出标志位
    while(!rcv_exit){
        memset(data, 0, sizeof(data));

        /* 中断方式读取断点数据,指定端点地址、报告长度,超时时间设置为0无线等待,直至有值 */
        r = libusb_interrupt_transfer(handle, hid->endpoint_address, data, hid->report_length, &length, 0);
        if ((r < 0) || (length == 0)){
            printf("bulk recive error,r:%s length:%d\n", libusb_strerror(r),length);
            usleep(500000);  // 单位微秒
        }else{
            printf("receive data:\n");
            for(j=0; j<length; j++) {
                printf("0x%x ",data[j]);
            }
            printf("\n");

            // 解析usb device数据包,获取按键按下状态、以及x、y坐标
            r = parse_usb_package(hid,data,&report);

            // 向模拟的usb触摸屏设备发送数据
            if(fd_gadget > 0 && r == 0){
               send_usb_packet(fd_gadget,&report);
            }

            // 向触摸屏输入设备发送数据
            if(fd_event > 0 && r == 0){
               touch_screen_pressed(fd_event, report.x, report.y, report.buttons);
            }
        }
    }

    // 关闭usb gadget设备
    if(fd_gadget > 0){
        close(fd_gadget);
    }

    // 关闭触摸屏输入设备
    if(fd_event > 0){
        close(fd_event);
    }

    /* 释放指定的接口 */
    r = libusb_release_interface(handle, hid->interface_number);
    if (0 != r){
        fprintf(stderr, "Error releasing interface.\n");
    }

    exit:
        if(kernelDriverDetached){
            //恢复驱动绑定,否则鼠标不可用
            libusb_attach_kernel_driver(handle, hid->interface_number);
        }

        libusb_close(handle);

    return r;
}

int main(int argc, char *argv[])
{
    char *program = argv[0];
    int option;
    const char *usb_device_name = NULL;  // usb设备名称 /dev/bus/usb/001/005
    const char *gadget_device_name = NULL;  // 模拟usb设备
    libusb_device **devs;
    ssize_t cnt;
    int r, i,fd = 0;
    uint16_t vid=0, pid=0;
    libusb_device_handle *handle = NULL;

    static const struct option options[] = {
        { "vendid", required_argument, NULL, 'v' },
        { "productid", required_argument, NULL, 'p' },
        { "gadgetdevicename", required_argument, NULL, 'o' },
        { "help",  no_argument, NULL, 'h' },
    };

    /* Parse command line options, if any */
    while ((option = getopt_long_only(argc, argv,"hv:p:d:o:",options, NULL))){
        if (option == -1)
            break;
        switch (option) {
        case 'v':
             vid = strtoul(optarg, NULL, 0);
             break;
        case 'p':
            pid = strtoul(optarg, NULL, 0);
            break;
        case 'o':
            gadget_device_name = optarg;
            break;
        case 'h':
            usage(program);
            exit(EXIT_SUCCESS);
            break;
        default:
            printf("ERROR: Invalid command line option\n");
            usage(program);
            exit(EXIT_FAILURE);
        }
    }

    printf("vid:0x%x pid:0x%x gadgetdevicename:%s\n",vid,pid,gadget_device_name);

    // 测试模拟的usb触摸屏设备
    if(gadget_device_name){
        // 打开设备文件
        fd = open(gadget_device_name, O_RDWR, 0666);
        if (fd < 0) {
            printf("Error could not open usb gadget device %s: %s\n", gadget_device_name, strerror(errno));
            return 1;
        }
         close(fd);
    }

    // 初始化libusb库
    r = libusb_init(NULL);
    if (r < 0)
        return r;


	// 获取usb设备列表,保存到devs
	cnt = libusb_get_device_list(NULL, &devs);
	if (cnt < 0) {
		libusb_exit(NULL);
		return 1;
	}

	// 遍历每一个usb设备,如果和指定的vid,pid匹配,输出usb设备信息
	for (i = 0; devs[i]; i++){
        if(device_match_vid_pid(devs[i], vid, pid)){
            print_device(devs[i], handle);
            break;
        }
    }

	// 释放由libusb_get_device_list函数分配的usb设备列表资源
	libusb_free_device_list(devs, 1);

    // 将程序中的SIGINT信号(通常由用户按下 Ctrl+C 产生)交给名为sig_handler的函数处理
    signal(SIGINT, sig_handler);
    // 将程序中的SIGTERM信号(通常由操作系统或其他进程发送给目标进程来请求其正常终止)交给名为sig_handler的函数处理
    signal(SIGTERM, sig_handler);

    // read usb data
    interrupt_data_rw(vid,pid, gadget_device_name);

    libusb_exit(NULL);
    return r;
}
3.1.2 配置解析

该应用程序目前仅支持三款USB触摸屏,其中有我们实验中一直使用的USB触摸屏(PID=e5e3,VID=1a86),这里我将USB触摸屏的HID报告描述符的描述信息保存到了hids数组中;由于我仅仅关注HID报告描述符中第一个触摸点的状态以及坐标等信息,因此上面记录了这些信息在介绍的USB数据包中的索引。

/* 存放不同usb触摸屏hid报告描述符解析后的信息 */
static const struct hid_descriptor_t hids[] = {
    [0] = { 0x1a86, 0xe5e3, 0x82, 52, 8, 3, 4, 5, 6, 0, 0x1000, 0, 0x1000, 0 },
    [1] = { 0x222a, 0x0001, 0x81, 52, 14, 2, 3, 4, 5, 0, 0x0780, 0, 0x0438, 0 },
    [2] = { 0x03eb, 0x8a6e, 0x83, 59, 8, 2, 3, 6, 7, 0, 0x0fff, 0, 0x0fff,1 },
};

其中:

(1) 0x1a86USB设备的idVendor

(2) 0xe5e3USB设备的idProduct

(3) 0x82USB通信的端点地址;

获取USB通信端点地址有两种方法:

方法一:可以通过cat /sys/bus/usb/devices/1-1.2/1-1.2:1.0/查看;

root@SOM-RK3399v2:/# cat /sys/bus/usb/devices/1-1.2/
1-1.2:1.0/           bDeviceProtocol      bNumInterfaces       descriptors          driver/              manufacturer         ep_00/               idProduct            busnum               devpath              idVendor             ......         
root@SOM-RK3399v2:/etc/profile.d# cat /sys/bus/usb/devices/1-1.2/1-1.2\:1.0/  
0003:1A86:E5E3.0007/  bInterfaceNumber      bInterfaceSubClass    driver/               modalias             
authorized            bInterfaceClass       bInterfaceProtocol    bNumEndpoints         ep_82/    
......

这里1-1.2表示的使用的USB触摸屏设备(开发板插入USB设备时,dmesg可以看到1-1.2),该目录下文件:

  • 1-1.2:1.0目录下为USB接口信息,最后一个数字0就是当前的接口编号(bInterfaceNumber);
  • ep_00表示枚举设备时使用的通信端点地址;
  • descriptors存放的USB设备描述符。

如果USB设备下有多个USB接口,则需要挨个查看确定为触摸功能的那个接口;那如何确认哪个接口是触摸功能呢?可以通过查看接口的HID报告描述符信息来确定;

root@SOM-RK3399v2:/# hexdump /sys/bus/usb/devices/1-1.2/1-1.2:1.0/0003:1A86:E5E3.0007/report_descriptor
0000000 0d05 0409 01a1 0185 2209 02a1 4209 0015
0000010 0125 0175 0195 0281 0795 0181 0875 5109
0000020 0195 0281 0105 0026 7510 5510 650e 0911
0000030 3530 4600 0879 0281 0026 4610 054c 3109
0000040 0281 0d05 4809 0281 09c0 a122 0902 1542
0000050 2500 7501 9501 8101 9502 8107 7501 0908
0000060 9551 8101 0502 2601 1000 1075 0e55 1165
0000070 3009 0035 7946 8108 2602 1000 4c46 0905
0000080 8131 0502 090d 8148 c002 2209 02a1 4209
0000090 0015 0125 0175 0195 0281 0795 0181 0875
....

通过分析HID报告描述符,来判断该描述符是不是描述了触摸功能;通过cat /sys/kernel/debug/hid/0003\:1A86\:E5E3.0007/rdesc命令可以获取到解析后的HID报告描述符,具体可以参考https://www.cnblogs.com/zyly/p/17747761.html#_label4_1

0x05, 0x0d,    // USAGE_PAGE (Digitizer)
0x09, 0x04,    // USAGE (Touch Screen)
0xa1, 0x01,    // COLLECTION (Application)  

如果该接口是USB触摸屏功能,那么找到USB接口目录下的ep_xx目录,那么xx就是端口号,比如这里就是82,16进制。

root@SOM-RK3399v2:/# ls /sys/bus/usb/devices/1-1.2/1-1.2:1.0/ | grep "ep"
ep_82

方法二:usb_test应用程序启动时会输出接口所使用的的bEndpointAddress

需要注意的是如果USB通信端点地址错误,可能出现如下错误:

bulk recive error,r:Input/Output Error length:0

(4) 52:USB触摸屏发送的数据包的长度;通过usbmon抓取到的数据包中有USB数据包的长度;

ffffffc0c88dab00 1925775309 C Ii:1:004:2 0:1 52 = 01010075 09080930 00000000 00000000 00000000 00000000 00000000 00000000
                               |__ 输入       |_______ 实际读取的长度 

需要注意的是如果接收缓存区大小设置的小于实际读取的数据包的长度,可能出现如下错误:

bulk recive error,r:Overflow length:0

(5) 8:按键状态在接收到的USB数据包中的索引为1的字节的位[0];

(6) 3:x坐标低8位在接收到的USB数据包中的索引为3;

(7) 4:x坐标高8位在接收到的USB数据包中的索引为4;

(8) 5:y坐标低8位在接收到的USB数据包中的索引为5;

(9) 6:y坐标高8位在接收到的USB数据包中的索引为6;

(10) 0:x坐标逻辑最小值,即接收到的USB数据包中x坐标的最小值;

(11) 0x1000:x逻辑最大值,即接收到的USB数据包中x坐标的最大值;

(12) 0:y坐标逻辑最小值,即接收到的USB数据包中y坐标的最小值;

(13) 0x1000:y逻辑最大值,即接收到的USB数据包中y坐标的最大值;

(14) 0:当前选中的接口编号,对于有些USB设备,可能存在多个接口,该值设置为所使用接口的bInterfaceNumber的值;

如果需要支持其他的USB触摸屏,需要自行获取自己使用的USB触摸屏的HID报告描述符,并按照上面结构体填入相应信息。

编译应用程序:

root@SOM-RK3399v2:/opt/libusb# gcc  -o usb_test usb_test.c  -I/usr/include/libusb-1.0   -lusb-1.0
root@SOM-RK3399v2:/opt/libusb# ls -l usb_test
-rwxr-xr-x 1 root root 25192 Oct 15 10:46 usb_test

3.2 测试

3.2.1 查看USB数据包

首先需要将USB触摸屏(PID=e5e3,VID=1a86)连接到开发板,输入如下命令:

root@SOM-RK3399v2:/opt/libusb# ./usb_test  -v 0x1a86 -p 0xe5e3
vid:0x1a86 pid:0xe5e3 usbdevicename:(null) gadgetdevicename:(null)
Dev (bus 1, device 4): 1A86 - E5E3 speed: 12M
  Manufacturer:              wch.cn
  Product:                   USB2IIC_CTP_CONTROL
  Configuration:
    wTotalLength:            34
    bNumInterfaces:          1
    bConfigurationValue:     1
    iConfiguration:          0
    bmAttributes:            80h
    MaxPower:                32
    Interface:   // USB触摸屏设备接口可能有多个,如果有多个,只关注bInterfaceSubClass=0/1、bInterfaceProtocol=0/2、bInterfaceClass=3的接口
      bInterfaceNumber:      0    // libusb API中需要该编号,用于指定是哪个接口
      bAlternateSetting:     0
      bNumEndpoints:         1
      bInterfaceClass:       3    // hid
      bInterfaceSubClass:    0    // 子类为0
      bInterfaceProtocol:    0    // 协议为0 
      iInterface:            0
      Endpoint:
        bEndpointAddress:    82h  // 端点地址
        bmAttributes:        03h
        wMaxPacketSize:      64
        bInterval:           1
        bRefresh:            0
        bSynchAddress:       0
Kernel Driver Active
Detach Kernel Driver

其中:

  • v:后面是USB设备的idVendor
  • p:后面是USB设备的idProduct

此时如果单个手指点击触摸屏,将会输出接收到的数据包:

receive data:
0x1 0x1 0x0 0x99 0x5 0xcc 0x6 0x30 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x1 0x1 0x1
receive data:
0x1 0x1 0x0 0x99 0x5 0xcc 0x6 0x30 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x1 0x1 0x1
receive data:
0x1 0x1 0x0 0x99 0x5 0xcc 0x6 0x30 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x1 0x1 0x1
receive data:
0x1 0x1 0x0 0x99 0x5 0xcc 0x6 0x30 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x1 0x1 0x1

可以看到每次接收到的数据包含52个字节,在上一篇博客中我们已经说过USB触摸屏(PID=e5e3,VID=1a86ID为1的报告实际读取的数据长度为52个字节:

字节索引 描述
字节0 报告ID(1)
字节1~8 描述第一个触摸点
字节9~16 描述第二个触摸点
字节17~24 描述第三个触摸点
字节25~32 描述第四个触摸点
字节33~40 描述第五个触摸点
字节41~48 描述第六个触摸点
字节49~50 描述检测和更新触摸点状态的时间间隔
字节51 描述触摸点数量

可以看到最后一个字节表示的是同时触摸点的数量,这里为1,因为我是单指按下的。

我们尝试同时6个手指点击触摸屏:

receive data:
0x1 0x1 0x0 0xf 0xd 0x99 0x0 0x30 0x0 0x1 0x1 0xe1 0x8 0xe6 0x3 0x30 0x0 0x1 0x2 0x85 0x2 0x22 0x3 0x30 0x0 0x1 0x3 0x75 0x5 0x77 0x2 0x30 0x0 0x1 0x4 0xc7 0xb 0xb3 0xd 0x30 0x0 0x1 0x5 0x75 0x6 0x55 0x5 0x30 0x0 0x1 0x1 0x6
receive data:
0x1 0x1 0x0 0x33 0xd 0xd5 0x0 0x30 0x0 0x1 0x1 0x0 0x9 0xee 0x3 0x30 0x0 0x1 0x2 0x8a 0x2 0x4c 0x3 0x30 0x0 0x1 0x3 0x61 0x5 0x22 0x3 0x30 0x0 0x0 0x4 0xc7 0xb 0xb3 0xd 0x30 0x0 0x1 0x5 0x70 0x6 0xee 0x5 0x30 0x0 0x1 0x1 0x6

可以看到最后一个字节为6,需要注意的是我使用的USB触摸屏最多支持同时有6个触摸点。

如果需要终止应用程序,输入CTRL + C,同时点击触摸屏即可。

3.2.2 模拟USB触摸

USB触摸屏(PID=e5e3,VID=1a86)连接到开发板,同时通过USB OTG线将USB3.0 Type-C接口连接到PC

此时PC识别到一个USB设备插入,在设备管理器查看该USB设备信息可以看到VIDPID就是我们在hid_keyboard_mouse.sh中设置的。

img

输入如下命令:

root@SOM-RK3399v2:/opt/libusb# ./usb_test  -v 0x1a86 -p 0xe5e3 -o /dev/hidg2
vid:0x1a86 pid:0xe5e3 usbdevicename:(null) gadgetdevicename:/dev/hidg2
Dev (bus 1, device 4): 1A86 - E5E3 speed: 12M
  Manufacturer:              wch.cn
  Product:                   USB2IIC_CTP_CONTROL
  Configuration:
    wTotalLength:            34
    bNumInterfaces:          1
    bConfigurationValue:     1
    iConfiguration:          0
    bmAttributes:            80h
    MaxPower:                32
    Interface:
      bInterfaceNumber:      0
      bAlternateSetting:     0
      bNumEndpoints:         1
      bInterfaceClass:       3
      bInterfaceSubClass:    0
      bInterfaceProtocol:    0
      iInterface:            0
      Endpoint:
        bEndpointAddress:    82h
        bmAttributes:        03h
        wMaxPacketSize:      64
        bInterval:           1
        bRefresh:            0
        bSynchAddress:       0
Kernel Driver Active
Detach Kernel Driver

其中:

  • v:后面是USB设备的idVendor
  • p:后面是USB设备的idProduct
  • o:后面是模拟的USB触摸屏设备;

此时我们点击触摸屏,就会实现控制PC的效果。等价于将USB触摸屏(PID=e5e3,VID=1a86)连接到PC

补充:由于我们使用的触摸屏比较小,而PC的屏幕比较大,因此在触摸屏上移动一小段位置,PC屏幕上鼠标箭头就会移动很多,这里存在一个缩放比例的问题,因此想选中PC上某一图标也是比较麻烦的。为此可以考虑将触摸屏的HDMI接口连接到PC,这样缩放比例就为1,操作起来就会很方便。

测试发现一个问题,双击经常会失灵。因为应用程序程序读取USB触摸屏PID=e5e3,VID=1a86)的数据包,解析后再通过模拟的USB触摸屏设备发送到PC。由于每一个数据包从接收到发送处理的时间间隔较长,在windows系统中当接收到的两次连续点击的数据包时间间隔超过一定时就被当做点击处理了,目前有三种优化思路:

  • 减少应用程序的处理时长,比如屏蔽掉printf函数;
  • 记录上一个触摸点以及发生的时间,与当前触摸点发生时间进行比较,如果小于某个时间间隔,先模拟上一个触摸点的按下和松开(具体实现需要自己调试测试);
  • windows系统:控制面板 --> 硬件和声音 --> 设备和打印机 --> 鼠标 --> 双击速度(设置为最慢);

如果需要终止应用程序,输入CTRL + C,同时点击触摸屏即可。

参考文章

[1] https://github.com/libusb/libusb

[2] Developers will wish to consult the API documentation

[3] Linux模拟触摸滑动以及按下

[4] i.MX6ULL】驱动开发13——电容触摸驱动实践(下)

[5] Linux获取/dev/input目录下的event对应的设备【转】

[6] 利用uinput模拟touchscreen

[7] input subsystem (五) CTP多点触摸协议

posted @ 2023-10-15 19:20  大奥特曼打小怪兽  阅读(137)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步