简介:《USB HID Usage Tables 1.3》是由USB Implementers' Forum发布的人机交互设备使用表规范,覆盖1996年至2022年的全部更新,面向硬件工程师、固件与驱动开发人员、系统集成商,用来统一键盘、鼠标、触摸屏、游戏控制器等设备的功能标识符,使各类操作系统能正确解析输入,保障跨平台兼容性。资源包为1个PDF文件,大小4.09MB,内容完整且便于离线阅读、检索与打印。已有558人学习下载。文档不仅按通用控件、桌面设备、游戏设备、消费设备等类别定义了Usage Page与Usage ID,还细致说明报告描述符结构、数据报告传输方式、启动协议及设备类定义,并收入了USB显示器控制、销售点终端等专项用途和HUT审查成果。开发者据此可快速定位按键、按钮、传感器等元素的标准用法,减少驱动适配工作量,是HID设备设计、调试与标准化测试不可或缺的权威参考资料。
1. USB HID Usage Tables 1.3 是外设固件最容易漏看的一本“字典”
先讲一个我踩过的坑:某个模拟项目X 的固件里,报告描述符反编译出来没有任何红黄警告,鼠标移动也正常,但三个按键怎么按系统都没反应。查了两天,最后发现是按钮集合里把 Usage 指向了 Generic Desktop 页的X,而不是 Button 页的按钮,主机的 HID 驱动把这三个 bit 当成了坐标增量。这种问题不看《USB HID Usage Tables 1.3》很难定位,因为规范本身不教你封装协议,它只回答一个关键问题:这串字节里的每一位,到底代表哪个语义。
《USB HID Usage Tables 1.3》是标准组织维护的使用表文档,和 HID 协议规范配套使用。它把鼠标、键盘、消费类按键、触摸屏、传感器、电源管理这些常见设备的使用项统一编成“Page + Usage ID”的词典,固件按这张表填描述符,操作系统和驱动才能把报告解读成“鼠标左键”“音量加”“触控点坐标”。没有它,外设开发就是闭眼调参的黑匣子。
这篇笔记适合三类人:写 USB 外设固件的嵌入式开发者、做主机端 HID 解析的驱动工程师、以及做设备兼容性测试的硬件测试。后面我会从怎么读表讲起,给出可直接抄的鼠标、键盘、消费按键描述符,再讲自定义 Usage 的边界和最容易翻车的几个坑。
2. 读懂 Usage Tables 的坐标系:Page、Usage ID 与 Usage Type 决定一个报告的意图
2.1 不要把 Page 当成“第几页”:它是 Usage 的高 16 位命名空间
很多新手第一次看到Usage Page会以为是“文档第几页”,这是个很大的误解。在 HID 里,一个完整的使用项是Usage Page(使用页) + Usage ID(使用 ID)组成的 32 位命名空间,前面是高 16 位,后面是低 16 位。报告描述符里经常用0x05, 0x01切到 Generic Desktop 页,再用0x09, 0x02声明 Mouse,其实就是在说“我取 0x0001 页的 0x0002 号”。
《USB HID Usage Tables 1.3》把已经分配好的页面按主题组织,常见的有下面几个:
| Page 值 | 页面名 | 典型用途 |
|---|---|---|
| 0x01 | Generic Desktop | 鼠标、键盘、指针、X/Y 轴、多轴控制器 |
| 0x02 | Simulation | 赛车方向盘、飞行摇杆、踏板 |
| 0x07 | Keyboard/Keypad | 键盘按键、修饰键、LED 指示灯 |
| 0x08 | LED | Scroll Lock、Num Lock 等指示灯 |
| 0x09 | Button | 按钮集合里的抽象按钮编号 |
| 0x0C | Consumer | 音量、播放暂停、亮度、菜单等消费控制 |
| 0x0D | Digitizer | 触摸屏、触控笔、压力传感 |
| 0x20 | Sensor | 加速度计、环境光、温湿度 |
| 0xFF00-0xFFFF | Vendor-Defined | 供应商自定义区域 |
拿到一个新外设的需求,先别急着写代码,第一步是确定设备属于哪个 Page。比如做播放器控制按键,应该落在 0x0C 消费页;做触摸板,就落在 0x0D 数字化设备页。选错了页,后面对应的几乎全是错的。
还要注意一点:《USB HID Usage Tables 1.3》不是把全世界的用途都收了。它更接近一个“已分配使用项的权威记录”,如果表里没有你要的语义,要么基于现有项拆分,要么走厂商自定义区,而不是自己往官方页里塞一个编号。
2.2 Usage Type 决定该放在集合上还是字段上
光有 Usage ID 还不够,规范还为每个使用项标了 Usage Type。这个类型告诉你它适合挂在哪里:
- CA(Application Collection):只能放在
Collection (Application)的 Usage 项上,用来定义一个完整的设备或应用集合,比如 Mouse、Keyboard。 - CL(Logical Collection):放在
Collection (Logical)或Collection (Physical)上,用来组织逻辑分组。 - C(Control):表示一个控制项,通常跟按钮、开关相关。
- DV(Dynamic Value):表示动态变化的值,比如坐标轴、传感器读数。
- Sel(Selector):表示从一组选项里选一个值,比如键盘的某个按键。
最常见的翻车场景,是把一个 CA 类型的使用项直接放在 Input 字段上。语义上等于告诉主机“这个字段本身是一台设备”,主机的输入子系统可能直接忽略掉后面的数据位。反过来,把 DV 类型的X轴放在集合 Usage 上,驱动解析出来的集合类别也会是错的。
我一般会先在文档里把对应 Usage 的 Type 列查出来,再决定它写在哪一行。比如 Generic Desktop 页的Mouse(0x02)是 CA,Pointer(0x01)也是 CA,但X(0x30)和Y(0x31)是 DV。按钮页 0x09 里的按钮编号,大多属于 C 或 Sel。这个顺序不可乱,乱了就是“描述符能编译、系统不认”的玄学问题。
2.3 把常用设备的使用项整理成速查表,比自己翻全文快
《USB HID Usage Tables 1.3》全文很长,我实际做项目时不会每次翻 PDF。我会把自己负责的品类摘成一张表,包含 Page、Usage ID、Usage Type、报告格式四列。这张表挂在项目文档里,评审和排错都能直接对。
| 设备 | Usage Page | Usage ID | Usage Type | 报告字段说明 |
|---|---|---|---|---|
| 鼠标 | 0x01 | 0x02(Mouse) | CA | Application Collection |
| 鼠标指针 | 0x01 | 0x01(Pointer) | CA | Physical Collection |
| 鼠标按键 | 0x09 | 0x01-0x03 | C | 3 个 bit,Variable |
| X/Y 坐标 | 0x01 | 0x30 / 0x31 | DV | 2 字节有符号或无符号 |
| 键盘 | 0x01 | 0x06(Keyboard) | CA | Application Collection |
| 按键数组 | 0x07 | 0x00-0x65 | Sel | 数组,每字节一个键值 |
| 音量加 | 0x0C | 0xE9 | C | 1 bit,Toggle 或 Momentary |
| 音量减 | 0x0C | 0xEA | C | 1 bit,Toggle 或 Momentary |
| 触控点 | 0x0D | 0x32(Tip Pressure) | DV | 压力值,字节长度看精度 |
这张速查表不需要一次建完,而是在每次踩坑后补一行。这样即使隔了半年再捡起一个旧方案,也不会因为忘记某个 Keypad 页的细节而重复排错。
3. 把 Usage 落进 Report Descriptor:最小键鼠描述符与必调参数
3.1 一个最小两键鼠标描述符:Mouse 的 Usage 排布顺序
要做 HID 鼠标,最稳妥的做法是照着标准的例子排字节。下面是一份最小两键鼠标描述符,包含 X/Y 轴,没有报告 ID:
// 最小两键鼠标 + X/Y 轴报告描述符 const uint8_t hid_mouse_desc[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) -> Type 是 CA 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) -> 逻辑集合用 CA 0xA1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (1) 0x29, 0x03, // Usage Maximum (3) 0x15, 0x00, // Logical Minimum 0 0x25, 0x01, // Logical Maximum 1 0x95, 0x03, // Report Count 3 0x75, 0x01, // Report Size 1 bit 0x81, 0x02, // Input (Variable, Absolute) 0x95, 0x01, // Report Count 1 0x75, 0x05, // Report Size 5 bits 0x81, 0x01, // Input (Constant) 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x95, 0x02, // Report Count 2 0x75, 0x08, // Report Size 8 bits 0x81, 0x02, // Input (Variable, Absolute) 0xC0, // End Physical 0xC0 // End Application };这里有几处容易写错的地方。第一,按钮字段用的是Button 页(0x09),不是 Generic Desktop 页,因为鼠标的按键在 Usage Tables 1.3 里是按钮页下面的抽象编号,和坐标轴不在同一个命名空间。第二,按钮字段声明了Usage Minimum=1、Usage Maximum=3,这样三个 bit 分别对应左键、右键、中键;如果你用三键鼠标,这个声明就是驱动识别按钮数的依据。第三,按钮后面补了 5 位的 Constant padding,让按钮字节凑满 1 个字节,然后再接坐标轴。
Logical Minimum和Logical Maximum是必调参数。坐标轴用-127/127,报告值为 0 表示中心,负值向左或向上,正值相反。如果你改成0/255,中心值就变成 128,固件取原始值时必须做相应偏移,否则鼠标会一直往一个方向漂移。
3.2 键盘:A 键在 Usage Tables 1.3 里的值是 0x04,不是 0x61
键盘描述符是最容易让新手误解的地方。看下面这份最小键盘描述符:
// 最小键盘描述符:6 键无冲方案,带修饰键字节 const uint8_t hid_kbd_desc[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) -> CA 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Keyboard/Keypad) 0x19, 0xE0, // Usage Minimum (Left Control) 0x29, 0xE7, // Usage Maximum (Right GUI) 0x15, 0x00, // Logical Minimum 0 0x25, 0x01, // Logical Maximum 1 0x75, 0x01, // Report Size 1 bit 0x95, 0x08, // Report Count 8 0x81, 0x02, // Input (Variable, Absolute) 0x95, 0x01, // Report Count 1 0x75, 0x08, // Report Size 8 bits 0x81, 0x01, // Input (Constant) 0x95, 0x06, // Report Count 6 0x75, 0x08, // Report Size 8 bits 0x15, 0x00, // Logical Minimum 0 0x25, 0x65, // Logical Maximum 101 0x19, 0x00, // Usage Minimum 0 0x29, 0x65, // Usage Maximum 101 0x81, 0x00, // Input (Array) 0xC0 // End Application };关键点看最后那个 Input 字段:它是一个Array 数组字段,每个字节放一个 Usage ID。按住字母 A 时,这个字段里某个字节的值就是 0x04,而不是 ASCII 的 0x61。这个 0x04 来自 Usage Tables 1.3 的 Keyboard/Keypad 页:A 键排在第一位后面,所以从 0x04 开始递增。如果写成 0x61,主机要么认为你在按另一个键,要么直接忽略。
修饰键字节的范围是 0xE0 到 0xE7,分别对应左 Ctrl、左 Shift、左 Alt、左 GUI、右 Ctrl、右 Shift、右 Alt、右 GUI。这个字节用 Variable 模式,每一位是一个独立的修饰键。它后面的常量字节是 HID 协议的保留字节,必须存在,许多兼容性测试就是从这里看报告结构是否规范。
3.3 消费者页面:0xE9 是音量加,不是十进制 233
消费类设备常做的是媒体键。这个场景我建议直接看下面这段描述符:
// 消费控制:音量加 + 音量减,各占 1 bit const uint8_t hid_consumer_desc[] = { 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x01, // Usage (Consumer Control) 0xA1, 0x01, // Collection (Application) 0x0A, 0xE9, 0x00, // Usage (Volume Up) 0x0A, 0xEA, 0x00, // Usage (Volume Down) 0x15, 0x00, // Logical Minimum 0 0x25, 0x01, // Logical Maximum 1 0x75, 0x01, // Report Size 1 bit 0x95, 0x02, // Report Count 2 0x81, 0x02, // Input (Variable, Absolute) 0xC0 // End Application };消费者页面的 Usage ID 经常超过 0xFF,所以不能只用0x09, 0xE9这种短格式,正式写法是0x0A, 0xE9, 0x00,用 16 位 Usage 格式声明。0xE9是音量加,0xEA是音量减,这两个值在 Usage Tables 1.3 的 Consumer 页里是稳定分配好的。直观记忆别去记十进制 233,直接记十六进制反而更不容易和键盘键码混淆。
这份描述符同样没有报告 ID。如果设备同时上报鼠标和多媒体键,就必须给每个集合加 Report ID,否则系统不知道这个报告属于哪个集合。加了 Report ID 后,报告的每个数据包第一字节永远是这个 ID,固件和主机端都对这一字节做路由,这是多合一 HID 设备的常见做法。
4. 自定义 Usage 与供应商区:没有这个按键,改表前先搞清分配规则
4.1 官方表里找不到的语义,优先考虑“复用现有项”
需求来了一个“拨盘旋钮”,希望做成 HID 外设。第一反应是去 Usage Tables 1.3 里找“Dial”。你会发现 Generic Desktop 页里有 0x37 Dial、0x38 Linear etc,但它们的原始含义和普通鼠标滚轮不一样,且很多主机驱动并不把它们映射成系统能识别的功能。遇到这种情况,不要急着自定义 Usage,先问自己两个问题:
- 这个旋钮语义能不能拆成现有 Usage 的组合?比如“旋转增量”可以用 Generic Desktop 页的
Wheel(0x38)或消费页的方向键代替。 - 这个设备只在自己的驱动软件里用,还是要兼容通用系统?
如果是自研软件配套,最省事的方式是产品使用厂商自定义区。Usage Tables 1.3 把 0xFF00 到 0xFFFF 这一大段页面预留给了厂商自定义,意味着标准驱动不解析它,数据会通过 HID 协议原样交给专用软件。
反过来,如果要做一个能被所有系统默认驱动的通用旋钮,那就不应该自定义。这类需求应该去找标准里已经存在的功能用法,比如模拟成相对鼠标轮或绝对坐标轴,而不是造一个主机不认识的新用法。
4.2 一个厂商区自定义拨盘描述符:能跑通但认你的人有限
当你确认必须走厂商自定义,描述符可以这样写:
// 厂商自定义拨盘设备描述符 const uint8_t hid_vendor_dial_desc[] = { 0x06, 0x00, 0xFF, // Usage Page (Vendor-Defined 0xFF00) 0x09, 0x01, // Usage (Vendor Dial) 0xA1, 0x01, // Collection (Application) 0x09, 0x10, // Usage (Vendor Dial Delta) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size 8 bits 0x95, 0x01, // Report Count 1 0x81, 0x02, // Input (Variable, Absolute) 0x09, 0x11, // Usage (Vendor Button) 0x15, 0x00, // Logical Minimum 0 0x25, 0x01, // Logical Maximum 1 0x75, 0x01, // Report Size 1 bit 0x95, 0x01, // Report Count 1 0x81, 0x02, // Input (Variable, Absolute) 0xC0 // End Application };注意这里用了0x06, 0x00, 0xFF三位扩展用法页,而不是普通的0x05, 0x01;原因是厂商自定义页从 0xFF00 开始,超出了单字节 Integer 的取值范围。后面的 Usage ID0x10、0x11也是自定义的,没有任何官方语义,只能靠主机端的专用软件按约定解释。
这样做的代价很明确:通用鼠标驱动不会认它,键盘驱动也不会认它,系统只会把它当作一个带厂商 HID 接口的设备。固件和专用软件必须同时匹配,不然就是一个只能在测试工具里看到数据的半成品。我以前做过一个数码旋钮,厂商页用法写好了,专用驱动没跟上,结果产品在客户机器上怎么转都没反应,最后花了一个迭代才在应用层把 HID 报告捞回来。
4.3 想进官方表,路径是提交申请而不是自己改文档
《USB HID Usage Tables 1.3》的分配是集中管理的,标准组织里有专门的工作组审核新 Usage。提交一条新 Usage 大体流程是:先证明现有页面没有等价语义,再写清适用设备类型、报告字段建议、兼容性影响,交给工作组评审,评审通过后进入候补清单,最后随下一个修订版发布。
这个周期通常以季度甚至年计,不适合产品研发的快速节奏。所以多数产品会选择两条腿走路:第一步用 0xFF00 厂商区先把产品做出来,专用软件可用;第二步同步走官方申请,万一将来进度赶上,再从厂商区迁移到正式 Usage ID。迁移时固件和驱动两边要同时改,还要做旧版本兼容,因此描述符里最好一开始就预留好 Report ID 和版本号字段,别把结构写死。
5. 避坑:照着旧资料能过编译,却被系统认不出来的 5 个实战问题
5.1 键盘 A 键写成了 ASCII 0x61,按下去没反应
现象:固件里把普通字母键值直接填 ASCII,按 A 发送 0x61。结果在系统里按 A 没反应,按某些功能键却跑出奇怪字符。
原因:Keyboard/Keypad 页的键值是按键索引,不是 ASCII 码。A 键在《USB HID Usage Tables 1.3》里是 0x04,0x61 对应的是键盘页里的另一个键位,多数是数字小键盘或应用键,所以主机执行了完全不同的动作。
解决:所有键盘键值必须查表,不能走 ASCII 转换。如果是从硬件扫描码转 HID,建议维护一张“扫描码 → HID Usage ID”的映射表,把 0x61 改成 0x04,并在联调时用系统自带记事本逐个验证按键。写完后可以对照Usage Minimum=0x00、Usage Maximum=0x65这个范围检查,数组字段里绝不允许出现范围之外的值。
5.2 消费者页的 Usage 忘了切页,音量键被识别成鼠标键
现象:同一个设备里先写了鼠标集合,再写消费者集合。音量加按键按下去,系统没有弹出音量调节,反而出现桌面图标选中或鼠标跳转。
原因:报告描述符里,Usage Page 是“有状态”的。上一个集合停在 Button 页,下一个集合如果没有显式写0x05, 0x0C,后面声明的 Usage ID 仍属于上一页。比如 0xE9 在 Button 页里不是一个有效的按钮编号,主机只能按旧页面的语义去猜。
解决:每进入一个新的 Application Collection,先切 Usage Page,再声明集合 Usage。消费者页里的 16 位 Usage 要统一用0x0A扩展格式,不要偷懒写0x09。这段我建议写成一个宏,比如USAGE_PAGE_CONSUMER和USAGE_ID_CONSUMER_VOL_UP,让代码一眼就能看到页和 ID 的归属。
5.3 数组字段的 Usage 范围比报告数量小,出现幽灵键
现象:键盘 6 键无冲设备,偶发按下某一个键时系统同时识别出另一个键被按下。测试工具看原始报告没有异常,但系统键码就是不对。
原因:数组字段声明了Usage Minimum=0和Usage Maximum=3,但Report Count=5。数组字段每个字节的值都对应一个 Usage ID,能用的键值只有 0 到 3,后面的字节超出范围,驱动拿超出的值解索引时产生未知键码,看起来就像多按了键。
解决:拉大Usage Maximum到键盘页全部键值,封面最大编号;或者把报告数量减少到和 Usage 范围一致。标准键盘数组往往是Usage Min=0、Usage Max=101、Report Count=6,这个组合能覆盖绝大部分键位,别为了省字节把范围缩到几个自定义键。
5.4 将 CA 类型的 Usage 放在 Input 字段上,设备被识别成“供应商设备”
现象:鼠标的位移正常,但点击按钮时系统属性里显示设备类别是 HID-compliant vendor-defined device,按钮事件完全丢失。
原因:Mouse 是一个 CA 类型的使用项,本来应该放在Collection (Application)前面。如果写在 Input 字段前面,等于告诉主机“这个字段是一个设备”,而不是“在这个集合里有一个按钮”。主机会按集合类型分发数据,字段语义就错位了。
解决:把集合级 Usage 只放 CA/CL 类型,字段级 Usage 放 DV/C/Sel 类型。每个 Usage ID 写在哪个位置,以文档里的 Type 列为准。遇到设备类别变成 vendor-defined,优先检查 Application Collection 的 Usage 是不是选错了。
5.5 以为新版表一定向后兼容,旧驱动接不住新 Usage
现象:把《USB HID Usage Tables 1.3》里新增的某个地市专用 Usage ID 写进描述符,新系统能识别,但同一台设备接到旧版本系统上直接无法使用,连基础功能都失效。
原因:Usage ID 是主机驱动的“字典索引”,旧系统没收录新 ID 时,会把这个字段当成未知数据。设备描述符里同时存在新老用法时,驱动可能直接丢弃整个报告,而不是只跳过未知字段。
解决:新页面 Usage 尽量只在报告 ID 隔离的新集合里使用;如果设备要兼容老系统,必须有降级方案。常见做法是同一个功能同时暴露两个集合,老系统匹配原有 Usage,新系统用新 Usage,固件根据收到的 Not Set Proper usage 或在初始化阶段决定用哪套报告格式。宁可软件复杂一点,不要把新旧语义混在同一个报告里。
6. 进阶:三招验证 Usage 分配到底有没有被主机认出来
6.1 先抓原始描述符,再对照文档看 Page/Usage 配对
设备插上后,我第一时间确认的是系统里看到的 report descriptor 和固件里写的是否一致。在 Linux 内核 HID 子系统的设备节点下,可以读取到主机侧解析出的描述符:
sudo usbhid-dump -m 1234:5678 -e > hid_desc_raw.txt head -80 hid_desc_raw.txt把输出里的 Usage Page、Usage 按十六进制记录下来,对照速查表确认每一项的页面归属。比如看到USAGE_PAGE (Generic Desktop)后面跟着USAGE (X),就该想到 X 是 0x30;如果看到鼠标按钮却出现在 Generic Desktop 页,说明固件切页有问题。这一步能过滤掉一半以上的低级错误。
6.2 用输入事件工具看实际映射,而不是只看描述符
描述符正确不等于系统驱动认它。在内核输入框架下,最终按键会映射到input_event。我常用 evtest 类工具直接观察事件:
sudo evtest /dev/input/event3按下键盘 A 键,如果事件流里出现KEY_A,说明 HID Usage 到内核键码的映射成功;如果事件流里根本没有 bytes,或者出现不认识的事件类型,说明 Usage 分配仍存在问题。这个验证要在按下每个按键时都做一遍,尤其是修改键和高位键值。
6.3 回归验证时故意发边界值,排除“恰好能用”的假象
最后一个技巧是在固件测试模式里故意把某个 Usage 改成范围边界值。比如键盘数组字段声明Usage Max=0x65,就发 0x00、0x01、0x65 三组值,确认驱动不会把 0x00 当成有效键;鼠标轴声明Logical Max=127,就发 0x81 和 0x7F,确认负向和正向极限都正常。这个习惯帮我抓出两次因为数组越界导致的幽灵键问题。
到这一步,USB HID Usage Tables 1.3 就不再是一本压在抽屉里的规范,而是一张能随时查、能验证、能排错的地图。我现在写任何 HID 描述符,第一件事是查 Type,第二件事是查边界值,第三件事才是看格式。希望帮到你。
本文还有配套的精品资源,点击获取