首页 / 资讯中心 / 文章详情

HID Usage Tables 详解:从编号表到即插即用的底层逻辑

HID Usage Tables 详解:从编号表到即插即用的底层逻辑 ★ FEATURED ARTICLE
简介《USB HID Usage Tables 1.3》是USB Implementers Forum于2022年1月发布的HID使用表规范文档面向USB设备固件工程师、驱动开发者与系统集成人员用于解决键盘、鼠标、游戏控制器、触摸屏等HID设备在跨平台识别与互操作上的标准依据问题。资源包共1个PDF文件约4.09MB内容涵盖Usage ID分类、Usage Page上下文定义、Report Descriptor结构、设备类行为要求及协议传输方式并附有Microsoft、Intel、Google、Apple、Logitech等厂商专家的贡献者列表与完整修订历史。文档还纳入HUT审查请求101至107及显示器控制、街机设备、POS、电源设备等多个衍生规范的用法定义便于读者追溯版本演进。目前已有560人学习下载适合需要对照标准定义Usage、编写报告描述符或排查设备兼容性问题的开发者作为案头参考。1. 键盘插上就能用靠的不是驱动是 Usage Tables一个 USB 键盘插到任意一台机器上几秒内就能打字中间没有装驱动这一步。很多人把这归功于操作系统「自带驱动」但真正让这件事成立的是一份看起来极其枯燥的编号表HID Usage Tables。它规定了「0x04 代表 a 键」「0x30 代表 X 轴」「0x09 页里的 0x01 代表按钮 1」这类映射。没有这张表主机收到的只是一串字节根本不知道第 3 个 bit 是空格还是某个厂商自定义的玄学功能。搞懂这张表的组织方式你才能明白为什么改一个 Report Descriptor 里的 Usage 就能让设备从「不识别」变成「即插即用」也才能在自定义 HID 设备翻车时知道该查哪一页。2. Usage Page 与 Usage ID16 位编号为什么要切成两半HID 描述符里每个用途Usage都是一个 16 位值但规范把它拆成高 8 位的 Usage Page 和低 8 位的 Usage ID。这个切分不是随意定的它解决的是「命名空间爆炸」问题键盘按键、鼠标轴、LED 指示灯、消费类多媒体键、游戏手柄按钮这些东西语义完全不同如果全塞进一个平坦的编号空间冲突几乎不可避免。切成页之后每一页就是一个独立语义域页内编号可以从小往大排互不干扰。2.1 页号是语义域不是分类标签常见的页号在规范里是固定的0x01 是 Generic Desktop0x07 是 Keyboard/Keypad0x08 是 LED0x09 是 Button0x0C 是 Consumer。注意 0x09 Button 页很特殊它只有一个 Usage ID 0x01但通过 Usage Minimum/Maximum 可以展开成「按钮 1 到按钮 N」。这就是为什么一个 16 键手柄不需要 16 个不同的 Usage ID描述符里写一段范围就够了。我一般会这么理解Page 决定「这是哪一类东西」ID 决定「这一类里的第几个」范围机制则是对 ID 的批量声明。2.2 0x01 Generic Desktop 页为什么是入口页几乎所有 HID 设备的顶层集合Application Collection都挂在 0x01 页下鼠标是 0x02键盘是 0x06游戏手柄是 0x05多轴控制器是 0x08。主机枚举时先看顶层集合的 Usage判断「这是个什么设备」再往下解析子集合和字段。如果顶层 Usage 写错页比如把键盘写成 0x07 页的某个键主机会把它当成一个奇怪的按键设备而不是键盘启动协议Boot Protocol也不会生效。这是很常见的踩坑点描述符能通过解析但设备行为完全不对。2.3 页内 ID 的分配逻辑从 0x00 到 0xFF 的取舍以 Keyboard/Keypad 页0x07为例0x00 是 Reserved0x01 到 0x03 是错误回滚ErrorRollOver、POST 失败、错误未定义0x04 开始才是 a、b、c 这些字母键。把错误状态放在最前面是有意为之Boot Protocol 的 8 字节键盘报告里如果同时按下超过 6 个键硬件会填 ErrorRollOver0x01主机据此知道「按键溢出了」而不是收到一堆乱码。这种「把异常编码进正常编号空间」的做法让协议层不需要额外的错误通道代价是占用了几个宝贵的 ID。/* 典型键盘报告描述符片段简化 */ 0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x06, /* Usage (Keyboard) —— 顶层集合声明为键盘 */ 0xA1, 0x01, /* Collection (Application) */ 0x05, 0x07, /* Usage Page (Keyboard/Keypad) */ 0x19, 0xE0, /* Usage Minimum (0xE0 左Ctrl) */ 0x29, 0xE7, /* Usage Maximum (0xE7 右GUI) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x01, /* Logical Maximum (1) */ 0x75, 0x01, /* Report Size (1 bit) */ 0x95, 0x08, /* Report Count (8) —— 8 个修饰键各占 1 bit */ 0x81, 0x02, /* Input (Data,Var,Abs) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x08, /* Report Size (8) */ 0x81, 0x01, /* Input (Const) —— 保留字节 */ 0x95, 0x06, /* Report Count (6) */ 0x75, 0x08, /* Report Size (8) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x65, /* Logical Maximum (0x65) */ 0x05, 0x07, /* Usage Page (Keyboard/Keypad) */ 0x19, 0x00, /* Usage Minimum (0) */ 0x29, 0x65, /* Usage Maximum (0x65) */ 0x81, 0x00, /* Input (Data,Array) —— 6 个按键槽位 */ 0xC0 /* End Collection */这段描述符里有两个关键设计修饰键Ctrl/Shift/Alt/GUI用位图表示每个键固定 1 bit所以能同时表达 8 个修饰键的状态普通按键用数组表示6 个槽位每个 8 bit填的是 Usage ID。数组模式下主机收到的是「按下了哪些键的编号」而不是「哪个位置被按下」。这就是为什么键盘不需要为每个物理按键分配独立的 bit104 键键盘的报告仍然只有 8 字节。参数上Logical Maximum 0x65 对应 Keyboard/Keypad 页里已定义的最大 Usage ID超出这个范围的值在数组里会被视为无效。3. Report DescriptorUsage 表怎么变成主机能解析的二进制结构Usage Tables 是「字典」Report Descriptor 是「语法」。字典告诉你 0x04 是 a 键语法告诉主机「这个设备的第 2 个字节到第 7 个字节是按键数组每个字节是一个 Usage ID」。两者配合主机才能把原始报告解析成有语义的输入事件。理解这一层才能明白为什么改了 Usage 表里的编号设备行为会跟着变。3.1 全局项、局部项、主项描述符的三层状态机Report Descriptor 的解析是一个状态机。全局项Global Item设置后续所有字段共享的参数比如 Usage Page、Logical Minimum/Maximum、Report Size、Report Count局部项Local Item只作用于下一个主项比如 Usage、Usage Minimum/Maximum主项Main Item才是真正产生字段的指令Input、Output、Feature、Collection、End Collection。解析器维护一个全局状态栈遇到 Push/Pop 可以保存和恢复。这个设计的动机是压缩描述符体积一个键盘有几十个按键字段如果每个字段都重复写一遍 Report Size 和 Logical Range描述符会膨胀好几倍。全局项让公共参数只写一次局部项让每个字段的 Usage 单独指定。3.2 Input/Output/Feature 三种报告的方向语义Input 是设备到主机Output 是主机到设备Feature 是双向的、通常用于配置。键盘的按键是 InputLED 状态是 Output比如 Caps Lock 灯。这里有个容易混淆的点LED 页0x08的 Usage 出现在 Output 报告里主机通过 Set_Report 把 LED 位图发给设备设备再点亮对应的灯。如果开发者把 LED 写成 Input主机会尝试从设备读 LED 状态但设备根本不产生这个报告结果就是灯永远不亮。规范里对每种 Usage 适合放在哪个方向有约定但解析器不会强制校验所以这类错误只能靠行为测试发现。3.3 Usage Minimum/Maximum 与数组、位图的配合数组Array和位图Bitmap是两种不同的字段组织方式。数组适合「从一组离散值里选若干个」比如按键槽位位图适合「每个 bit 独立表示一个布尔状态」比如修饰键、LED、按钮。Usage Minimum/Maximum 在两种模式下含义不同位图模式下它声明一段连续的 Usage 范围每个 Usage 对应 1 bit数组模式下它声明数组元素的合法取值范围主机收到某个值后去这个范围里查对应的 Usage。Button 页0x09的 0x01 配合 Usage Minimum 1、Maximum 16就能表达 16 个按钮的位图这是游戏手柄描述符里最常见的写法。/* 游戏手柄按钮位图片段 */ 0x05, 0x09, /* Usage Page (Button) */ 0x19, 0x01, /* Usage Minimum (Button 1) */ 0x29, 0x10, /* Usage Maximum (Button 16) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x01, /* Logical Maximum (1) */ 0x75, 0x01, /* Report Size (1) */ 0x95, 0x10, /* Report Count (16) —— 16 个按钮各占 1 bit */ 0x81, 0x02, /* Input (Data,Var,Abs) */这段代码产生 2 字节的按钮位图第 0 bit 是 Button 1第 15 bit 是 Button 16。注意 Report Count 是 16Report Size 是 1总宽度 16 bit。如果厂商想加第 17 个按钮不能简单把 Maximum 改成 17因为 Report Count 也得跟着改否则解析器会认为只有 16 个按钮第 17 个 bit 被当成填充。这种「范围、计数、尺寸三者必须一致」的约束是描述符调试里最常见的翻车来源之一。4. 从 Boot Protocol 到 Report Protocol兼容性是怎么被设计进去的USB HID 有一个其他设备类很少见的特性Boot Protocol。它规定键盘和鼠标在启动阶段必须能用一套固定的 8 字节键盘和 3 字节鼠标报告格式工作不需要主机先解析 Report Descriptor。这个设计的历史动机是 BIOS/UEFI 环境固件里的 USB 栈非常简陋没能力跑完整的描述符解析器但又必须在操作系统加载前支持键盘输入。Boot Protocol 就是那个「最小可用子集」。4.1 8 字节键盘报告的固定布局Boot Protocol 键盘报告固定 8 字节第 0 字节是修饰键位图左 Ctrl 到右 GUI第 1 字节保留第 2 到第 7 字节是 6 个按键槽位每个槽位填 Keyboard/Keypad 页的 Usage ID。这个布局和前面那段描述符产生的格式完全一致所以设备只要按这个格式发报告无论主机解不解析描述符都能用。代价是灵活性Boot Protocol 下不能有超过 6 个同时按下的普通键不能有厂商自定义字段不能有额外的轴。设备通过 Get_Protocol/Set_Protocol 请求在 Boot 和 Report 两种模式间切换操作系统加载后会切到 Report Protocol启用完整描述符定义的能力。4.2 为什么鼠标的 Boot 报告只有 3 字节鼠标 Boot 报告是 3 字节按键位图、X 位移、Y 位移每个位移是 8 位有符号数范围 -127 到 127。这个范围决定了 Boot Protocol 下鼠标的移动分辨率上限快速移动时如果单次位移超过 127会被截断或溢出。Report Protocol 下可以用 16 位甚至更大的 Logical Maximum 来表达更大位移也可以用多个轴。这个取舍很典型兼容性优先的路径必须足够简单简单到固件能硬编码解析功能完整的路径则依赖描述符灵活但要求主机有完整协议栈。4.3 描述符里的 Usage 如何影响协议切换后的行为切换到 Report Protocol 后主机按描述符解析报告此时 Usage 表的准确性直接决定字段语义。比如一个键盘在描述符里把某个按键声明成 Consumer 页的「播放/暂停」那么在 Report Protocol 下这个键会产生多媒体键事件而在 Boot Protocol 下它可能根本不存在因为 Boot 报告只有 6 个槽位且只认 Keyboard/Keypad 页。同一个物理按键在两种协议下行为不同这不是 bug是规范允许的。开发者如果发现「BIOS 里能用、进系统后某个键失灵」大概率是描述符里该键的 Usage 页写错或者 Report Protocol 下的报告长度和 Boot 模式不一致导致主机解析错位。5. 避坑Usage Tables 落地时最容易翻车的几个点5.1 现象设备枚举成功但按键全乱原因通常是 Usage Page 和 Usage ID 拼错了。16 位 Usage 值在描述符里是分两条指令写的先Usage Page (0x07)再Usage (0x04)解析器把它们组合成 0x00070004。如果只写了 Usage 没写 Page解析器会沿用上一个全局 Page结果可能是 0x00010004Generic Desktop 页的 0x04语义完全变了。解决方法是逐条核对每个 Usage 前面的 Page 指令尤其是跨页的字段比如键盘描述符里混了 LED 页的 Output 字段Page 必须重新声明。5.2 现象Report Count 和 Usage 范围不匹配主机只认前几个前面提过位图模式下 Usage Minimum/Maximum 的范围长度必须等于 Report Count。如果声明了 Button 1 到 Button 16但 Report Count 写成 8主机只会解析前 8 个按钮后 8 个 bit 被当成填充或直接忽略。反过来Report Count 大于范围长度多出来的位没有对应 Usage行为未定义。排查时把描述符按「Page → Usage 范围 → Report Size × Count」逐段列出来算一下总位数和报告长度是否对得上基本能定位。5.3 现象Logical Maximum 设小了摇杆值被截断模拟轴比如 Generic Desktop 页的 X/Y用 Logical Minimum/Maximum 声明取值范围。如果硬件是 12 位 ADC0 到 4095但描述符里 Logical Maximum 写成 255主机解析时会把超过 255 的值截断或视为越界。更隐蔽的是有符号轴Logical Minimum -127、Maximum 127 配 8 位字段和 Minimum -32768、Maximum 32767 配 16 位字段报告长度不同主机解析的字节数也不同。解决方法是让 Logical Range 和 Report Size 的位宽严格匹配12 位数据要么用 16 位字段承载要么在固件里先缩放到声明范围。5.4 现象自定义 Usage 在某个系统上不生效Usage Tables 里有一批「保留」和「厂商自定义」区间比如 0xFF00 到 0xFFFF 是 Vendor Defined。用这些 Usage 的设备在规范层面是合法的但主机操作系统不一定为它们提供通用映射需要配套的驱动或应用层解析。如果设备依赖某个系统内置的 Usage 映射换一个系统就可能失灵。排查时先确认该 Usage 是否在目标系统的 HID 映射表里有定义没有的话就得走自定义报告 用户态解析的路线不能指望即插即用。5.5 现象改了 Usage 表版本旧设备行为变了Usage Tables 的修订会新增 Usage、修正编号、废弃旧定义。如果固件里硬编码了某个 Usage ID而新版本规范调整了该 ID 的含义设备在新主机上可能表现异常。规避方式是尽量使用长期稳定的核心 Usage比如 Keyboard/Keypad 页的字母键、Generic Desktop 页的鼠标键盘顶层集合对新增的消费类、传感器类 Usage 保持谨慎并在固件里留一个可配置的映射层而不是把 Usage ID 散落在各处。6. 设计取舍一张编号表为什么能撑起整个 HID 生态回头看HID Usage Tables 最核心的设计决策是「用编号而不是字符串来标识功能」。字符串可读性好但每个报告都要带一串名字带宽和解析成本都高编号紧凑16 位就能表达页 ID报告里只传编号语义靠主机侧的字典查。这个取舍的代价是字典必须集中维护、版本必须协调于是有了这份由多家厂商共同修订的规范以及它长达二十多年的修订历史。另一个取舍是「页 ID」的两级结构一级页隔离语义域二级 ID 在域内编号既避免了全局冲突又保留了扩展空间。新增一类设备时开一个新页不影响已有页的编号稳定性。验证一个 HID 设备是否符合规范最直接的方法不是读文档而是抓描述符反解析把设备插上读出 Report Descriptor按全局项/局部项/主项逐条展开检查每个字段的 Usage Page、Usage 范围、Report Size、Report Count、Logical Range 是否自洽再对照 Usage Tables 确认每个 Usage 的语义和方向Input/Output/Feature是否匹配。我一般会把这个检查做成脚本每次改描述符都跑一遍比在系统里反复插拔试错快得多。血泪经验是描述符能枚举成功不代表语义正确主机对未知 Usage 往往静默忽略问题会拖到用户实际使用时才暴露。从那以后我每次设计自定义 HID 设备都强制先把 Usage 分配表列出来标清每个字段的页、ID、方向、位宽和逻辑范围再动手写描述符最后用反解析脚本对一遍。这张表看起来是额外工作但它把「玄学不识别」变成了可排查的编号问题。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站