USB设备开发学习(一)

最近有个相关需求,所以准备开始研究一下,怎么开发一个USB设备。本篇内容为我个人的一个学习总结,都是一个非常基础的内容。

如何上手

首先Google搜了一下有没有相关学习资料,搜到一本书——《圈圈教你玩USB》,搞到书了以后发现,需要买硬件设备,不过我认为这属于后期的内容了。

前期还是考虑拿我手头上有的设备来进行学习研究,第一个考虑的是手机,但是研究了一下,发现安卓对这块的支持不是很好,再加上安卓又包装了一层,这会让我不能很好的理解USB底层的细节。

因为我手头上正好有树莓派4b,所以又考虑使用树莓派4b来进行USB设备开发,进过搜索发现有一个项目能作为我的着手点:https://github.com/mtlynch/key-mime-pi

随后通过该项目来开始进行USB设备开发的学习研究。

工具

经过一番研究,最后还是选择Windows作为USB主机,因为Windows上的USB工具可视化做的更好。

USB Tree View

第一个工具是USB Tree View,能很好的展示主机上USB设备树的主从情况,USB描述信息等数据。

image-20240108190613077

可以从上面的USB设备树中看出,一台主板最底层的是主机控制器直连CPU,然后上层是USB根集线器,从中文名看不出来是啥,如果翻译成英文其实就是HUB,再类比一下就是USB扩展坞。比如在第一个主机控制器上,有一个通用USB集线器,其实就是我台式机上机箱的USB口,在机箱上有个USB HUB的芯片连接到主板的USB3.0接口。

并且点击已经连接上的设备,还可以查看该USB设备更详细的信息:

image-20240108192911485

具体的信息代表什么意思,之后再说,使用这个工具主要是能很方便的查看USB设备的详细信息。不过也有BUG,比如下图,改工具无法解析HID信息:

image-20240108193107383

Wireshark

wireshark装上USBPcap,能够抓取主机控制器上的USB流量,比如我电脑有三个主机控制器,所以就显示有三个USBPcap:

image-20240108193240595

如果不知道自己插的USB口属于哪个主机控制器,就可以使用USBTree View查看。不过由于Wireshark是抓取主机控制器上的流量,所以当我要研究单个USB设备的时候,需要通过表达式把该主机控制器上的其他设备给过滤掉。

Bus Hound

这个设备可以抓取指定USB设备的流量,不过缺点就是要花钱,要不然抓包size和数量都有限制。暂时没找到该软件的破解版,而且暂时也没有必须要使用这个软件的原因。下面是该软件的界面:

image-20240108193731353

使用树莓派4b作为PC的USB键盘

接下来就是通过key-mime-pi项目来学习USB设备的开发,简单看了下该项目,发现使用树莓派4b作为一个USB设备非常容易。

  1. 需要开启dwc2驱动:在树莓派的config.txt中添加dtoverlay=dwc2,设备启动后,确认一下:
1
2
$ lsmod|grep dwc2
dwc2 196608 0
  1. 运行下面这个脚本:
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
#!/usr/bin/env bash

# Adapted from https://github.com/girst/hardpass-sendHID/blob/master/README.md

# Exit on first error.
set -e

# Treat undefined environment variables as errors.
set -u

modprobe libcomposite

cd /sys/kernel/config/usb_gadget/
mkdir -p g1
cd g1

echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2

STRINGS_DIR="strings/0x409"
mkdir -p "$STRINGS_DIR"
echo "6b65796d696d6570690" > "${STRINGS_DIR}/serialnumber"
echo "keymimepi" > "${STRINGS_DIR}/manufacturer"
echo "Generic USB Keyboard" > "${STRINGS_DIR}/product"

FUNCTIONS_DIR="functions/hid.usb0"
mkdir -p "$FUNCTIONS_DIR"
echo 1 > "${FUNCTIONS_DIR}/protocol" # Keyboard
echo 0 > "${FUNCTIONS_DIR}/subclass" # No subclass
echo 8 > "${FUNCTIONS_DIR}/report_length"
# Write the report descriptor
# Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.html
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_DIR}/report_desc"

CONFIG_INDEX=1
CONFIGS_DIR="configs/c.${CONFIG_INDEX}"
mkdir -p "$CONFIGS_DIR"
echo 250 > "${CONFIGS_DIR}/MaxPower"

CONFIGS_STRINGS_DIR="${CONFIGS_DIR}/strings/0x409"
mkdir -p "$CONFIGS_STRINGS_DIR"
echo "Config ${CONFIG_INDEX}: ECM network" > "${CONFIGS_STRINGS_DIR}/configuration"

ln -s "$FUNCTIONS_DIR" "${CONFIGS_DIR}/"
ls /sys/class/udc > UDC

chmod 777 /dev/hidg0

经过研究发现利用的是USB gadget驱动,USB的gadget驱动可以通过在/sys/kernel/config/usb_gadget/目录下创建目录/删除目录来控制USB设备的绑定与解绑。
后面有需要可以去看看这部分的代码,位于Linux内核的:linux/drivers/usb/dwc2linux/drivers/usb/gadget

通过流量在学习理解USB协议

首先讲讲一个前置的知识点:USB一定会分为主机和设备的,并且通信永远都是由主机先发起的。

在我的环境下,首先打开Wireshark,开始捕获USBPcap1的流量,然后把树莓派的type-c口(电源供电口)连接到主机的USB口上,这个时候主机并不会识别树莓派为USB设备,仅供电,所以这个时候树莓派开始启动。

然后开始把wireshark能抓到的流量地址都给过滤掉,这些都不属于树莓派的USB流量。

image-20240108195656706

然后在树莓派上以root权限运行上面的脚本,然后再查看wireshark抓到的流量:

image-20240108195844733

使用USBPcap抓到的usb流量地址一般为:x.y.z,x为USB总线(Bus)号,一般为主机控制器的编号,我抓到的USBPcap1上的流量,x的值都为1。y为设备号,指向该Bus上的某个设备,但是有个问题USBPcapUSBTree View对于设备编号的方法不一样,USBPcap是按照顺序来的,USBTree View一开始已经对所有USB接口编号号了,不管你插没插上USB设备。

不过按我理解,USBPcap可能更符合底层实际情况,因为它不是自己编的号,而是解析了抓到的流量。

z的值表示的是端点号(Endpoint),我觉得有点像一个程序的文件描述符(fd),USB主机和设备间就是通过端点号来进行通信的,当USB设备还未在主机上注册时,默认使用0端点号来进行通信(有点像我还没获取到ip的时候,DHCP请求包是从广播地址发出去的?)。

设备描述符

设备描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* USB_DT_DEVICE: Device descriptor */
struct usb_device_descriptor {
__u8 bLength;
__u8 bDescriptorType;

__le16 bcdUSB;
__u8 bDeviceClass;
__u8 bDeviceSubClass;
__u8 bDeviceProtocol;
__u8 bMaxPacketSize0;
__le16 idVendor;
__le16 idProduct;
__le16 bcdDevice;
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;
} __attribute__ ((packed));

USB协议有好几个结构体,结构体每个字段的含义可以参考:https://zhuanlan.zhihu.com/p/558716468

然后查看USBPcap上的结果:

image-20240108201442949

再对比一下USBTree View上的详情:

image-20240108201600746

然后能发现,控制这个描述符的代码是下面这几行:

1
2
3
4
5
6
7
8
9
10
echo 0x1d6b > idVendor  # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2

STRINGS_DIR="strings/0x409"
mkdir -p "$STRINGS_DIR"
echo "6b65796d696d6570690" > "${STRINGS_DIR}/serialnumber"
echo "keymimepi" > "${STRINGS_DIR}/manufacturer"
echo "Generic USB Keyboard" > "${STRINGS_DIR}/product"

主要就是定义了设备的协议(USB2.0),然后是供应商和产品的ID,bNumConfigurations配置描述符数量,剩下的都是一些标识信息。

设备协议应该影响到后续的协议结构体,还有传输的最高速度。供应商信息大部分情况下就是作为标识信息作用的,但是我后面看到有些驱动可能会识别指定的供应商和产品ID。目前只是想模拟一下鼠标/键盘这种东西,这些ID是可以随便改的,大不了就是电脑显示是未知设备,但是不影响使用。

然后就是定义了配置描述符的数量,虽然主机将会向设备请求指定数量的配置描述符。

配置描述符

设备描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
struct usb_config_descriptor {
__u8 bLength;
__u8 bDescriptorType;

__le16 wTotalLength;
__u8 bNumInterfaces;
__u8 bConfigurationValue;
__u8 iConfiguration;
__u8 bmAttributes;
__u8 bMaxPower;
} __attribute__ ((packed));

然后查看USBPcap上的结果:

image-20240108202748447

一开始都是请求固定长度为9的配置描述符。

image-20240108202826647

然后设备描述符的wTotalLength字段会告诉主机,配置描述符的实际长度,接着主机就会读取完整的配置描述符:

image-20240108202922061

image-20240108202948118

从USBPcap的流量中可以发现,在配置描述符的响应包里,除了配置描述符的信息,还包含了接口描述符,端点描述符,应该注册的是一个USB HID设备,所以还包含着HID描述符。

image-20240108203133252

image-20240108203339732

在这里面一个是可以控制电源的相关数据,设备是可以自供电还是需要USB设备供电,设备是否可以通过USB远程唤醒,然后就是设备能接受的最高电流。(不确定现在的主板能控制电流控制的这么精细?不过也只是最大电流,也许吧?)

然后主要就是获取设备的接口描述符的数量,去请求获取设备的接口描述符,还有一个是bConfigurationValue,该值用在Set Configuration Request里,具体的作用还不知晓。(告诉设备主机接受并注册了该配置?)

控制该部分的代码如下:

1
2
3
4
5
6
7
CONFIG_INDEX=1
CONFIGS_DIR="configs/c.${CONFIG_INDEX}"
mkdir -p "$CONFIGS_DIR"
echo 250 > "${CONFIGS_DIR}/MaxPower"
CONFIGS_STRINGS_DIR="${CONFIGS_DIR}/strings/0x409"
mkdir -p "$CONFIGS_STRINGS_DIR"
echo "Config ${CONFIG_INDEX}: ECM network" > "${CONFIGS_STRINGS_DIR}/configuration"

除了电源属性的配置,好像也没有其他需要我们修改控制的字段了。(电源属性一般也不需要改。)

接口描述符

接口描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* USB_DT_INTERFACE: Interface descriptor */
struct usb_interface_descriptor {
__u8 bLength;
__u8 bDescriptorType;

__u8 bInterfaceNumber;
__u8 bAlternateSetting;
__u8 bNumEndpoints;
__u8 bInterfaceClass;
__u8 bInterfaceSubClass;
__u8 bInterfaceProtocol;
__u8 iInterface;
} __attribute__ ((packed));

然后查看USBPcap上的结果:

image-20240108204446080

再查看USBTree View上的信息:

image-20240108204611197

在接口描述符中,主要定义的是设备的类型和端点的数量,在USB协议中,端点的通信是单向的(更向Linux的文件描述符了),所以在这里定义了两个端点描述符,一个表示的是输入,一个表示的是输出。

另一个就是标识接口的类型,在这里标识的是一个HID设备,所以主机会再去读取HID描述符。并且定义了接口协议为键盘,这样就会让键盘的HID驱动来处理该USB的通信。

接口类型可以看Linux的代码中定义的有以下几类(同样是在ch9.h中定义的):

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
/*
* Device and/or Interface Class codes
* as found in bDeviceClass or bInterfaceClass
* and defined by www.usb.org documents
*/
#define USB_CLASS_PER_INTERFACE 0 /* for DeviceClass */
#define USB_CLASS_AUDIO 1
#define USB_CLASS_COMM 2
#define USB_CLASS_HID 3
#define USB_CLASS_PHYSICAL 5
#define USB_CLASS_STILL_IMAGE 6
#define USB_CLASS_PRINTER 7
#define USB_CLASS_MASS_STORAGE 8
#define USB_CLASS_HUB 9
#define USB_CLASS_CDC_DATA 0x0a
#define USB_CLASS_CSCID 0x0b /* chip+ smart card */
#define USB_CLASS_CONTENT_SEC 0x0d /* content security */
#define USB_CLASS_VIDEO 0x0e
#define USB_CLASS_WIRELESS_CONTROLLER 0xe0
#define USB_CLASS_PERSONAL_HEALTHCARE 0x0f
#define USB_CLASS_AUDIO_VIDEO 0x10
#define USB_CLASS_BILLBOARD 0x11
#define USB_CLASS_USB_TYPE_C_BRIDGE 0x12
#define USB_CLASS_MISC 0xef
#define USB_CLASS_APP_SPEC 0xfe
#define USB_CLASS_VENDOR_SPEC 0xff

#define USB_SUBCLASS_VENDOR_SPEC 0xff

Linux内置的HID驱动协议,我看源码里好像只定义了鼠标和键盘,其他设备要额外下载驱动?待后续研究。

image-20240108205640292

端点描述符

端点描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* USB_DT_ENDPOINT: Endpoint descriptor */
struct usb_endpoint_descriptor {
__u8 bLength;
__u8 bDescriptorType;

__u8 bEndpointAddress;
__u8 bmAttributes;
__le16 wMaxPacketSize;
__u8 bInterval;

/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));

查看USBPcap信息:

image-20240108210312040

主要是定义了端口号,和该端口的方向,还有使用该端口进行通信的方式(中断传输),通信数据包的最大尺寸(8)字节。

我查看了下,Linux Gadget目录下的文件,好像没有控制端点信息的文件:

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
$ tree
.
├── bcdDevice
├── bcdUSB
├── bDeviceClass
├── bDeviceProtocol
├── bDeviceSubClass
├── bMaxPacketSize0
├── configs
│   └── c.1
│   ├── bmAttributes
│   ├── hid.usb0 -> ../../../../usb_gadget/g1/functions/hid.usb0
│   ├── MaxPower
│   └── strings
│   └── 0x409
│   └── configuration
├── functions
│   └── hid.usb0
│   ├── dev
│   ├── no_out_endpoint
│   ├── protocol
│   ├── report_desc
│   ├── report_length
│   └── subclass
├── idProduct
├── idVendor
├── max_speed
├── os_desc
│   ├── b_vendor_code
│   ├── qw_sign
│   └── use
├── strings
│   └── 0x409
│   ├── manufacturer
│   ├── product
│   └── serialnumber
└── UDC

就一个no_out_endpoint文件,如果置为1,端点描述符可能就只有一个了。另外猜测report_length文件应该是控制数据包的最大长度。

字符串描述符

还有一个字符串描述符,也是位于ch9.h

1
2
3
4
5
6
7
struct usb_string_descriptor {
__u8 bLength;
__u8 bDescriptorType;

__le16 wData[1]; /* UTF-16LE encoded */
} __attribute__ ((packed));

主要作用还是标识信息,在wireshark我们没抓到字符串描述符,我认为应该是默认情况下,驱动不会请求字符串描述符,应为没啥用,这些信息是给人看的,机器不需要。只要当我们打开设备管理器(或者USBTree View),要查看这个设备的信息的时候,才会获取该描述符。

可以在USBTree View中查看:

image-20240108211105540

比如在接口描述符中,iInterface字段的值就是字符串描述符的偏移。

HID报告描述符

当设备通过接口描述符得知该设备是HID设备时,将会再获取HID报告描述符:

image-20240108211340446

HID报告描述符在代码中的位置:

1
2
3
4
# Write the report descriptor
# Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.html
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_DIR}/report_desc"

该值的含义在注视里写了,从https://www.kernel.org/doc/html/latest/usb/gadget_hid.html网站获取的:

image-20240108211622029

所以在这里我们还得在学学,理解一下HID的报告描述符,完整的文档可以参考:https://usb.org/sites/default/files/hut1_4.pdf,官方文档,优点是齐全,缺点是太多了,对于我们新手来说,不太适合,我认为比较适合上手了以后作为参考手册。

解析HID配置描述符

首先,是作为新手的我来参试解析一下HID的配置描述符。

我们先看看key-mime-pi项目的通信代码,树莓派告诉主机按了什么键:

1
2
3
4
5
6
7
def send(hid_path, control_keys, hid_keycode):
with open(hid_path, 'wb+') as hid_handle: # hid_path = "/dev/hidg0"
buf = [0] * 8
buf[0] = control_keys
buf[2] = hid_keycode
hid_handle.write(bytearray(buf))
hid_handle.write(bytearray([0] * 8))

需要发送两个8字节的buf,这个可以理解,因为在端点描述符里限制了最大的包大小为8字节。第二个buf的8字节全都置为0,第一个buf的第一个字节为控制字符,经过研究设置了8个控制字符:

1
2
3
4
5
6
7
8
#define KEY_LEFTCTRL 0xe0 // Keyboard Left Control
#define KEY_LEFTSHIFT 0xe1 // Keyboard Left Shift
#define KEY_LEFTALT 0xe2 // Keyboard Left Alt
#define KEY_LEFTMETA 0xe3 // Keyboard Left GUI
#define KEY_RIGHTCTRL 0xe4 // Keyboard Right Control
#define KEY_RIGHTSHIFT 0xe5 // Keyboard Right Shift
#define KEY_RIGHTALT 0xe6 // Keyboard Right Alt
#define KEY_RIGHTMETA 0xe7 // Keyboard Right GUI

GUI应该就是windows的win键,mac的command。

第一个buf的第二个字节不设置,默认为0,第三字节到第8字节,有6个字节,为输入的按键。

大致了解了一下,我们是如何输入的,然后再来看看HID的报告描述符:

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
static struct hidg_func_descriptor my_hid_data = {
.subclass = 0, /* No subclass */
.protocol = 1, /* Keyboard */
.report_length = 8,
.report_desc_length = 63,
.report_desc = {
0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */
0x09, 0x06, /* USAGE (Keyboard) */
0xa1, 0x01, /* COLLECTION (Application) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x95, 0x08, /* REPORT_COUNT (8) */
0x81, 0x02, /* INPUT (Data,Var,Abs) */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
0x95, 0x05, /* REPORT_COUNT (5) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x05, 0x08, /* USAGE_PAGE (LEDs) */
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
0x91, 0x02, /* OUTPUT (Data,Var,Abs) */
0x95, 0x01, /* REPORT_COUNT (1) */
0x75, 0x03, /* REPORT_SIZE (3) */
0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
0x95, 0x06, /* REPORT_COUNT (6) */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */
0xc0 /* END_COLLECTION */
}
};

首先是USAGE_PAGEUSAGE,这两个字段可以看参考文档,都是目前我们只能选取定义好的那些应用。暂时还不知道是否影响驱动的识别。

主要看集合部分的内容,集合以COLLECTION开始END_COLLECTION结束。

第一部分:

1
2
3
4
5
6
7
8
0x05, 0x07,     /*   USAGE_PAGE (Keyboard)                */
0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */
0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x01, /* LOGICAL_MAXIMUM (1) */
0x75, 0x01, /* REPORT_SIZE (1) */
0x95, 0x08, /* REPORT_COUNT (8) */
0x81, 0x02, /* INPUT (Data,Var,Abs) */

按我的理解,这部分的含义如下:

指定键盘功能,最小值为Keyboard LeftControl(0xe0),最大值为Keyboard Right GUI(0xe7)。逻辑最小值为0,最大值为1,1为按下,0为没有按下。一个按键占1bit,有8个按键,一共占1字节。比如:0b00000001就表示LeftControl键被按下了。从这看,我们可以一次性把8个控制键都按下。

第二部分:

0x95, 0x01,     /*   REPORT_COUNT (1)                     */
0x75, 0x08,     /*   REPORT_SIZE (8)                      */
0x81, 0x03,     /*   INPUT (Cnst,Var,Abs)                 */

有8个1bit的值,总共1字节,并且是常量(Cnst全程是constant),上下文没看到有设置值,所以认为是默认值0。(暂时也不确定设置为其他值有没有影响。)

第三部分:

0x95, 0x05,     /*   REPORT_COUNT (5)                     */
0x75, 0x01,     /*   REPORT_SIZE (1)                      */
0x05, 0x08,     /*   USAGE_PAGE (LEDs)                    */
0x19, 0x01,     /*   USAGE_MINIMUM (Num Lock)             */
0x29, 0x05,     /*   USAGE_MAXIMUM (Kana)                 */
0x91, 0x02,     /*   OUTPUT (Data,Var,Abs)                */
0x95, 0x01,     /*   REPORT_COUNT (1)                     */
0x75, 0x03,     /*   REPORT_SIZE (3)                      */
0x91, 0x03,     /*   OUTPUT (Cnst,Var,Abs)                */

定义了一个LED功能,总共占1字节,高3bit为常量0,低5bit估计表示的是小键盘灯,大小写灯这些键盘上的灯,并且是由主机发送给设备的,主机可以主动控制键盘灯的亮灭。

第四部分:

1
2
3
4
5
6
7
8
0x95, 0x06,     /*   REPORT_COUNT (6)                     */
0x75, 0x08, /* REPORT_SIZE (8) */
0x15, 0x00, /* LOGICAL_MINIMUM (0) */
0x25, 0x65, /* LOGICAL_MAXIMUM (101) */
0x05, 0x07, /* USAGE_PAGE (Keyboard) */
0x19, 0x00, /* USAGE_MINIMUM (Reserved) */
0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */
0x81, 0x00, /* INPUT (Data,Ary,Abs) */

定义了键盘功能,按键的值从0-0x65,一共有102个键,逻辑值也是从0-0x65,一个按键占1字节,有6个按键,总共占6字节。

到这里键盘的HID报告描述符的定义就分析完了,我们发现该定义的含义和我们的输入能吻合。

发送的buf第一字节就是表示8个控制按键,第二字节固定为0,后面6个字节为输入按键。

(到这我就在想,键盘只能同时按6个键?不过在一个流量包里的6个字节仍然有顺序,又想想好像无所谓,好像并不影响同时按几个按键,毕竟有顺序嘛。只影响速度?)

剩下还有两个个问题:第一,我的键盘是104键的键盘,为啥有102+8=110个值?经过研究发现:

1
2
3
4
5
6
7
8
9
10
#define KEY_NONE 0x00 // No key pressed
#define KEY_ERR_OVF 0x01 // Keyboard Error Roll Over - used for all slots if too many keys are pressed ("Phantom key")
// 0x02 // Keyboard POST Fail
// 0x03 // Keyboard Error Undefined

#define KEY_102ND 0x64 // Keyboard Non-US \ and |
#define KEY_BACKSLASH 0x31 // Keyboard \ and |
// 上面这两个冲突了,多了一个
#define KEY_HASHTILDE 0x32 // Keyboard Non-US # and ~
// 估计我键盘没有Non-US相关的键

这么算一下,刚好104键的键盘。

第二个问题:为啥还要发送一个全为0的数据包,经过研究我认为是在系统的内存上,映射了8字节的内存给键盘,系统一直重复获取键盘这8字节的内容,键盘只是控制这8字节的内存数据的值。所以当我们输入值的时候,修改了内存之后,还需要把内存值修改回0(表示松开按键),要不然系统下一次扫描该内存段,会认为我们还在按压该按键。

简单点说,就是第一次发送8字节的buf,表示告诉系统,我们按下了哪些按键,第二次发送8字节的buf,表示我们松开了按键。

下一步研究方向

接下来就考虑以下几个方向的研究:

  1. 微调HID报告描述符,看看对实际使用有什么影响。
  2. 通过修改接口描述符字段和HID报告描述符字段,来模拟一个鼠标。
  3. 研究一下手柄,讲道理手柄也是使用HID协议,但是Linux的代码里没看到相关定义。
  4. 研究非HID协议,比如U盘,网卡,打印机这些。
  5. 研究驱动系统细节,在Linux内核的drivers目录下,可以搜索module_usb_driver字符串,这是一个宏定义函数,usb主机端的驱动都是通过该函数注册到内核当中的。

USB设备开发学习(一)

https://nobb.site/2024/01/08/0x84/

Author

Hcamael

Posted on

2024-01-08

Updated on

2024-01-25

Licensed under