
简介《USB HID Usage Tables 1.3》是USB Implementers Forum于2022年发布的HID使用表规范文档面向USB设备开发者、硬件工程师、驱动程序员与系统集成商用于解决HID设备功能元素标准化定义与跨平台互操作问题。压缩包内为1个PDF文件约4.09MB完整收录规范正文、贡献者名单与修订历史便于离线查阅与团队共享。文档系统定义了Usage ID分类、Usage Page上下文、Report Descriptor结构、设备类行为要求及协议传输方式并涵盖通用控制、桌面设备、游戏设备、消费设备、模拟设备等类别同时说明向后兼容策略。1.3版整合了HUT审查请求101至107及多项既有规范中的用法由Microsoft、Intel、Google、Apple、Logitech等公司专家参与制定权威性较高。已有560人学习适合需要对照标准设计报告描述符、排查兼容性问题或构建符合规范HID设备的读者作为案头参考。1. 拿到一份 200 多页的 HID Usage Tables先别急着翻键盘那一章做 USB 外设的同行大概都有过这种体验设备插上电脑系统识别了驱动也装了但按键映射就是错位或者自定义的旋钮在某个系统上完全没反应。翻日志、抓包、改描述符折腾半天最后发现问题出在一个 Usage ID 上——要么用错了页要么用了一个厂商私有值要么把某个 Usage 的类型理解反了。这类问题不靠猜靠查表。而《USB HID Usage Tables 1.3》就是那张表本身。这份文档是 USB-IF 在 2022 年 1 月发布的 HID 用法表第 1.3 版覆盖从 1996 年到 2022 年的历次修订把键盘、鼠标、游戏手柄、触摸屏、消费电子控制、传感器、电源设备等各类 HID 设备的 Usage Page 和 Usage ID 全部定义清楚。它不是教程是一份标准参考手册但对做固件、写驱动、调描述符的人来说它的价值在于你写的每一个 Report Descriptor 里的 Usage都能在这里找到出处和约束。适合谁做 USB 外设固件的、写 HID 驱动或中间件的、做设备兼容性测试的以及需要给自定义设备分配合规 Usage 的团队。2. Usage Page 与 Usage ID 的寻址逻辑为什么你的描述符总被系统误判2.1 一个 Usage 是 32 位高 16 位定页、低 16 位定项HID 描述符里写Usage (0x09)或Usage (0x00010030)很多人一开始不理解这两种写法有什么区别。关键在于 HID Usage 的编码结构一个 Usage 是 32 位值高 16 位是 Usage Page低 16 位是 Usage ID。当 Page 为 0 时高 16 位省略只写低 16 位。所以0x09等价于0x00090009即 Button Page0x09下的 Usage 9而0x00010030是 Generic Desktop Page0x01下的 Usage 0x30也就是 X 轴。这个结构决定了你在描述符里写 Usage 时必须心里有数当前生效的 Usage Page 是什么。描述符里用Usage Page (Generic Desktop)设定页后续的Usage (X)、Usage (Y)都落在这个页下。一旦页设错后面所有 Usage 全部跑偏。我见过一个翻车案例某同事在描述一个自定义旋钮时前面设了Usage Page (Vendor-Defined 0xFF00)后面却写了Usage (0x30)本意是 X 轴结果系统把它当成厂商私有页下的 0x30完全不认。现象是设备能枚举、能出报告但上位机拿到的轴值永远是 0——因为操作系统按私有页处理直接忽略了。常见做法是在描述符开头集中声明所有用到的 Usage Page每个 Page 下的 Usage 紧跟其后不要跨页混写。如果确实需要多个页用Usage Page切换切换后立即写该页下的 Usage形成「页—项」成对出现的节奏。2.2 Generic Desktop 页是入口但别把所有东西都往里塞Generic Desktop Page0x01是绝大多数输入设备的起点鼠标、键盘、摇杆、多轴控制器都从这里取 Application Usage。比如鼠标的顶层 Collection 用Usage (Mouse)即 0x01/0x02键盘用Usage (Keyboard)0x01/0x06游戏手柄用Usage (Game Pad)0x01/0x05。这些是 Application Usage决定操作系统把这个设备归到哪一类。但问题在于很多开发者习惯性地把所有自定义控件都挂到 Generic Desktop 下比如把某个特殊功能键写成Usage (0x00FF)之类的未定义值。这在规范里是不允许的——Generic Desktop 页的 Usage ID 是封闭定义的未分配的值不应使用。正确做法是标准功能用标准页私有功能用 Vendor-Defined Page0xFF00–0xFFFF并在文档里注明。1.3 版在 3.2 节「Handling Unknown Usages」里明确说了接收方遇到未知 Usage 应当忽略而非报错但这不意味着你可以随便填——忽略意味着你的功能直接失效用户看到的就是「这个键没反应」。下面这段描述符片段是一个三轴旋钮的正确写法注意 Page 与 Usage 的配对关系// 三轴旋钮X/Y/Z 三轴每轴 8 位有符号范围 -127~127 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x08, // Usage (Multi-axis Controller) 0xA1, 0x01, // Collection (Application) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x09, 0x32, // Usage (Z) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x03, // Report Count (3) 0x81, 0x02, // Input (Data,Var,Abs) 0xC0 // End Collection这段代码的逻辑是先声明 Generic Desktop 页顶层 Collection 用 Multi-axis Controller0x01/0x08然后在同一个页下依次声明 X、Y、Z 三个 Usage逻辑范围 -127 到 127每个字段 8 位共 3 个字段输入类型为数据、变量、绝对值。参数上要注意Logical Minimum用 0x81 表示 -127 而不是 -128是因为 0x80 在部分解析器里会被当作特殊值处理留一格余量是血泪经验。Report Count (3)对应三个轴如果实际硬件只有两轴这里改成 2否则上位机会多读一个字节导致后续字段错位。2.3 Report Descriptor 里的 Usage 只是声明报告里的数据靠 Usage 对齐描述符定义的是「有哪些字段、每个字段对应哪个 Usage」而实际传输的 Input Report 是一串字节。操作系统解析报告时按描述符里字段的顺序和大小逐段切分再按 Usage 把每段映射到具体功能。所以描述符里 Usage 的顺序必须和报告里数据的顺序严格一致。这一点在单字段设备上不容易出错一旦字段多了尤其是混用不同大小的字段错位就是家常便饭。举个典型场景一个带滚轮的鼠标报告里依次是按键位图1 字节、X 位移1 字节、Y 位移1 字节、滚轮1 字节。描述符里如果先写 X、Y 再写 Button而报告里 Button 在最前面系统就会把按键状态当成 X 位移鼠标指针乱飞。排查这类问题的办法不是盯着描述符看而是用抓包工具把实际报告字节打出来对照描述符逐字段核对偏移。1.3 版文档在 3.1 节「HID Usage Table Conventions」里给了 Usage 表的阅读约定包括 ID 的十六进制表示、类型标注LC/OOC/MC 等这些标注在排查时很有用——比如你看到一个 Usage 标着 MCMomentary Control就知道它是瞬时控制不该被当作持续状态来读。3. 从表里查到落地键盘、消费控制与厂商私有页的实操路径3.1 键盘页0x07的 Usage ID 与 LED 页0x08的联动Keyboard/Keypad Page 是 0x07里面定义了标准按键的 Usage ID0x04 是 A0x05 是 B依次到 0x1D 是 Z0x1E 到 0x27 是数字 1 到 0修饰键在 0xE0 到 0xE7分别是左 Ctrl、左 Shift、左 Alt、左 GUI、右 Ctrl、右 Shift、右 Alt、右 GUI。这些值在写键盘固件时是硬编码的但容易出错的地方在于修饰键和普通键的报告结构不同标准键盘报告的第一个字节是修饰键位图第二个字节保留第三到第八字节是最多 6 个同时按下的普通键 Usage ID。LED 页0x08则是输出方向Num Lock、Caps Lock、Scroll Lock 等指示灯的 Usage ID 分别是 0x01、0x02、0x03。键盘固件需要处理来自主机的 Output Report根据 LED 页的 Usage 点亮对应指示灯。这里有个坑Output Report 的字段顺序同样由描述符决定如果描述符里 LED 字段的顺序和固件解析的顺序不一致就会出现「按了 Caps Lock 但灯不亮」或者「灯亮反了」的现象。常见做法是在描述符里把 5 个 LED 位打包成一个字节的位图固件按位解析位 0 对应 Num Lock、位 1 对应 Caps Lock依此类推和文档里 LED 页的 Usage 顺序保持一致。下面是一个键盘 LED 输出报告的描述符片段// 键盘 LED 输出报告5 个 LED 位打包为 1 字节 0x05, 0x08, // Usage Page (LED) 0x09, 0x01, // Usage (Num Lock) 0x09, 0x02, // Usage (Caps Lock) 0x09, 0x03, // Usage (Scroll Lock) 0x09, 0x04, // Usage (Compose) 0x09, 0x05, // Usage (Kana) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x05, // Report Count (5) 0x91, 0x02, // Output (Data,Var,Abs) 0x75, 0x03, // Report Size (3) 补齐到 1 字节 0x95, 0x01, // Report Count (1) 0x91, 0x03, // Output (Const,Var,Abs) 填充位逻辑说明先切到 LED 页依次声明 5 个 LED Usage每个 1 位共 5 位输出类型为数据变量绝对值然后用 3 个常量填充位补齐到 8 位保证整个输出报告是整字节。参数上Report Count (5)对应 5 个 LED如果设备只有 3 个灯改成 3填充位相应改成 5 位。填充位必须用Const类型否则主机会把填充位当成有效数据写入导致行为异常。3.2 Consumer Page0x0C是多媒体键的归宿但 ID 选择有讲究多媒体键盘上的音量加、音量减、静音、播放暂停、下一曲这些键标准做法是走 Consumer Page0x0C而不是塞进 Keyboard 页。Consumer 页的 Usage ID 里0xE9 是 Volume Increment0xEA 是 Volume Decrement0xE2 是 Mute0xCD 是 Play/Pause0xB5 是 Scan Next Track。这些 ID 在 1.3 版里有明确表格查的时候注意区分「One Shot Control」和「Re-Trigger Control」的类型标注——比如 Play/Pause 标的是 OOCOn/Off Control意味着它是切换型按一次开、再按一次关固件里就不该把它当瞬时按键处理。实际落地时Consumer 页的键通常和 Keyboard 页的键共存在同一个设备里形成复合设备。描述符里需要两个 Application Collection或者一个顶层 Collection 下挂多个 Report ID。用 Report ID 区分是更干净的做法Keyboard 报告用 Report ID 1Consumer 报告用 Report ID 2主机按 ID 分发。这里容易翻车的是 Report ID 的声明位置——Report ID必须写在每个报告的第一个字段之前且每个报告都要重新声明漏写会导致主机把两个报告的数据混在一起解析。3.3 Vendor-Defined Page 不是垃圾桶分配前先查有没有标准页可用自定义设备最常犯的错是一上来就用 Vendor-Defined Page0xFF00–0xFFFF把所有控件包进去。这样做能跑但代价是操作系统完全不认识这些 Usage所有功能都得靠上位机软件自己解析换一台机器、换一个系统就可能失效。1.3 版文档覆盖的页已经相当全Generic Desktop、Simulation Controls、VR Controls、Sport Controls、Game Controls、Generic Device Controls、Keyboard、LED、Button、Ordinal、Telephony、Consumer、Digitizer、Sensor 等。在决定用私有页之前先按功能逐页翻一遍大概率能找到现成的 Usage。比如做的是一个带力度感应的打击垫很多人第一反应是私有页但 Sport Controls Page0x04里有 Exercise Machines 相关 UsageGame Controls Page0x05里有 3D Game Controller 和 Gamepad 的定义Digitizer 页里有触摸和笔输入的标准 Usage。只有当功能确实无法归入任何标准页时才用私有页并且在文档里把私有 Usage 的语义写清楚方便后续维护和第三方对接。私有页的 Usage ID 建议从 0x01 开始连续分配不要跳号跳号在后期扩展时容易和旧版本冲突。4. 避坑实录Usage 用错之后的四类典型故障4.1 现象设备枚举成功但某个轴永远读不到值原因描述符里 Usage Page 切换后没有紧跟对应 Usage或者 Usage 写在了错误的页下。比如在 Generic Desktop 页下写了Usage (0x0201)本意是某个私有功能但 0x0201 在 Generic Desktop 页里未定义主机按未知 Usage 忽略处理。解决用 HID 描述符解析工具把描述符反解成树状结构逐节点核对每个 Usage 的完整 32 位值Page ID确认 Page 与 ID 的配对正确。对于自定义功能改用 Vendor-Defined Page 并显式声明。4.2 现象按键映射错位按 A 出 B原因Keyboard 页的 Usage ID 和实际键位对不上常见于非美式布局的键盘或者固件里把扫描码直接当 Usage ID 用。扫描码和 HID Usage ID 是两套体系不能混用。解决查 1.3 版 Keyboard/Keypad Page 的表格按物理键位对应的标准 Usage ID 重新映射。对于布局差异用 HID LANGID文档 3.6 节配合主机端做布局适配而不是在固件里硬改 Usage。4.3 现象多媒体键在某个系统上无效换一个系统正常原因Consumer 页的 Usage ID 用了较新版本才加入的值旧系统或旧驱动不认识按未知 Usage 忽略。1.3 版相比早期版本新增了不少 Usage如果目标环境是旧系统需要确认该 ID 在目标系统的 HID 表里是否存在。解决优先选用 Consumer 页里长期存在的经典 ID如 0xE9、0xEA、0xE2、0xCD避免用边缘 ID。如果必须用新 ID在设备文档里注明最低系统要求或者提供私有页的备用映射。4.4 现象Output Report 写入后设备无反应但 Input Report 正常原因Output Report 的描述符字段类型写成了Input或者 Report ID 在 Output 报告里漏声明导致主机把数据发到了错误的报告通道。解决检查描述符里 Output 字段的0x91标签Input 是0x81确认每个 Output 报告前都有对应的Report ID。用抓包工具确认主机确实发出了 Output 传输且数据长度与描述符声明的报告长度一致。5. 把 Usage 表变成可检索的本地数据一个解析脚本和验证习惯文档是 PDF200 多页查一个 Usage 靠翻页效率太低。我一般会把 Usage 表抽成结构化数据落到本地做快速检索。思路是用 Python 把 PDF 里的表格文本按页提取正则匹配「Usage ID、Usage Name、Usage Type」三列输出成 CSV 或 JSON再配一个简单的查询脚本。这样在写描述符时输入一个功能名就能反查 ID输入一个 ID 就能确认页和类型比翻 PDF 快一个数量级。下面是一个提取和查询的骨架脚本依赖pdfplumber做表格抽取import pdfplumber import re import json def extract_usage_table(pdf_path, page_range): 从指定页范围提取 Usage 表返回 [{page, usage_id, name, type}] rows [] with pdfplumber.open(pdf_path) as pdf: for pno in page_range: page pdf.pages[pno] tables page.extract_tables() for table in tables: for row in table: # 典型行[0x30, X, DV, Axis] if row and len(row) 3: uid (row[0] or ).strip() if re.match(r^0x[0-9A-Fa-f]{2,4}$, uid): rows.append({ page: pno, usage_id: uid, name: (row[1] or ).strip(), type: (row[2] or ).strip() }) return rows def build_index(rows, out_path): 按 Usage ID 建索引输出 JSON index {} for r in rows: index.setdefault(r[usage_id].lower(), []).append(r) with open(out_path, w, encodingutf-8) as f: json.dump(index, f, ensure_asciiFalse, indent2) if __name__ __main__: # 页范围按实际 PDF 的页码调整这里以键盘页为例 data extract_usage_table(hut13.pdf, range(87, 96)) build_index(data, usage_index.json) print(fextracted {len(data)} rows)逻辑说明extract_usage_table逐页调用extract_tables对每一行做正则过滤只保留第一列是十六进制 ID 的行避免把表头和说明文字混进来。page_range需要按实际 PDF 的物理页码调整文档里的章节页码和 PDF 页码通常有偏移先用一两页试跑确认抽出来的行数和肉眼看到的一致再扩大范围。build_index以 Usage ID 为键建索引同一个 ID 在不同页下可能重名所以值是列表而不是单值。参数上正则^0x[0-9A-Fa-f]{2,4}$覆盖 2 到 4 位十六进制因为部分页的 Usage ID 是 16 位如 0x0001按实际表格调整位数范围。抽完数据后验证习惯比脚本本身更重要。我现在的做法是每写一个新设备的描述符先用索引脚本把用到的每个 Usage 反查一遍确认三件事——Page 对不对、ID 在目标页里是否已定义、Usage Type 是否和实际行为匹配比如瞬时按键不该标成 OOC。这三步走完描述符层面的低级错误基本清零。从那以后我每次改描述符都强制走一遍「反查—核对类型—抓包验证」的流程宁可多花十分钟也不想到量产阶段再回头改固件。希望帮到你。本文还有配套的精品资源点击获取