读懂 HID 报告描述符 (实现全键无冲的键盘 HID 报告描述符)

读懂 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 控制按键的报告描述符写法

从图1 中可以看到,USAGE ID 从 0xE0 到 0xE7 这 8 个按键,映射到了一个字节上,当某一个比特位为1时,代表指定的按键按下,由此,就实现了一字节映射八个按键的功能。

而 0xE0 到 0xE7 这 8 个按键,对应的功能就是上述的八个控制按键,见 HID Usage Table 的 Keyboard/Keypad Page (0x07) 章节(图2):

图x 0xE0 ~ 0xE7 键值映射

由此推理,键盘上 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” 文件

报告描述符:

图3 报告描述符

使用 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 indexUSAGE IDFunc
00xE0 ~ 0xE7LeftControl ~ RightGUI
1NULLConst Value 0
20x04 ~ 0x0B(A & a) ~ (H & h)
30x0C ~ 0x13(I & i) ~ (P & p)
140x5E ~ 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:

图4 键盘测试

八个控制按键没有测试,原因是把控制按键打开之后,电脑上会触发好多快捷键,为了方便测试,特别地把控制按键给关闭了。当然,截屏接也没有测试,原因是触发截屏后,相当于焦点从测试网页中离开了,网页也就无法及时获取键盘的状态。除此之外,所有按键均被按下。

以上描述符只能把 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 生成的报告描述符工程也在本资源中。

  • 9
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值