☰
【HIDS】规范精讲[1]:从连接建立到数据通路——HID over GATT 的底层契约
2026/10/19 2:04:15 网站建设 项目流程

做 BLE 外设开发,HID 是绕不开的一块。键盘、鼠标、遥控器、游戏手柄,这些设备在 BLE 上统一走 HID Service。很多人第一次接触 HIDS 时,觉得它就是把 USB HID 搬到 GATT 上,其实不然。HIDS 在协议栈里的位置、对底层传输的假设、以及对 GATT 子过程的强制要求,决定了它和普通自定义服务完全不是一个物种。这篇文章把 HID Service拆开揉碎,结合一份真实射频抓包,看看一个 HID 设备从连上手机到能收发数据,中间到底经历了什么。


目录

一、HID Service 到底解决什么问题

二、服务依赖:零依赖的独立服务

三、传输层绑定:LE only,BR/EDR 请走传统 HID Profile

四、GATT 子过程要求:一份罕见的全强制清单

五、字节序与错误码:小端优先,不自定义错误

六、v1.1 的核心增量:Shorter Connection Intervals

七、规范语言:shall、should、may、must、can 的微妙差别

八、表格里的 M、O、E、C:读懂规范的结构化语言

九、代码视角:连接建立后的第一件事

十、HID over GATT 的底层逻辑

十一、测验


一、HIDService 到底解决什么问题

HID Service 的官方定义只有一句话:

The HID Service exposes control, data, and associated formatting for HID Devices and HID Hosts.

翻译成工程语言:它把 HID 设备的控制指令、业务数据、以及数据格式描述,统一封装成 GATT characteristic 暴露给对端。键盘按下一个键,鼠标移动一段距离,这些原始事件通过 Report characteristic 以 Notification 的形式推给主机;主机要改键盘 LED 状态、要读取当前电量,通过 Write/Read 完成。格式化信息则靠 Report Map characteristic 描述,告诉主机每个字节每个 bit 代表什么含义。

这里最容易被忽视的是 associated formatting。HID 数据不是简单的字节流,它有一套自描述协议,主机不需要提前知道设备类型,通过 Report Map 就能解析出这是键盘、鼠标还是自定义设备。这套机制让 BLE HID 保留了 USB HID 的通用性,又适应了 GATT 的轻量框架。

二、服务依赖:零依赖的独立服务

HIDS 在规范里明确声明:

This service is not dependent upon any other services.

意味着你在实现 HID 设备时,不需要先实现 Battery Service、Device Information Service 或其他任何服务作为前置条件。HID Service 是一个完全独立的 GATT 服务,它的所有 characteristic 都围绕 HID 业务本身展开。

当然,实际产品里通常会同时带 Battery Service 和 Device Information Service,因为主机系统需要显示电量、需要识别设备型号。但那是产品需求,不是协议要求。规范把这一点说死,是为了让最小化实现成为可能——一个只做按键上报的遥控器,理论上只需要 HID Service 就能跑起来。

三、传输层绑定:LEonly,BR/EDR请走传统HIDProfile

规范在 Inter-layer dependencies 里划了一条硬线:

This service shall operate over the LE transport only. For BR/EDR, the HID Profile [5] shall be used.

这句话看似简单,背后有深刻的架构原因。BR/EDR 有完整的 L2CAP 通道、有 SDP 服务发现、有传统 HID Profile 已经成熟运行多年。BLE 则完全不同,它基于 GATT/ATT,连接建立流程、数据包大小、功耗模型都变了。HID Service 选择只绑定 LE 传输,是为了避免在一份规范里同时维护两套语义,也让实现者不用考虑跨传输的兼容问题。

对嵌入式工程师来说,这意味着如果你在做双模设备,经典蓝牙和 BLE 的 HID 是两条独立路径,不能混用。代码里要分别处理 HID over GATT 和 HID over L2CAP 的初始化、连接管理和数据收发。

四、GATT 子过程要求:一份罕见的全强制清单

HIDS 对 GATT 子过程的要求非常强硬,规范里用一张表列出来,所有项都是 Mandatory:

Read Long Characteristic Value、Write Without Response、Write Characteristic Value、Notifications、Read Characteristic Descriptors、Write Characteristic Descriptors,六项全是 M。这在 BLE Profile 里并不常见,很多 Profile 会把部分能力标成 Optional 或 Conditional。

为什么HID敢这么要求?因为 HID 的数据模型离不开这些操作。Report Map 可能很长,需要 Read Long;输入报告靠 Notification 推;输出报告和 Feature 报告靠 Write;CCCD 配置靠 Read/Write Descriptor。少一个,HID 功能就不完整。规范用全 M 告诉实现者:不要想着偷工减料,这些是底线。

这里有个工程细节值得注意。Write Without Response 和 Write Characteristic Value 同时被列为 M,意味着设备必须同时支持带响应和不带响应的写。带响应的写用于关键控制指令,比如 Protocol Mode 切换;不带响应的写用于对时延敏感但允许丢包的场景,比如某些高频输出报告。抓包里也能看到这两种写并存:

上图是真实射频抓包里连接建立后的前几百毫秒。LLCP 层先做完 Version Exchange、Data Length Update、Feature Exchange,然后 ATT 层立刻发起 MTU Exchange,整个过程不到 1 秒。Data Length Update 把链路层 PDU 扩到 251 字节,为后续大数据量传输铺路;MTU Exchange 则决定了 ATT 层一次能传多少有效载荷。HID 设备如果支持长 Report Map 或大数据量输出,这一步至关重要。

五、字节序与错误码:小端优先,不自定义错误

规范对字节序的规定很简洁:

All characteristics used with this service shall be transmitted with the least significant octet first (i.e., little endian).

BLE 生态统一用小端,HIDS 没有例外。这意味着你在解析 Report Map、构造 HID 描述符、处理多字节数值时,都要按小端处理。比如一个 16 位的 X 轴坐标值 0x1234,线上传输顺序是 0x34 0x12。

错误码方面,HIDS 明确不定义任何应用层错误码:

This service does not define any application error codes that are used in Attribute Protocol.

这意味着 HID 设备遇到错误时,只能使用 ATT 标准错误码,比如 Invalid Handle、Read Not Permitted、Write Not Permitted、Insufficient Authentication 等。不要试图在 HID Service 里发明新的错误码,主机也不会识别。

六、v1.1 的核心增量:Shorter Connection Intervals

从 v1.0 到 v1.1,HIDS 只增加了一个正式特性:

HID Shorter Connection Intervals,简称 SCI。它的目标是把 HID 延迟压到 7.5 ms 以下,同时保持低功耗。传统 BLE 连接间隔最小是 7.5 ms,对于游戏手柄、触控笔这类对延迟敏感的设备,7.5 ms 仍然不够理想。SCI 允许在特定条件下使用更短的连接间隔,让 HID 设备在 BLE 上也能逼近有线体验。

但 SCI 不是单边能决定的,规范明确要求 Host 和 Controller 都要支持。这意味着你在选型芯片时,不仅要看蓝牙版本,还要确认链路层和主机协议栈是否实现了 SCI 相关接口。如果 Controller 不支持,即使应用层写了 SCI 代码,也跑不出效果。

七、规范语言:shall、should、may、must、can 的微妙差别

读蓝牙规范,先要学会读它的语言。HIDS 在开头用了一整节解释这些关键词:

shall 和 mandatory 是强制的,必须实现;should 和 may 是推荐或可选;must 表示事实或自然结果;can 表示能力。这些词不是随便用的,它们直接对应到测试认证时的检查项。

更实际的是 RFU 和 Prohibited 的处理。规范要求:

  • RFU 字段发送时必须置 0,接收时必须忽略,不能因为 RFU 非 0 而拒绝报文。

  • Prohibited 值永远不能发送,收到后必须忽略整个报文,且不响应。

这两条规定对互操作性至关重要。比如 HID 描述符里某些 bit 是 RFU,你的设备发送时置 0,对方设备收到后即使这些 bit 意外被置 1,也要当成 0 处理。这样未来规范扩展新功能时,老设备不会直接崩溃。

八、表格里的 M、O、E、C:读懂规范的结构化语言

HIDS 用 M/O/E/N/A/C.# 来标记表格中的每一项要求:

M 是 Mandatory,O 是 Optional,E 是 Excluded,N/A 是 Not Applicable,C.# 是 Conditional。Conditional 最灵活,它表示某项要求取决于其他条件是否满足。比如某个 characteristic 只在设备支持键盘功能时才强制,这就是典型的 Conditional。

读规范表格时,先看清楚角色列,再看要求列。一个设备可能同时是 HID Device 和 HID Host,不同角色下的要求可能完全不同。E 表示在这个角色下绝对不允许支持,即使你的硬件有能力也不行。

九、代码视角:连接建立后的第一件事

从代码角度看,HID 设备连接建立后的初始化流程大致如下。下面是一个基于 BlueZ 的简化示例,展示如何在用户态注册 HID Service 并处理 MTU 协商:

import struct from dbus_next.aio import MessageBus from dbus_next.constants import GattCharacteristicWriteType HIDS_SVC = '00001812-0000-1000-8000-00805f9b34fb' REPORT_MAP = '00002a4b-0000-1000-8000-00805f9b34fb' HID_CONTROL = '00002a4c-0000-1000-8000-00805f9b34fb' PROTOCOL_MODE = '00002a4e-0000-1000-8000-00805f9b34fb' REPORT = '00002a4d-0000-1000-8000-00805f9b34fb' async def init_hid_device(bus: MessageBus): # 1. 注册 HID Service 及所有 characteristic # 2. 设置 Report Map 描述符 # 3. 等待对端 MTU Exchange # 4. 根据协商结果调整分包策略 pass def parse_report_map(data: bytes): # Report Map 是 HID 描述符的二进制表示 # 小端解析,逐项解析 Usage Page、Usage、Report ID、Report Count 等 offset = 0 while offset < len(data): b = data[offset] tag = b & 0xF0 size = b & 0x03 offset += 1 value = data[offset:offset+size] offset += size yield tag, value # 发送输入报告示例 def build_keyboard_report(modifier: int, keycode: int) -> bytes: # Report ID 1, 8 bytes keyboard input report return struct.pack('<8B', modifier, 0, keycode, 0, 0, 0, 0, 0)

这段代码虽然简化,但体现了 HID 开发的核心:Report Map 解析、小端数据处理、Report 构造。实际项目里还需要处理 CCCD 订阅、Protocol Mode 切换、Suspend/Resume 等状态机。

十、HID over GATT 的底层逻辑

HID Service 不是简单的特征集合,它是一套围绕 HID 业务建立的通信契约。从连接建立那一刻起,LLCP 的 Version Exchange、Data Length Update、Feature Exchange、PHY Update,到 ATT 的 MTU Exchange,每一步都在为高效、低延迟的 HID 数据传输做准备。规范用全 M 的 GATT 子过程要求、严格的小端字节序、明确的 RFU/Prohibited 规则,确保不同厂商的 HID 设备和主机能够互操作。

理解这些底层契约,比记住某个 characteristic 的 UUID 更重要。因为当你遇到连接不稳定、数据解析错误、延迟过高等问题时,答案往往不在应用层,而在这些看似枯燥的规范细节里。

十一、测验

1. 蓝牙 BLE HID 设备为什么必须支持 Read Long Characteristic Value?请结合 HID 数据模型说明。(某头部手机厂商蓝牙协议栈面试真题)

答:

HID 的 Report Map characteristic 通常包含完整的 HID 描述符,长度可能超过默认 ATT MTU(23 字节)。例如一个支持键盘、鼠标、 consumer control 的多功能设备,其 Report Map 可能达到几十甚至上百字节。如果不支持 Read Long,主机无法完整读取 Report Map,也就无法解析后续收到的输入报告,导致设备无法正常工作。因此 HIDS 将 Read Long 列为 Mandatory,确保任何主机都能获取完整的 HID 描述符。

2. HID over GATT 中,Notification 和 Write Without Response 分别适用于什么场景?为什么规范要求两者都是 Mandatory?

答:

Notification 用于设备到主机的异步数据推送,典型场景是键盘按键、鼠标移动等输入报告,它不需要主机确认,延迟最低。Write Without Response 用于主机到设备的快速控制,比如某些对时延敏感但允许偶尔丢失的输出报告。两者都是 Mandatory,是因为 HID 的双向通信模型同时依赖这两种机制:输入路径必须能推,输出路径必须能写。缺少任何一个,HID 功能都不完整。

3. 在 BLE HID 开发中,如果主机发送了一个 RFU 位被置 1 的 HID 控制指令,设备应该如何处理?为什么?

答:

设备应该将该 RFU 位视为 0,继续正常处理整个报文,不能因为该位为 1 而拒绝或报错。规范这样设计是为了向前兼容:未来版本可能会利用这些 RFU 位定义新功能,老设备如果直接拒绝报文,会导致新主机无法与老设备通信。忽略 RFU 位是蓝牙协议栈实现的基本素养,也是通过蓝牙认证测试的必要条件。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询