读懂 HID 报告描述符 (实现全键无冲的键盘 HID 报告描述符)
前言
为什么有些键盘支持全键无冲,而大部分键盘只支持六键无冲?为什么有些鼠标支持横向滚轮,而大部分键盘只支持纵向滚轮?同样是键盘,支持全键无冲的键盘就能卖得更贵一些,但它们的电路并没有什么区别,同样是鼠标,支持横向滚轮的鼠标就能卖到五百多,是普通鼠标的好几倍价格。
支持全键无冲,支持横向滚轮,在于它们的 HID 报告描述符和对应的 HID 报文与普通的键盘鼠标不同,本文将使用 PLUS-F5270 开发板,模拟一个全键无冲键盘,讲解 HID 报告描述符。
报告描述符
报告描述符,用于描述一个报文的结构和用途,从而使 Host 接收到 HID 报文之后,能够按照报告描述符描述的报文格式,来解析报文内容,一般在 USB 设备枚举的时候将报告描述符发送给电脑。
一个报告描述符可以描述多个报文,不同的报文需通过报文中第一个字节所表达的 Report ID 进行区分。当报告描述符中只描述了一个报文格式,且没有描述 Report ID 时,报文中也不会有 Report ID。
报告描述符的结构主要由报文字段的长度,数量,属性 (输入或输出) 决定,而报文的用途则由 HID Usage Table 决定,可见:https://usb.org/document-library/hid-usage-tables-14
报告描述符的 HID Items 很多,但理解下面的内容即可实现键盘鼠标等常见产品的报告描述符:
- USAGE_PAGE,用途页,指定设备的功能
- USAGE,用法索引,设备功能的用法
- USAGE_MINMUM,用法最大值,当一个设备由多个用法时,可指定用法的最大值和最小值,这样可以表达这个区间的所有用法
- USAGE_MAXMUM,用法最小值
- COLLECTION,集合的开始,当设备具有多个数据集合时,需使用 COLLECTION 进行包围,类似于结构体的花括号。
- END_COLLECTION,集合的结束,类似结构体的右花括号。
- INPUT,输入数据的格式(数据/固定值,数组还是数据…)
- OUTPUT,输出数据的格式(数据/固定值,数组还是数据…)
- LOGICAL,数据逻辑值,由最大值和最小值决定
- LOGICAL_MINMUM,逻辑最大值
- LOGICAL_MAXMUN,逻辑最小值
- REPORT_ID,当报告描述符中描述了多个报告时,可使用报告描述符进行区分
- REPORT_SIZE,指定报表数据区域所包含的位数
- REPORT_COUNT,报表中数据域的数目
这里不再讲述每个 Item 的具体字节代表的含义,如果需要解析报告描述符,则可使用这个网站进行解析:
https://www.usbzh.com/tool/usb.html
下面分别以鼠标和键盘为例,讲述它们的报告描述符内容:
假设鼠标的报文如下:
struct mouse_report {
uint8_t left_btn :1;
uint8_t right_btn :1;
uint8_t middle_btn :1;
uint8_t reserved :5;
int8_t x;
int8_t y;
int8_t wheel;
};
则鼠标的报告描述符应按照如下格式:
USAGE_PAGE (Generic Desktop) // 通用桌面用途
USAGE (Mouse) // 功能为鼠标,需要补充具体的功能
COLLECTION (Application) // { Application
USAGE (Pointer) // 指针类型,为什么是这个类型并不清楚
COLLECTION (Physical) // { Physical
USAGE_PAGE (Button)
USAGE_MINIMUM (Button 1)// 3个按键
USAGE_MAXIMUM (Button 3)
LOGICAL_MINIMUM (0) // 每个按键使用只有 0 和 1 两种状态
LOGICAL_MAXIMUM (1)
REPORT_COUNT (3) // 3个按键,五个条目
REPORT_SIZE (1) // 每个条目 1bit 大小
INPUT (Data,Var,Abs) // 输入,变量,绝对值
REPORT_COUNT (1) // 加入 5bit 常量,补齐一字节,相当于 reserved
REPORT_SIZE (5)
INPUT (Cnst,Var,Abs) // 输入,常量,绝对值
USAGE_PAGE (Generic Desktop) // 通用桌面用途
USAGE (X) // X 移动量
USAGE (Y) // Y 移动量
USAGE (Wheel) // 滚轮滚动量
LOGICAL_MINIMUM (-127) // 最小值 -127
LOGICAL_MAXIMUM (127) // 最大值 +127
REPORT_SIZE (8) // 每个报文 8bit 大小
REPORT_COUNT (3) // 一共有3个报文
INPUT (Data,Var,Rel) // 输入,数据,相对值
END_COLLECTION // } Physical
END_COLLECTION // } Application
假设键盘的报文如下:
struct keyboard_report {
uint8_t control_key;
uint8_t reserverd1;
uint8_t keycode[6];
};
struct leds_report {
uint8_t capslock :1;
uint8_t scrolllock :1;
uint8_t numlock :1;
uint8_t compose :1;
uint8_t kana :1;
};
标准的键盘报告描述符中规定,第一个字节表达八个控制按键 (Shift / Control / Alt / GUI,左右各四个,GUI 在 Windows 操作系统下是 Windows 键,Linux 下是 Super 键,Mac 下为 Command 键) 的状态值,每个比特代表一个按键的状态。
第二个字节为保留字节,恒定为0,原因暂不清楚,哪怕是自定义的键盘描述符,也不能拿掉这个字节。
剩下六个字节存储六个键值,也就是说,标准的键盘能够一次发送 8 + 6 = 14 个按键,这也是平常所说的六键无冲。
键盘有状态指示灯,因此还需要接收一个电脑发过来的报文,代表五个指示灯的状态。
标准键盘的描述符如下所示:
USAGE_PAGE (Generic Desktop) // 通用桌面用途
USAGE (Keyboard) // 用法为键盘
COLLECTION (Application) // {
USAGE_PAGE (Keyboard) // 键盘用途
USAGE_MINIMUM (Keyboard LeftControl) // 最小方法为左控制键
USAGE_MAXIMUM (Keyboard Right GUI) // 最大方法为右控制键
LOGICAL_MINIMUM (0) // 逻辑最小值为0
LOGICAL_MAXIMUM (1) // 逻辑最大值为1
REPORT_SIZE (1) // 每个报文的大小为 1bit
REPORT_COUNT (8) // 总共8个报文
INPUT (Data,Var,Abs) // 输入,变量,绝对值
REPORT_COUNT (1) // 8bit 常量
REPORT_SIZE (8)
INPUT (Cnst,Var,Abs) // 输入,常量,绝对值
USAGE_PAGE (Keyboard) // 键盘用途
USAGE_MINIMUM (Reserved (no event indicated)) // 从这个按键开始
USAGE_MAXIMUM (Keyboard Right GUI) // 到这个按键结束,右 224 个按键
LOGICAL_MINIMUM (0) // 逻辑最小值为0
LOGICAL_MAXIMUM (224) //逻辑最大值为 224,实际上逻辑值就是每个按键的 USAGE ID
REPORT_COUNT (6) // 六个报文
REPORT_SIZE (8) // 每个报文大小为 8bit
INPUT (Data,Ary,Abs) // 输入,数组,绝对值
USAGE_PAGE (LEDs) // LED用途
USAGE_MINIMUM (Num Lock) // 这个指示灯开始
USAGE_MAXIMUM (Kana) // 这个指示灯结束,kana 在日文键盘上有这个灯,作用不清楚
REPORT_COUNT (5) // 5个报文
REPORT_SIZE (1) // 每个报文 1bit
OUTPUT (Data,Var,Abs) // 输出,变量,绝对值
REPORT_COUNT (1) // 添加 3bit 常量,补齐1字节
REPORT_SIZE (3)
OUTPUT (Cnst,Var,Abs) // 输出,常量,绝对值
END_COLLECTION // }
这里介绍下学习报告描述符的网站:https://www.usbzh.com/article/detail-775.html
实现全键无冲的 HID 报告描述符
实现思路
标准的键盘报告描述符中规定,第一个字节表达八个控制按键 (Shift / Control / Alt / GUI,左右各四个,GUI 在 Windows 操作系统下是 Windows 键,Linux 下是 Super 键,Mac 下为 Command 键) 的状态值,每个比特代表一个按键的状态。
因此,在报告描述符中,它的写法见图1:
从图1 中可以看到,USAGE ID 从 0xE0 到 0xE7 这 8 个按键,映射到了一个字节上,当某一个比特位为1时,代表指定的按键按下,由此,就实现了一字节映射八个按键的功能。
而 0xE0 到 0xE7 这 8 个按键,对应的功能就是上述的八个控制按键,见 HID Usage Table 的 Keyboard/Keypad Page (0x07) 章节(图2):
由此推理,键盘上 104 个按键,如果通过这种方法,最少可以使用 104/8 = 13个字符,来表达按键状态。
但在实际测试的时候发现,键盘报文的第二个字节,必须保留为常量,否则电脑不处理这个报文,至于为什么,在编写本文时话没有特别追溯。因此,最少需要14个字节的报文来表达键盘按键状态,全键盘中需要使用到 USAGE ID 从 0x04 到 0x65 的键值,其中 0x32 (Keyboard Non-US # and ˜) 和 0x64 (Keyboard Non-US \and |) 虽然没有用到,但在这个范围内,因此实际需要 15 个字节才能表达完整的按键状态。
实现代码
如果有人直接通过手写数组来实现报告描述符,我愿称他为大神。
报告描述符较为复杂,可使用工具生成,usb-if 官网提供了相应的工具,可在 https://www.usb.org/hid 最下面的 Tools 中找到相应的软件,例如 Waratah,但这个软件的相关资料太少,一时半会没用起来,但最下面有一个 Deprecated Links and Tools 链接,点击进去之后 (https://usb.org/deprecated-links-and-tools) ,提供了一个 HID Descriptor Tool 工具 (https://usb.org/document-library/hid-descriptor-tool) 却十分好用。
HID Descriptor Tool 还提供了许多 HID 报告描述符的模板,有助于学习 HID 报告描述符。
打开这个软件,然后在左侧选择相应的 HID Items,选择和填写相应的信息,右侧即可生成相应的描述符内容,编好描述符后,点击 File->Save As,保存类型选择为 “.h” 文件
报告描述符:
使用 HID Descriptor Tool 软件生成代码:
char ReportDescriptor[73] = {
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)
0x95, 0x08, // REPORT_COUNT (8)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x04, // USAGE_MINIMUM (Keyboard a and A)
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x62, // REPORT_COUNT (98)
0x75, 0x01, // REPORT_SIZE (1)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x06, // REPORT_SIZE (6)
0x81, 0x03, // INPUT (Cnst,Var,Abs)
0x05, 0x08, // USAGE_PAGE (LEDs)
0x19, 0x01, // USAGE_MINIMUM (Num Lock)
0x29, 0x05, // USAGE_MAXIMUM (Kana)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x95, 0x05, // REPORT_COUNT (5)
0x75, 0x01, // REPORT_SIZE (1)
0x91, 0x02, // OUTPUT (Data,Var,Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x03, // REPORT_SIZE (3)
0x91, 0x03, // OUTPUT (Cnst,Var,Abs)
0xc0 // END_COLLECTION
};
报文格式:
共 15 个字节,每字节的每个比特代表一个 USAGE ID,例如 0 号字节的 bit0 代表 E0 号按键是否按下,bit1 代表 E1 号按键是否按下,按下为1,松开为0。
byte index | USAGE ID | Func |
---|---|---|
0 | 0xE0 ~ 0xE7 | LeftControl ~ RightGUI |
1 | NULL | Const Value 0 |
2 | 0x04 ~ 0x0B | (A & a) ~ (H & h) |
3 | 0x0C ~ 0x13 | (I & i) ~ (P & p) |
… | … | … |
14 | 0x5E ~ 0x6F | (Keypad 6) ~ (Keyboard Application) |
在 MindSDK 上下载一个 PLUS-F5270 TinyUSB HID 键盘的样例,然后进行修改。
首先实现一个按键功能,
打开 tud_usb_descriptors.c,把生成数组内容转移到 desc_hid_report[] 种,删掉数组种原有的内容。
到main.c 中,编写发送报文的代码:
bool keycode_has_send = false;
while (1)
{
tud_task();
if (!board_btn_presserd())
{
if (keycode_has_send)
{
GPIO_SetBits(GPIOD, GPIO_PIN_2);
keycode_has_send = false;
uint8_t report_buf[15];
memset(report_buf, 0x00, sizeof(report_buf));
tud_hid_report(0, report_buf, sizeof(report_buf));
}
}
else if (!keycode_has_send)
{
keycode_has_send = true;
GPIO_ClearBits(GPIOD, GPIO_PIN_2);
uint8_t report_buf[15];
memset(report_buf, 0xFF, sizeof(report_buf));
report_buf[0] = 0x00;
report_buf[1] = 0x00;
report_buf[10] = 0xFB;
tud_hid_report(0, report_buf, sizeof(report_buf));
}
}
这里报文格式为自定义格式,不能使用 TinyUSB 自带 tud_hid_keyboard_report() 函数发送键盘报文,所以使用 tud_hid_report() 发送报文,这段代码中,当按键按下之后,除八个控制按键,以及截屏键之外所有的按键,全部按下,松开按键后,释放所有按键。
测试效果
打开键盘测试网页https://www.zfrontier.com/lab/keyboardTester,将 PLUS-F5270 通过 MCU-USB1 接口连接至电脑,按下按键,测试结果如图x:
八个控制按键没有测试,原因是把控制按键打开之后,电脑上会触发好多快捷键,为了方便测试,特别地把控制按键给关闭了。当然,截屏接也没有测试,原因是触发截屏后,相当于焦点从测试网页中离开了,网页也就无法及时获取键盘的状态。除此之外,所有按键均被按下。
以上描述符只能把 HID Usage Table 的 Keyboard/Keypad Page (0x07) 章节的前 98 个按键和 8 个控制按键给定义出来,部分键盘支持音量,屏幕亮度调节,多媒体功能的功能就需要修改报告描述符来实现了,例如加入报告 ID,实现多个报文,在另一个报文中加入多媒体控制功能。
部分电脑的 BIOS 或嵌入式系统可能无法处理复杂的 HID 的报告描述符,而只认识固定的键盘报文,为了保证键盘的兼容性,在设计键盘的报告描述符时,还需要考虑报文的前八个字节 (不包含Report ID) 能够与标准键盘报文进行兼容,这样才能保证键盘能在各个平台上使用,不过,在抓取某C开头品牌机械键盘的报告描述符时发现,该品牌也是使用这本文的方式来实现全键无冲,下面是抓取到的该品牌 MX 3.0s 机械键盘的报告描述符部分内容:
char ReportDescriptor[] =
{
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x05, 0x07, // Usage Page (Kbrd/Keypad)
0x19, 0xE0, // Usage Minimum (0xE0)
0x29, 0xE7, // Usage Maximum (0xE7)
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,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x07, // Usage Page (Kbrd/Keypad)
0x19, 0x04, // Usage Minimum (0x04)
0x29, 0x94, // Usage Maximum (0x94)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x90, // Report Count (144)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x85, 0x08, // Report ID (8)
0x05, 0x08, // Usage Page (LEDs)
0x19, 0x01, // Usage Minimum (Num Lock)
0x29, 0x05, // Usage Maximum (Kana)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x91, 0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x01, // Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x85, 0x09, // Report ID (9)
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x0A, 0x01, 0xFF, // Usage (0xFF01)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x1D, // Report Count (29)
0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
......
};
从描述符中可以看出,该键盘将键值 0x04 (A & a) 到 0x94 (Keyboard LANG5) 映射到了 144 个比特位上 (这里似乎是一个 bug,0x04 ~ 0x94 一共 145 个键值,应该映射到 145 个比特位上) ,每个比特位代表一个按键的状态,由此发送 18 + 2 = 20 个字节,即可实现全键无冲的功能,经过测试,Windows,Ubuntu 和 MacOS 甚至 TinyUSB Host HID 都能使用该键盘。
该工程可见本文绑定的资源,同时使用 HID Descriptor Tool 生成的报告描述符工程也在本资源中。