人机接口设备协议(HID)
摘要 蓝牙HID协议定义了人机接口设备的通信规范,包含两种角色:HID Host(接收设备,如电脑)和HID Device(输入设备,如键盘)。数据流向分为Input Report(设备→主机)、Output Report(主机→设备)和Feature Report(双向配置)。实现方式包括经典蓝牙的HID over L2CAP(使用PSM 0x0011控制通道和0x0013中断通道)和BLE的H
人机接口设备协议(HID)
本笔记为作者再学习蓝牙Host协议栈的一些心得体会,如有不对的地方,请包含与谅解!
————by wsoz
人机接口设备协议(HID)
HID协议定义了人机接口设备中的协议、特征和使用规程,是在USB HID协议上进行的改进。
HID协议角色
HID协议定义了两种角色:
| 角色 | 名称 | 说明 | 典型设备 |
|---|---|---|---|
| HID Host | 主机 | 接收并处理HID数据的设备 | 电脑、手机、平板、游戏主机 |
| HID Device | 设备 | 产生并发送HID数据的设备 | 键盘、鼠标、游戏手柄、遥控器 |
角色关系
┌─────────────┐ ┌─────────────┐
│ HID Device │ ── HID Reports ──→ │ HID Host │
│ (键盘) │ ←── Output/Feature │ (电脑) │
└─────────────┘ └─────────────┘
数据流向
| 方向 | Report类型 | 说明 |
|---|---|---|
| Device → Host | Input Report | 输入数据(按键、鼠标移动等) |
| Host → Device | Output Report | 输出数据(如键盘LED状态) |
| 双向 | Feature Report | 配置/状态信息 |
与L2CAP的关系
蓝牙HID有两种实现方式:
| 方式 | 底层协议 | 说明 |
|---|---|---|
| HID over L2CAP | 直接基于L2CAP | 经典蓝牙HID(BR/EDR),使用PSM 0x0011和0x0013 |
| HOGP | 基于GATT/ATT | 低功耗蓝牙HID(BLE),HID over GATT Profile |
HID SDP服务发现
在建立HID连接之前,Host需要通过SDP查询Device的HID服务记录,获取关键信息(如HID Descriptor、PSM等)。
SDP角色说明
SDP是服务发现协议,本质是"服务发布与查询":
| 角色 | SDP行为 | 说明 |
|---|---|---|
| HID Device | 发布HID服务记录 | 告诉别人"我是HID设备,这是我的Report Descriptor" |
| HID Host | 查询HID服务记录 | 问对方"你是HID设备吗?给我你的Report Descriptor" |
重点:只有HID Device有HID SDP服务记录(被查询方),HID Host没有HID SDP记录但会查询Device的记录(查询方)。类似餐厅有菜单,顾客看菜单点菜。
HID服务UUID
| UUID | 名称 | 说明 |
|---|---|---|
| 0x1124 | HumanInterfaceDeviceService | HID服务类UUID |
HID SDP属性
HID Device的SDP记录包含以下属性:
| 属性ID | 名称 | 类型 | 说明 |
|---|---|---|---|
| 0x0200 | HIDDeviceReleaseNumber | uint16 | 设备版本号 |
| 0x0201 | HIDParserVersion | uint16 | HID解析器版本(通常0x0111=1.1.1) |
| 0x0202 | HIDDeviceSubclass | uint8 | 设备子类(键盘0x40、鼠标0x80、组合0xC0) |
| 0x0203 | HIDCountryCode | uint8 | 国家码(键盘布局,0=不指定) |
| 0x0204 | HIDVirtualCable | bool | 是否支持虚拟线缆 |
| 0x0205 | HIDReconnectInitiate | bool | 设备是否会主动重连 |
| 0x0206 | HIDDescriptorList | sequence | 最重要,包含Report Descriptor |
| 0x0207 | HIDLANGIDBaseList | sequence | 语言ID列表 |
| 0x0208 | HIDBatteryPower | bool | 是否电池供电 |
| 0x0209 | HIDRemoteWake | bool | 是否支持远程唤醒 |
| 0x020B | HIDProfileVersion | uint16 | HID Profile版本(0x0101=1.1) |
| 0x020C | HIDSupervisionTimeout | uint16 | 监督超时(单位:基带slot) |
| 0x020D | HIDNormallyConnectable | bool | 是否通常可连接 |
| 0x020E | HIDBootDevice | bool | 是否支持Boot Protocol |
HIDDescriptorList结构
这是最关键的属性,Host从这里获取Report Descriptor:
HIDDescriptorList = sequence {
sequence {
uint8 descriptor_type, // 0x22 = Report Descriptor
string descriptor_data // Report Descriptor二进制数据
},
... // 可以有多个描述符
}
| descriptor_type | 说明 |
|---|---|
| 0x22 | Report Descriptor |
| 0x23 | Physical Descriptor(极少用) |
Host查询HID服务流程
HID Host HID Device
│ │
│ ① SDP连接 │
│ ─────────────────────────────────────→│
│ │
│ ② ServiceSearchAttributeRequest │ 查询HID服务
│ ServiceClassID = 0x1124 │
│ ─────────────────────────────────────→│
│ │
│ ③ ServiceSearchAttributeResponse │ 返回服务记录
│ - ProtocolDescriptorList │ (PSM信息)
│ - HIDDescriptorList │ (Report Descriptor)
│ - HIDDeviceSubclass │ (设备类型)
│ ←─────────────────────────────────────│
│ │
│ ④ 解析获取Report Descriptor │
│ │
│ ⑤ 建立L2CAP连接... │
Host通过SDP获取Report Descriptor后,才能正确解析后续收到的HID Report数据。
HID逻辑链路
经典蓝牙HID在L2CAP层建立两条逻辑通道:
| 通道 | PSM | 名称 | 用途 |
|---|---|---|---|
| HID Control | 0x0011 | 控制通道 | 连接管理、GET/SET请求、握手响应 |
| HID Interrupt | 0x0013 | 中断通道 | 传输HID Report数据(Input/Output) |
通道关系图
┌─────────────────────────────────────────────────────────┐
│ HID Layer │
├────────────────────────┬────────────────────────────────┤
│ HID Control Channel │ HID Interrupt Channel │
│ (PSM 0x0011) │ (PSM 0x0013) │
├────────────────────────┴────────────────────────────────┤
│ L2CAP │
└─────────────────────────────────────────────────────────┘
两通道的区别
| 特性 | Control Channel | Interrupt Channel |
|---|---|---|
| 连接顺序 | 先建立 | 后建立 |
| 数据类型 | 控制命令、握手 | Report数据 |
| 传输方式 | 请求-响应模式 | 异步传输 |
| 典型操作 | GET_REPORT、SET_REPORT | INPUT报告、OUTPUT报告 |
连接建立流程
HID Device HID Host
│ │
│ ① L2CAP Connect (PSM=0x0011) │ 建立Control通道
│ ←───────────────────────────────────→ │
│ │
│ ② L2CAP Connect (PSM=0x0013) │ 建立Interrupt通道
│ ←───────────────────────────────────→ │
│ │
│ ③ HID Control: 获取HID描述符等 │ 配置阶段
│ ←───────────────────────────────────→ │
│ │
│ ④ HID Interrupt: Input Reports │ 数据传输阶段
│ ─────────────────────────────────────→│
注意:必须先建立Control通道,再建立Interrupt通道。断开时顺序相反。
HID封包格式
HID封包由Header和可选的Payload组成:
┌──────────────────┬─────────────────────┐
│ Header │ Payload │
│ (1字节) │ (0-N字节) │
└──────────────────┴─────────────────────┘
Header字段结构
Bit: 7 6 5 4 3 2 1 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ Message Type(4bit) │ Parameter(4bit) │
└────┴────┴────┴────┴────┴────┴────┴────┘
| 字段 | 位置 | 说明 |
|---|---|---|
| Message Type | Bit 7-4 | 消息类型,定义操作类型 |
| Parameter | Bit 3-0 | 参数,含义随消息类型变化 |
消息类型列表
| Message Type | 值 | 名称 | 方向 | 通道 | 说明 |
|---|---|---|---|---|---|
| HANDSHAKE | 0x0 | 握手 | Device→Host | Control | 响应Host的请求,返回结果码 |
| HID_CONTROL | 0x1 | 控制 | 双向 | Control | 控制命令(挂起、退出挂起、虚拟线缆拔出) |
| GET_REPORT | 0x4 | 获取报告 | Host→Device | Control | Host请求获取指定Report |
| SET_REPORT | 0x5 | 设置报告 | Host→Device | Control | Host发送Report给Device |
| GET_PROTOCOL | 0x6 | 获取协议 | Host→Device | Control | 查询当前协议模式 |
| SET_PROTOCOL | 0x7 | 设置协议 | Host→Device | Control | 设置协议模式(Boot/Report) |
| DATA | 0xA | 数据 | 双向 | Interrupt | 传输Input/Output Report数据 |
HANDSHAKE响应码
HANDSHAKE是Device回复Host在Control通道上请求的机制:
| Host请求 | 成功时回复 | 失败时回复 |
|---|---|---|
| GET_REPORT | DATA(直接返回数据) | HANDSHAKE(错误码) |
| GET_PROTOCOL | DATA(直接返回数据) | HANDSHAKE(错误码) |
| SET_REPORT | HANDSHAKE(成功码) | HANDSHAKE(错误码) |
| SET_PROTOCOL | HANDSHAKE(成功码) | HANDSHAKE(错误码) |
规律:GET类请求成功时用DATA返回数据,不发HANDSHAKE;SET类请求无论成功失败都用HANDSHAKE回复。
成功的GET_REPORT:
Host ──GET_REPORT──→ Device
Host ←──DATA──────── Device (直接返回数据)
失败的GET_REPORT:
Host ──GET_REPORT──→ Device
Host ←──HANDSHAKE── Device (返回错误码)
SET_REPORT:
Host ──SET_REPORT──→ Device
Host ←──HANDSHAKE── Device (返回成功/失败)
HANDSHAKE的Parameter值定义:
| Parameter值 | 名称 | 说明 |
|---|---|---|
| 0x0 | SUCCESSFUL | 成功 |
| 0x1 | NOT_READY | 设备未就绪 |
| 0x2 | ERR_INVALID_REPORT_ID | 无效的Report ID |
| 0x3 | ERR_UNSUPPORTED_REQUEST | 不支持的请求 |
| 0x4 | ERR_INVALID_PARAMETER | 无效参数 |
| 0xE | ERR_UNKNOWN | 未知错误 |
| 0xF | ERR_FATAL | 致命错误 |
示例:HANDSHAKE成功响应
Header: 0x00
├── Message Type = 0x0 (HANDSHAKE)
└── Parameter = 0x0 (SUCCESSFUL)
HID_CONTROL操作码
| Parameter值 | 名称 | 说明 |
|---|---|---|
| 0x0 | ||
| 0x1 | ||
| 0x2 | ||
| 0x3 | SUSPEND | 挂起(进入低功耗) |
| 0x4 | EXIT_SUSPEND | 退出挂起 |
| 0x5 | VIRTUAL_CABLE_UNPLUG | 虚拟线缆拔出(断开连接) |
前三个从USB HID继承而来,在蓝牙HID中无实际意义,已弃用。实际只用后三个。
SUSPEND(挂起):Host一段时间不使用设备时发送,通知Device进入低功耗模式(如降低扫描频率、关闭LED等)。
EXIT_SUSPEND(退出挂起):恢复设备到正常工作状态。Host或Device都可以发起——例如用户按下键盘任意键唤醒时,Device主动发送EXIT_SUSPEND。
VIRTUAL_CABLE_UNPLUG(虚拟线缆拔出):模拟USB拔线动作,逻辑上断开HID连接。收到后双方断开L2CAP连接,效果等同于物理拔掉USB线。
GET_REPORT / SET_REPORT
Parameter字段的低2位表示Report类型:
| Parameter[1:0] | Report类型 |
|---|---|
| 0x1 | Input Report |
| 0x2 | Output Report |
| 0x3 | Feature Report |
Report ID说明
当一个HID设备支持多种Report时,需要用Report ID区分:
| 场景 | Report ID字段 |
|---|---|
| 设备只有1种Report | 省略(不传,节省1字节) |
| 设备有多种Report | 必须传(1字节,值由HID Descriptor定义) |
典型多Report设备示例(多功能键盘):
| Report ID | 用途 | 说明 |
|---|---|---|
| 0x01 | 标准按键 | A-Z、0-9、F1-F12等 |
| 0x02 | 媒体键 | 音量+/-、播放、暂停、下一曲 |
| 0x03 | 系统控制 | 电源、睡眠、唤醒 |
Report ID的值和含义由设备的HID Descriptor定义,Host通过SDP获取。
GET_REPORT请求格式
┌────────────┬─────────────┬────────────────┐
│ Header │ Report ID │ Buffer Size │
│ (1字节) │ (0或1字节) │ (2字节) │
└────────────┴─────────────┴────────────────┘
| 字段 | 说明 |
|---|---|
| Header | 0x4x,低2位表示Report类型 |
| Report ID | 可选,多Report设备必须指定 |
| Buffer Size | 2字节小端序,Host告诉Device缓冲区大小 |
示例:GET_REPORT请求获取键盘状态
Header: 0x41 → GET_REPORT + Input Report
Report ID: 0x01 → 键盘Report
Buffer Size: 0x08 0x00 → 8字节缓冲区(小端序)
SET_REPORT请求格式
┌────────────┬─────────────┬────────────────┐
│ Header │ Report ID │ Report Data │
│ (1字节) │ (0或1字节) │ (N字节) │
└────────────┴─────────────┴────────────────┘
| 字段 | 说明 |
|---|---|
| Header | 0x5x,低2位表示Report类型 |
| Report ID | 可选,多Report设备必须指定 |
| Report Data | 要设置的Report数据 |
示例:SET_REPORT设置键盘LED状态(Caps Lock亮)
Header: 0x52 → SET_REPORT + Output Report
Report ID: 0x01 → 键盘Report
Report Data: 0x02 → Bit1=1 表示Caps Lock LED亮
GET_PROTOCOL / SET_PROTOCOL
HID定义了两种协议模式:
| 模式 | 值 | 说明 |
|---|---|---|
| Boot Protocol | 0x00 | 固定格式,用于BIOS/启动阶段 |
| Report Protocol | 0x01 | 完整格式,由HID Descriptor定义(默认模式) |
为什么需要Boot Protocol?
┌─────────────────────────────────────────────────────────────┐
│ BIOS启动阶段 │
│ ┌─────────────┐ │
│ │ 没有完整的 │ 无法解析复杂的HID Descriptor │
│ │ HID驱动程序 │ ────→ 使用固定格式的Boot Protocol │
│ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 操作系统启动后 │
│ ┌─────────────┐ │
│ │ 完整HID驱动 │ 可以解析HID Descriptor │
│ │ 已加载 │ ────→ 切换到Report Protocol │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
封包格式
| 操作 | Header | Payload |
|---|---|---|
| GET_PROTOCOL | 0x60 | 无 |
| SET_PROTOCOL (Boot) | 0x70 | 无 |
| SET_PROTOCOL (Report) | 0x71 | 无 |
协议切换流程
HID Host HID Device
│ │
│ GET_PROTOCOL (Header: 0x60) │ 查询当前协议
│ ─────────────────────────────────────→│
│ │
│ DATA (Header: 0xA0, Data: 0x01) │ 返回:Report Protocol
│ ←─────────────────────────────────────│
│ │
│ SET_PROTOCOL (Header: 0x70) │ 切换到Boot Protocol
│ ─────────────────────────────────────→│
│ │
│ HANDSHAKE (Header: 0x00) │ 确认成功
│ ←─────────────────────────────────────│
注意:Boot Protocol只支持键盘和鼠标,其他HID设备(如游戏手柄)没有Boot Protocol。
DATA
DATA消息用于在Interrupt通道上传输Report数据:
┌────────────┬─────────────┬────────────────┐
│ Header │ Report ID │ Report Data │
│ 0xAx │ (0或1字节) │ (N字节) │
└────────────┴─────────────┴────────────────┘
| Header值 | 含义 |
|---|---|
| 0xA1 | Input Report(Device→Host) |
| 0xA2 | Output Report(Host→Device) |
| 0xA3 | Feature Report |
Report Data格式
Report Data的具体格式由**HID Descriptor(HID描述符)**定义。以下是常见设备的Boot Protocol标准格式:
键盘Boot Protocol Report(8字节):
字节: 0 1 2 3 4 5 6 7
┌────────┬────────┬────┬────┬────┬────┬────┬────┐
│Modifier│Reserved│Key0│Key1│Key2│Key3│Key4│Key5│
└────────┴────────┴────┴────┴────┴────┴────┴────┘
| 字段 | 字节 | 说明 |
|---|---|---|
| Modifier | 0 | 修饰键位图(Ctrl/Shift/Alt/GUI) |
| Reserved | 1 | 保留,固定为0x00 |
| Key0-Key5 | 2-7 | 最多同时按下6个普通键的键码 |
Modifier位定义:
| Bit | 键 | Bit | 键 |
|---|---|---|---|
| 0 | Left Ctrl | 4 | Right Ctrl |
| 1 | Left Shift | 5 | Right Shift |
| 2 | Left Alt | 6 | Right Alt |
| 3 | Left GUI(Win) | 7 | Right GUI |
鼠标Boot Protocol Report(3字节):
字节: 0 1 2
┌────────┬────────┬────────┐
│Buttons │ X移动 │ Y移动 │
└────────┴────────┴────────┘
| 字段 | 字节 | 说明 |
|---|---|---|
| Buttons | 0 | 按键位图(Bit0=左键,Bit1=右键,Bit2=中键) |
| X移动 | 1 | X轴相对移动量(有符号,-127~+127) |
| Y移动 | 2 | Y轴相对移动量(有符号,-127~+127) |
Boot Protocol格式是固定的,Report Protocol格式由HID Descriptor定义,可以更复杂(如支持滚轮、更高精度等)。
示例
键盘按下’A’键的Input Report:
Header: 0xA1 → DATA + Input Report
Report ID: 0x01 → 键盘Report ID
Data: 00 00 04 00 00 00 00 00
│ │
│ └── 按键码 0x04 = 'A'
└── Modifier = 0 (无修饰键)
键盘按下Ctrl+C的Input Report:
Header: 0xA1
Report ID: 0x01
Data: 01 00 06 00 00 00 00 00
│ │
│ └── 按键码 0x06 = 'C'
└── Modifier = 0x01 (Left Ctrl)
HID描述符
HID描述符(HID Descriptor)是HID协议的核心,它告诉Host设备能产生什么样的数据、数据的格式是什么。Host通过SDP获取HID描述符后,才能正确解析Report数据。
描述符类型
蓝牙HID中涉及三种描述符(继承自USB HID):
| 描述符 | 说明 |
|---|---|
| HID Descriptor | 顶层描述符,包含版本号、国家码、以及下级描述符的数量和长度 |
| Report Descriptor | 最重要,定义Report的具体格式(有哪些字段、每个字段多少位、含义是什么) |
| Physical Descriptor | 可选,描述物理设备信息(极少使用) |
实际开发中最关键的是Report Descriptor,它决定了Report Data的解析方式。
Report Descriptor结构
Report Descriptor由一系列**Item(项)**组成,每个Item描述Report的一个属性:
┌────────┬────────┬────────┬─── ─── ───┬────────┐
│ Item 1 │ Item 2 │ Item 3 │ ... │ Item N │
└────────┴────────┴────────┴─── ─── ───┴────────┘
Item格式
每个Item由前缀字节和可选的数据字节组成:
短Item格式(最常用):
Bit: 7 6 5 4 3 2 1 0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ Tag (4bit) │ Type(2bit)│Size(2bit)│
└────┴────┴────┴────┴────┴────┴────┴────┘
后跟 0/1/2/4 字节数据
| 字段 | 位置 | 说明 |
|---|---|---|
| Size | Bit 1-0 | 后续数据长度:0=0字节,1=1字节,2=2字节,3=4字节 |
| Type | Bit 3-2 | Item类型:0=Main,1=Global,2=Local |
| Tag | Bit 7-4 | 具体功能标识 |
Item类型
| Type | 名称 | 说明 |
|---|---|---|
| Main | 主项 | 定义Report的数据结构(Input/Output/Feature/Collection/End Collection) |
| Global | 全局项 | 设置全局属性(Usage Page、Logical Min/Max、Report Size/Count等) |
| Local | 局部项 | 设置局部属性(Usage、Usage Min/Max等),仅对下一个Main Item有效 |
三者区别:
| Global | Local | Main | |
|---|---|---|---|
| 作用 | 设置属性 | 设置属性 | 定义字段 |
| 有效期 | 一直有效,直到被覆盖 | 仅对下一个Main有效 | - |
简单理解:Global和Local都是"准备工作"(设置属性),Main才是"真正干活"(定义数据字段)。Global设置后一直保持,Local用一次就失效。
示例:
75 08 → [Global] Report Size = 8 // 设置:每个字段8位(一直有效)
95 06 → [Global] Report Count = 6 // 设置:共6个字段(一直有效)
19 00 → [Local] Usage Min = 0 // 临时:用途从0开始(只对下一个Main有效)
29 65 → [Local] Usage Max = 0x65 // 临时:用途到0x65(只对下一个Main有效)
81 00 → [Main] Input // 定义:6×8bit=6字节Input字段
// Local失效,Global保持
常用Item标签
注意:Tag的含义取决于Type,同样的Tag值在不同Type下含义不同。例如Tag=0x0在Global下是Usage Page,在Local下是Usage。所以下面按Type分类列出。
Main Item(Type=0x0)
| Tag | 名称 | 说明 |
|---|---|---|
| 0x8 | Input | 定义Input Report的数据字段 |
| 0x9 | Output | 定义Output Report的数据字段 |
| 0xB | Feature | 定义Feature Report的数据字段 |
| 0xA | Collection | 开始一个数据集合 |
| 0xC | End Collection | 结束一个数据集合 |
后续数据字节Input/Output/Feature的数据位含义:
| Bit | 值=0 | 值=1 |
|---|---|---|
| 0 | Data(数据) | Constant(常量) |
| 1 | Array(数组) | Variable(变量) |
| 2 | Absolute(绝对值) | Relative(相对值) |
各属性区别:
| 属性 | 区别 | 典型应用 |
|---|---|---|
| Data vs Constant | Data=实际有意义的数据;Constant=填充/保留位,Host忽略 | Reserved字段用Constant |
| Array vs Variable | Array=字段值是"哪个被选中"(如键码);Variable=每个bit/字段是独立开关 | 键码用Array,Modifier用Variable |
| Absolute vs Relative | Absolute=当前绝对值;Relative=相对上次的变化量 | 鼠标移动用Relative,摇杆位置用Absolute |
示例对比:
键盘Modifier(8个独立开关):
81 02 → Input (Data, Variable, Absolute)
每个bit独立表示一个修饰键是否按下:Bit0=LeftCtrl, Bit1=LeftShift...
键盘Key Codes(6个键码槽位):
81 00 → Input (Data, Array)
每个字节的值表示"哪个键被按下":0x04=A, 0x05=B...
鼠标移动(相对位移):
81 06 → Input (Data, Variable, Relative)
值表示相对上次的移动量:+5表示向右移动5个单位
Global Item(Type=0x1)
| Tag | 名称 | 说明 |
|---|---|---|
| 0x0 | Usage Page | 用途页(如:通用桌面、键盘、LED等) |
| 0x1 | Logical Minimum | 逻辑最小值 |
| 0x2 | Logical Maximum | 逻辑最大值 |
| 0x7 | Report Size | 每个数据字段的位数 |
| 0x9 | Report Count | 数据字段的个数 |
| 0x8 | Report ID | Report ID值 |
Local Item(Type=0x2)
| Tag | 名称 | 说明 |
|---|---|---|
| 0x0 | Usage | 具体用途(如:键盘、鼠标、X轴、Y轴等) |
| 0x1 | Usage Minimum | 用途范围最小值 |
| 0x2 | Usage Maximum | 用途范围最大值 |
对比:Tag=0x0时,Global是Usage Page(用途页),Local是Usage(具体用途);Tag=0x1时,Global是Logical Minimum,Local是Usage Minimum。
常用Usage Page
| 值 | Usage Page | 说明 |
|---|---|---|
| 0x01 | Generic Desktop | 通用桌面(键盘、鼠标、游戏手柄等设备类型) |
| 0x07 | Keyboard/Keypad | 键盘按键码 |
| 0x08 | LED | LED指示灯(Caps Lock、Num Lock等) |
| 0x09 | Button | 按钮(鼠标按键等) |
| 0x0C | Consumer | 消费类设备(媒体键、音量等) |
Report Descriptor编写模板
第1步:声明设备类型
├── Usage Page (这是什么类型的设备页)
└── Usage (具体是什么设备)
第2步:开始集合
└── Collection (Application) ←── 开始
第3步:定义每个字段(重复N次)
│
├── 【准备属性 - Global】
│ Usage Page = ? // 这个字段属于哪类
│ Logical Min/Max = ? // 值的范围
│ Report Size = ? // 每个字段多少位
│ Report Count = ? // 几个这样的字段
│
├── 【准备用途 - Local】
│ Usage 或 Usage Min/Max // 具体什么用途
│
└── 【定义字段 - Main】
Input/Output/Feature // 真正创建字段!
第4步:结束集合
└── End Collection ←── 结束
编写步骤总结:
| 步骤 | 做什么 | 用什么Item |
|---|---|---|
| 1 | 声明设备类型 | Global(Usage Page) + Local(Usage) |
| 2 | 开始集合 | Main(Collection) |
| 3 | 设置属性 | Global(Size/Count/Min/Max) |
| 4 | 设置用途 | Local(Usage) |
| 5 | 创建字段 | Main(Input/Output/Feature) |
| 6 | 重复3-5定义更多字段 | … |
| 7 | 结束集合 | Main(End Collection) |
记住:Global和Local都是"准备工作",只有遇到Main(Input/Output/Feature)时才真正创建字段!
简单示例:2个按钮的设备
05 09 → Usage Page (Button) // 第1步:按钮类型
09 01 → Usage (Button 1) // 这是按钮设备
A1 01 → Collection (Application) // 第2步:开始集合
15 00 → Logical Minimum (0) // 准备Global:值范围0-1
25 01 → Logical Maximum (1)
75 01 → Report Size (1) // 每个字段1位
95 02 → Report Count (2) // 2个字段
09 01 → Usage (Button 1) // 准备Local:按钮1
09 02 → Usage (Button 2) // 准备Local:按钮2
81 02 → Input (Data, Variable) // 定义!创建2×1bit的Input
C0 → End Collection // 第4步:结束
结果:Report Data = 1字节,低2位分别表示按钮1和按钮2的状态。
Report Descriptor实例
以标准键盘的Boot Protocol Report为例,解读其Report Descriptor:
05 01 → Usage Page (Generic Desktop) // 用途页:通用桌面
09 06 → Usage (Keyboard) // 用途:键盘
A1 01 → Collection (Application) // 开始应用集合
// ===== Modifier Keys(修饰键,1字节) =====
05 07 → Usage Page (Keyboard/Keypad) // 用途页:键盘键码
19 E0 → Usage Minimum (0xE0 = Left Ctrl) // 起始:Left Ctrl
29 E7 → Usage Maximum (0xE7 = Right GUI) // 结束:Right GUI
15 00 → Logical Minimum (0) // 逻辑最小值:0
25 01 → Logical Maximum (1) // 逻辑最大值:1
75 01 → Report Size (1) // 每个字段1位
95 08 → Report Count (8) // 共8个字段
81 02 → Input (Data, Variable, Absolute) // 定义为Input:8×1bit=1字节
// ===== Reserved(保留,1字节) =====
95 01 → Report Count (1) // 1个字段
75 08 → Report Size (8) // 每个字段8位
81 01 → Input (Constant) // 定义为常量Input:1×8bit=1字节
// ===== LED Output(LED状态,1字节) =====
05 08 → Usage Page (LED) // 用途页:LED
19 01 → Usage Minimum (Num Lock) // 起始:Num Lock
29 05 → Usage Maximum (Kana) // 结束:Kana
95 05 → Report Count (5) // 5个LED
75 01 → Report Size (1) // 每个1位
91 02 → Output (Data, Variable, Absolute) // 定义为Output:5bit
95 01 → Report Count (1) // 填充
75 03 → Report Size (3) // 3位补齐到1字节
91 01 → Output (Constant) // 填充位
// ===== Key Codes(按键码,6字节) =====
05 07 → Usage Page (Keyboard/Keypad) // 用途页:键盘键码
19 00 → Usage Minimum (0) // 起始:0
29 65 → Usage Maximum (0x65) // 结束:0x65
15 00 → Logical Minimum (0) // 逻辑最小值
25 65 → Logical Maximum (0x65) // 逻辑最大值
75 08 → Report Size (8) // 每个字段8位
95 06 → Report Count (6) // 共6个字段
81 00 → Input (Data, Array) // 定义为Input数组:6×8bit=6字节
C0 → End Collection // 结束集合
这个Descriptor定义的Report格式正好对应前面介绍的键盘Boot Protocol Report:1字节Modifier + 1字节Reserved + 6字节Key Codes,同时还定义了1字节LED Output Report。
实际Report数据示例
根据上面的Descriptor,定义了两种Report:
注意:Header由协议栈自动填充,调用
hid_device_interupt_report()时只需传入Data部分。
Input Report(Device→Host,8字节):
[Modifier] [Reserved] [KeyCode0] [KeyCode1] [KeyCode2] [KeyCode3] [KeyCode4] [KeyCode5]
Output Report(Host→Device,1字节):
[LED]
场景1:按下 ‘A’ 键
Header: 0xA1
Data: 00 00 04 00 00 00 00 00
│ │ │
│ │ └── 0x04 = 'A'
│ └── Reserved 固定0
└── Modifier = 0(无修饰键)
场景2:按下 Shift+A(大写A)
Header: 0xA1
Data: 02 00 04 00 00 00 00 00
│ │
│ └── 0x04 = 'A'
└── 0x02 = Bit1=1 = Left Shift
场景3:同时按下 A、B、C
Header: 0xA1
Data: 00 00 04 05 06 00 00 00
│ │ │
│ │ └── 0x06 = 'C'
│ └── 0x05 = 'B'
└── 0x04 = 'A'
场景4:松开所有键
Header: 0xA1
Data: 00 00 00 00 00 00 00 00
每次按键必须发两包:按下报文 + 松开报文,否则Host会认为持续按着。
Modifier位定义:
Bit0=Left Ctrl Bit4=Right Ctrl
Bit1=Left Shift Bit5=Right Shift
Bit2=Left Alt Bit6=Right Alt
Bit3=Left GUI Bit7=Right GUI
该Descriptor同时定义了1字节LED Output Report(Host→Device),用于控制键盘指示灯:
场景5:Host点亮 Caps Lock
Header: 0xA2
Data: 02
│
└── Bit1=1 = Caps Lock亮
LED位定义(低5位有效,高3位为填充):
Bit0 = Num Lock
Bit1 = Caps Lock
Bit2 = Scroll Lock
Bit3 = Compose
Bit4 = Kana
流程总结
本节总结HID Host与Device之间的完整交互流程。
整体流程概览
┌──────────────────────────────────────────────────────────────────────────┐
│ HID 连接完整流程 │
├──────────────────────────────────────────────────────────────────────────┤
│ ① 底层连接 │ Inquiry → Paging → LMP → Baseband连接建立 │
├──────────────────────────────────────────────────────────────────────────┤
│ ② 服务发现 │ SDP查询 → 获取HID服务记录 → 解析Report Descriptor │
├──────────────────────────────────────────────────────────────────────────┤
│ ③ 通道建立 │ L2CAP Control(0x0011) → L2CAP Interrupt(0x0013) │
├──────────────────────────────────────────────────────────────────────────┤
│ ④ HID配置 │ SET_PROTOCOL(可选) → 准备就绪 │
├──────────────────────────────────────────────────────────────────────────┤
│ ⑤ 数据传输 │ Input Report ←→ Output Report │
├──────────────────────────────────────────────────────────────────────────┤
│ ⑥ 断开连接 │ VIRTUAL_CABLE_UNPLUG 或 L2CAP断开 │
└──────────────────────────────────────────────────────────────────────────┘
详细交互流程
以蓝牙键盘连接电脑为例:
电脑 (HID Host) 蓝牙键盘 (HID Device)
│ │
═══════════════════════════════ 阶段①:底层连接 ═══════════════════════════════
│ │
│ Inquiry (查找设备) │
│ ────────────────────────────────────────────→│
│ │
│ Inquiry Response (我是键盘,地址XX:XX:XX) │
│ ←────────────────────────────────────────────│
│ │
│ Paging + LMP + Baseband连接 │
│ ←──────────────────────────────────────────→ │
│ │
═══════════════════════════════ 阶段②:服务发现 ═══════════════════════════════
│ │
│ L2CAP Connect (PSM=0x0001, SDP) │
│ ────────────────────────────────────────────→│
│ L2CAP Connect Response │
│ ←────────────────────────────────────────────│
│ │
│ SDP ServiceSearchAttributeRequest │
│ 查询: UUID=0x1124 (HID) │
│ ────────────────────────────────────────────→│
│ │
│ SDP ServiceSearchAttributeResponse │
│ 返回: HIDDescriptorList (Report Desc) │
│ HIDDeviceSubclass = 0x40 (键盘) │
│ HIDBootDevice = true │
│ ←────────────────────────────────────────────│
│ │
│ L2CAP Disconnect (SDP通道) │
│ ←──────────────────────────────────────────→ │
│ │
│ 【Host解析Report Descriptor,了解键盘Report格式】
│ │
═══════════════════════════════ 阶段③:HID通道建立 ═════════════════════════════
│ │
│ L2CAP Connect (PSM=0x0011, Control) │ 建立控制通道
│ ────────────────────────────────────────────→│
│ L2CAP Connect Response │
│ ←────────────────────────────────────────────│
│ L2CAP Config Request/Response │
│ ←──────────────────────────────────────────→ │
│ │
│ L2CAP Connect (PSM=0x0013, Interrupt) │ 建立中断通道
│ ────────────────────────────────────────────→│
│ L2CAP Connect Response │
│ ←────────────────────────────────────────────│
│ L2CAP Config Request/Response │
│ ←──────────────────────────────────────────→ │
│ │
═══════════════════════════════ 阶段④:HID配置(可选) ═══════════════════════════
│ │
│ 【如果在BIOS阶段,可能需要切换到Boot Protocol】
│ │
│ SET_PROTOCOL (Header=0x70, Boot Protocol) │ Control通道
│ ────────────────────────────────────────────→│
│ HANDSHAKE (Header=0x00, 成功) │
│ ←────────────────────────────────────────────│
│ │
│ 【正常OS环境下使用默认的Report Protocol,无需SET_PROTOCOL】
│ │
═══════════════════════════════ 阶段⑤:数据传输 ════════════════════════════════
│ │
│ ══════ 用户按下 'A' 键 ══════ │
│ │
│ DATA Input Report (Header=0xA1) │ Interrupt通道
│ Data: 00 00 04 00 00 00 00 00 │ 按下A键
│ ←────────────────────────────────────────────│
│ │
│ DATA Input Report (Header=0xA1) │
│ Data: 00 00 00 00 00 00 00 00 │ 释放A键
│ ←────────────────────────────────────────────│
│ │
│ ══════ 用户按下 Caps Lock ══════ │
│ │
│ DATA Input Report (Header=0xA1) │
│ Data: 00 00 39 00 00 00 00 00 │ 按下CapsLock
│ ←────────────────────────────────────────────│
│ │
│ DATA Output Report (Header=0xA2) │ Interrupt通道
│ Data: 02 │ 点亮CapsLock LED
│ ────────────────────────────────────────────→│
│ │
│ ══════ 一段时间无操作 ══════ │
│ │
│ HID_CONTROL (Header=0x13, SUSPEND) │ Control通道
│ ────────────────────────────────────────────→│ 通知键盘进入低功耗
│ │
│ ══════ 用户按任意键唤醒 ══════ │
│ │
│ HID_CONTROL (Header=0x14, EXIT_SUSPEND) │ Control通道
│ ←────────────────────────────────────────────│ 键盘请求退出挂起
│ │
═══════════════════════════════ 阶段⑥:断开连接 ════════════════════════════════
│ │
│ 【方式A:虚拟线缆拔出】 │
│ HID_CONTROL (Header=0x15, VIRTUAL_CABLE_UNPLUG)
│ ────────────────────────────────────────────→│
│ │
│ 【方式B:直接断开L2CAP】 │
│ L2CAP Disconnect (Interrupt通道) │ 先断Interrupt
│ ←──────────────────────────────────────────→ │
│ L2CAP Disconnect (Control通道) │ 再断Control
│ ←──────────────────────────────────────────→ │
│ │
各阶段要点总结
| 阶段 | 关键操作 | 涉及协议/通道 | 说明 |
|---|---|---|---|
| ①底层连接 | Inquiry、Paging、LMP | Baseband | 建立蓝牙物理连接 |
| ②服务发现 | SDP查询HID服务 | L2CAP PSM=0x0001 | 获取Report Descriptor |
| ③通道建立 | 建立Control→Interrupt | L2CAP PSM=0x0011/0x0013 | 必须先Control后Interrupt |
| ④HID配置 | SET_PROTOCOL(可选) | HID Control通道 | BIOS阶段可能需要Boot Protocol |
| ⑤数据传输 | Input/Output Report | HID Interrupt通道 | 主要数据交互阶段 |
| ⑥断开连接 | VIRTUAL_CABLE_UNPLUG或L2CAP断开 | Control通道/L2CAP | 先断Interrupt后断Control |
实际抓包示例
以下是一个简化的HID键盘连接抓包示例(仅展示HID层):
# 1. Control通道建立后,Host可能查询当前协议模式
[Host → Device] GET_PROTOCOL
Header: 0x60
[Device → Host] DATA (返回当前协议)
Header: 0xA0
Data: 0x01 (Report Protocol)
# 2. 用户按下 Ctrl+C(复制)
[Device → Host] DATA Input Report
Header: 0xA1
Data: 01 00 06 00 00 00 00 00
│ │
│ └── Key Code 0x06 = 'C'
└── Modifier 0x01 = Left Ctrl
# 3. 用户释放按键
[Device → Host] DATA Input Report
Header: 0xA1
Data: 00 00 00 00 00 00 00 00 (全0表示无按键)
# 4. Host设置键盘LED(Num Lock + Caps Lock亮)
[Host → Device] DATA Output Report
Header: 0xA2
Data: 03 (Bit0=NumLock, Bit1=CapsLock)
常见场景处理
| 场景 | Host行为 | Device行为 |
|---|---|---|
| 首次配对连接 | SDP查询→保存Report Descriptor→建立通道 | 响应SDP→等待连接 |
| 重新连接(已配对) | 可跳过SDP(使用缓存的Descriptor)→直接建立通道 | 主动发起连接或等待连接 |
| BIOS/启动阶段 | SET_PROTOCOL切换到Boot Protocol | 切换到固定8字节格式 |
| 休眠唤醒 | 发送EXIT_SUSPEND或等待Device唤醒 | 发送EXIT_SUSPEND唤醒Host |
| 设备断开 | 清理连接状态,等待重连 | 可主动重连(如果HIDReconnectInitiate=true) |
HID应用理解
首先对应HID键盘等,我们就通过HID描述符对我们的report字段进行定义
static uint8_t hid_device_descriptor[] =
{
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x06, /* Usage (Keyboard) */
0xA1, 0x01, /* Collection (Application) */
0x05, 0x07, /* Usage Page (Keyboard/Keypad) */
0x19, 0xE0, /* Usage Minimum (Keyboard LeftControl) */
0x29, 0xE7, /* Usage Maximum (Keyboard Right GUI) */
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) - modifier */
0x95, 0x01, /* Report Count (1) */
0x75, 0x08, /* Report Size (8) */
0x81, 0x01, /* Input (Const,Array,Abs) - reserved */
0x05, 0x08, /* Usage Page (LEDs) */
0x19, 0x01, /* Usage Minimum (Num Lock) */
0x29, 0x05, /* Usage Maximum (Kana) */
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, 0x01, /* Output (Const,Array,Abs) - LED padding */
0x05, 0x07, /* Usage Page (Keyboard/Keypad) */
0x19, 0x00, /* Usage Minimum (Reserved) */
0x29, 0x65, /* Usage Maximum (Keyboard Application) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x65, /* Logical Maximum (101) */
0x75, 0x08, /* Report Size (8) */
0x95, 0x06, /* Report Count (6) */
0x81, 0x00, /* Input (Data,Array,Abs) - key array */
0xC0, /* End Collection */
};
此HID描述符定义的数据格式就是
typedef struct __attribute__((packed)) {
uint8_t modifier; // 8bit: E0~E7 (LCTRL, LSHIFT, LALT, LGUI, RCTRL, RSHIFT, RALT, RGUI)
uint8_t reserved; // 保留
uint8_t keys[6]; // 6个普通按键keycode(00~65),最多同时6键
} hid_kbd_input_report_t; // 共8字节
因此我们此时就要区分修饰键和普通按键的区别:
- 按照HID的标准协议,键盘的按键标识就是通过一个字节来进行标识的
- 同一个字节可以标识不同的按键,主要看此按键的位置是放在描述符定义的修饰按键字节处还是普通按键处
- 比如:0X04在修饰按键中表示左ALT,在普通按键中表示按键A。
对于HID描述符,我们通常采用DT来辅助我们快速生成


之后就是利用HID描述符定义的report格式进行我们的数据发送即可。
最后再次以HID键盘获取按键为例子:
PC 是这样“知道哪个键”的:
1. 先读你的 HID 描述符
知道输入报告是 8 字节,byte0 是修饰键、byte2..7 是键值数组。
2. 再按 HID 标准码表解释 keycode
例如 0x04 解释为 A,0x1E 解释为 1,这是 USB HID Usage Tables 的标准,不是你自定义的。
3. 结合修饰键位一起算最终字符
比如 byte0 的 Shift 位=1 且 keycode=0x04,PC 就当成 A(大写)而不是 a。
4. 靠“按下/松开”的报告变化判定状态
按下时报告里有 keycode;松开时把对应 keycode 清掉(常见是发全 0),PC 就知道键释放了。
更多推荐



所有评论(0)