
学习一下 HID 设备的报告描述符 ...... 矜辰所致前言最近遇到个鼠标设备的需求之前 USB 的描述符学习也有提到 HID 设备描述符相关的内容但是没有针对性的说明现在直接去看设备的报告描述符还是有点懵还得仔细的研究一下。所以本文主要就是来了解学习一下 HID 设备的报告描述符。相关博文USB 设备描述符 配置描述符 报表描述符沁恒微蓝牙从机添加服务和特征示例一文搞清 BLE 蓝牙 UUID.我是矜辰所致全网同名尽量用心写好每一系列文章不浮夸不将就认真对待学知识的我们矜辰所致金石为开目录前言一、 基础说明1.1 HID 设备1.2 主机识别 HID 设备流程二、 报告描述符格式2.1 USB 官方说明文档2.2 报告描述符解析2.2.1 Items 格式2.2.2 Main Items2.2.3 Global Items2.2.4 Local Items2.2.5 HID 报告描述符阅读框架2.2.6 补充说明2.3 常用工具三、示例解析结语一、 基础说明依旧是简要过一遍基础知识说明不懂的小伙伴自行 AI 搜索详细了解。1.1 HID 设备HIDHuman Interface Device人机接口设备是一套用于实现人与计算机 / 电子设备交互的通用设备类规范核心特点是免驱 。Windows、macOS、Linux 等主流操作系统均内置通用 HID 驱动具备良好跨平台兼容性设备完成枚举 / 连接后即可被系统自动识别。典型 HID 设备键盘、鼠标、触摸板、触摸屏、游戏手柄、操纵摇杆等。核心机制设备与主机之间依靠 HID 报告完成双向数据交互报告描述符用来描述报告的数据结构、每一位数据的含义。报告分为三类输入报告、输出报告、功能报告。USB‑HID 与 BLE‑HOGP蓝牙 HID均遵循 HID 上层语义规范仅底层通信载体不一样USB 使用中断传输、控制传输BLE‑HOGP 基于 GATT 的 Notify 与 Write 完成数据交互。1.2 主机识别 HID 设备流程报告描述符只定义数据包格式不告诉主机自己是 HID 设备USB 靠设备描述符→配置描述符→接口描述符识别这是 HID 接口再从 HID 类描述符拿到报告描述符的地址长度再去读取报告描述符。BLE (HOGP) 靠GATT 服务 特征值表明自己是 HID 设备再读取存放在 Characteristic 里的报告描述符。附带一下描述符英文和中文对照Device Descriptor设备描述符Configuration Descriptor配置描述符Interface Descriptor接口描述符String Descriptor字符串描述符Endpoint Descriptor端点描述符HID 类特定描述符HID DescriptorHID 描述符Report Descriptor报告描述符USBBLE (HOGP)主机怎么知道设备是 HID枚举时读取Interface Descriptor接口描述符识别到bInterfaceClass 0x03建立连接后 GATT 发现服务看到 0x1812必要或者 扫描广播看到 Service UUID 0x1812非必要仅用于扫描识别主机怎么找到 Report Descriptor报告描述符Interface 后面跟着HID DescriptorHID类描述符里面写了 Report Descriptor 的长度和类型主机再发标准请求去读在 HID Service (0x1812) 下找到Report Map Characteristic(UUID 0x2A4B)通过 GATT Read 读出完整报告描述符USB HID 的描述符识别流程每个描述符的解析可参考博主之前的博文USB 设备描述符 配置描述符 报表描述符BLE HID 的描述符识别流程有关 UUID 和 服务于特征值的定义设置可参考博主之前的博文一文搞清 BLE 蓝牙 UUID沁恒微蓝牙从机添加服务和特征示例这里使用博主手头的例子说明一下 二、 报告描述符格式如何看懂报告描述符对于新手来说是一道坎虽然我们很多应用都是使用现成的模板但是看不懂都不会修改了所以有必要学习一下解析方式的。2.1 USB 官方说明文档对于报告描述符符的解析在 USB 官方有一份文档《 HID 1.11 核心规范 》https://www.usb.org/sites/default/files/hid1_11.pdfUSB-IF里面实际上把所有 USB 我们需要的知识都说明了只不过原版英文文档除了驱动开发人员一般都不会去详细的阅读研究。对于我们本文要说到的 HID 报告描述符在文档第六章描述符部分的 6.22 小结2.2 报告描述符解析对于报告描述符我们看很多示例代码虽然是uint8_t的数组但基本是按照一组一组的格式书写的。2.2.1 Items 格式每一组大多两个字节有些多一点数据都是一个 Items 这个在官方文档中有解析方式如下图我们键鼠基本都是用的 Short Items 本文也只讨论 Short Items ]我们使用下面例子来说明一下 ex 0x05, 0x01第一步先拆解 0x05 0000 0101 (二进制)││││ │ │ └┬┘ bSize 01 → 数据占 1 字节││││ └┬┘ bType 01 → Global 类└┴┴┴ bTag 0000 → Usage Page得到这是一条 Global Usage Page 指令后面跟 1 字节数据。第二步读数据 0x01查 Usage Tables0x01 Generic Desktop Controls通用桌面控制鼠标、键盘、游戏手柄都在这一页。所以 0x05, 0x01 “把当前的 Usage Page 设为 Generic Desktop”。解析每一个 Short Item一定是看 Item 前缀字节先看 bType再看 bTag最后拿 bSize。bType 也就 3 种类型00Main01Global10Local确定了 bType 再去看 bTag 同一个 bTag 在不同的 bType 中的含义不同我们这里列举一下常用的 bTag 组合。2.2.2 Main Items当bType 00时是 Main Items它的不同的 bTag 表格如下bTag bit7~bit4prefix1B名称说明0x80x81Input定义输入字段设备→主机后面跟 1 字节位掩码0x90x91Output定义输出字段主机→设备如键盘 LED后面跟 1 字节位掩码0xA0xA1Collection开集合数据0x01Application0x00Physical0xB0xB1Feature定义 Feature 字段双向配置后面跟 1 字节位掩码0xC0xC0End Collection关闭当前 Collection集合结束 0xC0无后续数据bSize0Main 类 基本上大部分的情况只用 0x81、0x91、0xA1、0xC0 这 4 个。上面表格里面提到了 后面跟 1 字节位掩码这个位掩码是什么其实就是 后面跟着的 data 数据比如0x81,0x06这个0x06就是这个 8bit 属性字节它每位的解析如下表格bit规范名称1 含义0 含义bit0Data/ConstantConstant常量 / 填充位主机忽略Data有效数据bit1Variable/ArrayVariable独立变量Array选择器数组bit2Relative/AbsoluteRelative相对增量鼠标位移、滚轮Absolute绝对值摇杆、坐标bit3Wrap值溢出环绕不环绕、钳位bit4NonLinear非线性映射线性映射绝大多数设备bit5No Preferred StateNo Preferred无首选状态Preferred State有首选状态bit6Null State支持 Null 状态No Null Position无空状态bit7Buffered BytesBuffered BytesBit Field普通位域bit3~bit7普通键鼠全部写 0几乎不用改。工程常用组合:值二进制含义典型用途0x000000 0000Data, Array, AbsoluteBoot 键盘 6 字节键码数组、Consumer 控制数组0x020000 0010Data, Variable, Absolute鼠标按键、修饰键、摇杆绝对坐标0x060000 0110Data, Variable, Relative鼠标 X/Y 位移、滚轮0x010000 0001Constant, Array, AbsolutePadding 填充位保留域主机忽略2.2.3 Global Items当bType 01时是 Global Items它的不同的 bTag 表格如下bTagbit7~bit4prefix1Bprefix2Bprefix4B名称说明0x00x050x060x07Usage Page当前 Usage Page如0x01Generic Desktop0x10x150x160x17Logical Minimum逻辑最小值0x20x250x260x27Logical Maximum逻辑最大值0x30x350x360x37Physical Minimum物理最小值带单位换算极少用0x40x450x460x47Physical Maximum物理最大值极少用0x50x550x560x57Unit Exponent单位指数极少用0x60x650x660x67Unit单位编码极少用0x70x750x760x77Report Size每个字段的位宽bit0x80x85——Report ID报告 ID1 字节多 Report 时区分0x90x950x960x97Report Count字段数量0xA0xA4——Push压栈保存 Global 状态无数据0xB0xB4——Pop出栈恢复 Global 状态无数据Push (0xA4) / Pop (0xB4) 是无数据不是一字节数据表格设计关系只能放那里重点只记0x05、0x15/0x25、0x75、0x95其他遇到再查。2.2.4 Local Items当bType 10时是 Local Items它的不同的 bTag 表格如下bTagprefix1Bprefix2Bprefix4B名称说明0x00x090x0A0x0BUsage单个 Usage ID需配合 Usage Page0x10x190x1A0x1BUsage MinimumUsage 数组 / 位图的最小 ID0x20x290x2A0x2BUsage MaximumUsage 数组 / 位图的最大 ID0x30x39——Designator IndexDesignator 索引物理描述符极少用0x40x49——Designator Minimum—极少用0x50x59——Designator Maximum—极少用0x60x69——String Index字符串索引极少用0x70x79——String Minimum—极少用0x80x89——String Maximum—极少用0x90x99——DelimiterUsage 集合别名1 开始 (Open)0 结束 (Close)极少用实际开发 90% 只用到前 3 个0x09 (Usage)、0x19 (Usage Min)、0x29 (Usage Max)。2.2.5 HID 报告描述符阅读框架OK讲到这里其实我们都能够根据上面的表格介绍去分析任何一个 HID 报告描述符但是即便我们知道每个独立的 Items 是什么意思我们看到整个报告描述符还是很懵圈这么多 Items 是如何组合起来要如何读懂是个问题 。报告描述符的核心本质 就像一台状态机脚本不是一行行独立语句Global设置全局参数寄存器持续保留直到被同类型覆盖 。Local写临时草稿写好放这等待 Main 来使用 。Main是真正干活的把当前 Global 当前 Local 组合在一起定义报告里面的一段二进制位域用完立刻把 Local 草稿清空。Collection大括号 { }逻辑分组不产生数据通俗类比写 C 语言Global → 全局变量一直存在后面可改Local → 局部临时变量Main Input/Output → 函数调用读取当前全局 临时变量生成结构体字段然后临时变量清零。固定阅读框架建议按下面步骤阅读描述符一、看 Collection 集合分清大括号范围先把 “大括号” 划分出来知道哪一段代码属于鼠标哪一段属于键盘。前缀含义0xA1, 0x01Application集合开始{→ 一个独立设备功能鼠标 / 键盘 / 媒体键0xA1, 0x00Physical集合开始{→ 内部细分按钮组、坐标轴组0xC0集合结束}二、在{ }内部逐段读状态机Global设置 UsagePage、ReportSize、ReportCount、Min/Max↓LocalUsage / UsageMin~Max写临时草稿↓Global可以继续改全局参数↓Local可以覆盖之前的临时草稿↓MainInput / Output / Feature ←【真正消费点消耗GlobalLocal执行完毕Local立刻清空】↓Local 立即清空Global 保留不变Global 和 Local 可以交错出现只要它们都出现在 下一个 Main 之前。三、遇到一条 Main确定 4 个问题Collection / End Collection 除外问题查什么① 当前 Global 是什么UsagePage ? / ReportSize ?每个字段多少 bit/ ReportCount ?几个字段/ Logical Min/Max ?数值范围② 当前 Local 是什么Usage / UsageMin~Max ?代表什么硬件按键X 轴Y 轴滚轮LED③ Main 掩码是什么0x02Data,Var,Abs绝对值/0x06Data,Var,Rel相对值/0x00Data,Array,Abs数组④ 占多少 bit总 bit ReportSize × ReportCount累计到包长里四、累计 bit算出报告包长度所有 Input/Output/Feature 的 bit 分别独立累加 ÷ 8向上取整 报告数据包总字节数如果 bit 不是 8 的整数倍后面必须跟 Constant Padding0x81, 0x01补齐字节边界。最终总结一下看到 0xA1 → 画 { Collection不消费 Local看到 0xC0 → 画 } End Collection看到 0x05 → 换章节 Usage Page看到 0x09 → 记功能 Usage看到 0x75 → 记位宽 Report Size看到 0x95 → 记个数 Report Count看到 0x81 → 算 bit Input真正消费点Size × Count看到 0x91 → 算 bit Output真正消费点看到 0xB1 → 算 bit Feature真正消费点键鼠极少用最后【同类内】加总 ÷ 8 → 包长2.2.6 补充说明每个Collection 前放它需要的 UsageCollection 是 Mainitem解析器遇到它时会把当前挂起的 Local 状态结算给这个 collection(典型就是它前面的 Usage)然后清空Local 状态后面再遇到 Input/Output/Feature 时必须重新声明Local项。Local 生效范围 Local 对下一个消费型 Main itemInput/Output/Feature/Collection生效中间可隔 Global消费后立即清空。Local 多 Usage 处理多个 Usage 累积为有序列表不是覆盖。Count1 时多余忽略Count1 时按序分配各字段CountUsage数 时最后一个 Usage 重复 。Global 全局覆盖持续生效直到被改写跨 Collection 边界 。2.3 常用工具USB官网提供了一个 HID 工具HID Descriptor Tool 大家可以自行下载。这里还推荐一个 在线解析工具直接粘贴十六进制数组出结果https://eleccelerator.com/usbdescreqparser/三、示例解析来分析一个 HID 报表实例其实详细的步骤和方式上文种都已经说明了我们找一个简洁的用注释说明// 鼠标报告描述符3键 X/Y 滚轮带 padding 凑整字节constuint8_tHID_ReportDescriptorMouse[]{// // Application 集合声明这是一个鼠标设备// 0x05,0x01,// Global UsagePage Generic Desktop(0x01) 【全局切换到大章节通用桌面设备】0x09,0x02,// Local Usage Mouse 【临时草稿我是鼠标】0xA1,0x01,// Main Collection Application { 应用集合开始上面那个0x09 0x06 (Local: Usage)被 collection 消费消费靠这个识别鼠标0x09,0x01,// Local Usage Pointer 【草稿指针设备】0xA1,0x00,// Main Collection Physical { 物理分组开始// // 第一块3个按键左键/中键/右键// 0x05,0x09,// Global UsagePage Buttons(0x09) 【全局切换到按键页】0x19,0x01,// Local UsageMin 10x29,0x03,// Local UsageMax 3 【草稿代表按键1~按键3】0x15,0x00,// Global LogicalMin 00x25,0x01,// Global LogicalMax 10x75,0x01,// Global ReportSize 1 bit 每个字段占 1 bit0x95,0x03,// Global ReportCount 3 一共 3 个字段0x81,0x02,// Main Input【消费上面 GlobalLocal】// 计算3 × 1bit 3bit// 掩码 0x02 Data, Variable, Absolute// → bit0左键, bit1中键, bit2右键0松开, 1按下// Main 执行完Local 清空Global 保留但马上被下面改写// // 第一块补充5bit 填充位Padding 凑整字节// 因为上面 3bit 不够 1 字节补 5bit 凑齐到 8bit 1 字节// // 注意UsagePage 仍是上面的 ButtonsLogicalMin/Max 仍是 0/1// 但 Padding 是 Constant主机不解析含义所以无所谓0x75,0x05,// Global ReportSize 5 bit 覆盖原来的 10x95,0x01,// Global ReportCount 1 覆盖原来的 30x81,0x01,// Main Input【消费】// 掩码 0x01 Constant, Array, Absolute常量填充无含义// 计算1 × 5bit 5bit// 累计 3bit 5bit 8bit 1 字节按键字节完成// Local 清空本来就没有 Local// // 第二块X / Y 位移相对值// 0x05,0x01,// Global UsagePage Generic Desktop 【全局改写回通用桌面页】// Wheel(0x38) 和 X(0x30)/Y(0x31) 都属于这个页0x09,0x30,// Local Usage X 【草稿X 轴】0x09,0x31,// Local Usage Y 【追加到草稿列表Y 轴】// 注意两个 Usage 是追加不覆盖0x15,0x81,// Global LogicalMin -127 0x81 是 -127 的有符号 byte 补码0x25,0x7F,// Global LogicalMax 1270x75,0x08,// Global ReportSize 8 bit 覆盖为 80x95,0x02,// Global ReportCount 2 X 和 Y 两个字段0x81,0x06,// Main Input【消费】// 掩码 0x06 Data, Variable, Relative相对位移// 计算2 × 8bit 16bit 2 字节// → 第 2 字节 X 偏移, 第 3 字节 Y 偏移// Local 清空Usage X/Y 销毁// // 第三块滚轮 Wheel相对值// // 注意UsagePage 仍是上面的 Generic DesktopGlobal 保留所以 Wheel(0x38) 能正确解析// 注意LogicalMin/Max -127/127Global 保留下面没改// 注意ReportSize 8bitGlobal 保留0x09,0x38,// Local Usage Wheel 【草稿滚轮】0x95,0x01,// Global ReportCount 1 覆盖为 1Size 还是 80x81,0x06,// Main Input【消费】// 掩码 0x06 Data, Variable, Relative相对位移// 计算1 × 8bit 8bit 1 字节// → 第 4 字节 滚轮偏移// Local 清空0xC0,// } Physical 结束0xC0// } Application 结束};// // 最终 Input Report 数据包布局共 4 字节// // ┌────────┬──────────────────────────────┬────────┐// │ 字节0 │ bit0左键 bit1中键 bit2右键 │ 5bit 填充│// │ 字节1 │ X 偏移有符号-127~127 │// │ 字节2 │ Y 偏移有符号-127~127 │// │ 字节3 │ 滚轮偏移有符号-127~127 │// └────────┴──────────────────────────────┴────────┘// 累计 bit3 5 16 8 32 bit 4 字节无跨字节字段//// Output Report 0 字节本描述符无 0x91 Output 项// Feature Report 0 字节本描述符无 0xB1 Feature 项结语本文主要学习了一下 HID 设备描述符在实际应用中大部分情况下我们其实不需要从头开始写一个 HID 报告描述符的我们在能够读懂的前提下只需要做一些简单的修改就能够在自己的应用中使用起来了。好了本文就到这里。谢谢大家