3、HCI层详解:Host Controller Interface,UART与USB传输,HCI Command/Event/Data包格式

各位同学,今天我们来啃一块硬骨头——HCI层。说实话,很多做蓝牙应用开发的工程师,干了好几年都没真正搞懂HCI。他们只知道调API,却不知道底层在干什么。我个人觉得,HCI是蓝牙协议栈里最值得花时间研究的一层,因为它正好卡在Host和Controller之间,是软件和硬件的分水岭。

说白了,HCI就是蓝牙主机和蓝牙控制器之间的“翻译官”。主机这边跑着协议栈上层(L2CAP、RFCOMM这些),控制器那边跑着链路层和物理层。两边怎么沟通?就是靠HCI。你想想看,如果没有一个标准接口,每个芯片厂商都搞一套私有协议,那Android系统就没法适配那么多蓝牙芯片了。

3.1 HCI的物理传输:UART与USB

HCI本身是逻辑上的协议,它需要物理载体。最常见的两种载体就是UART和USB。嗯,这里要注意,不同的传输方式,HCI的封包格式会有一点点差异。

3.1.1 UART传输

UART是蓝牙芯片最传统的连接方式。早期蓝牙模块基本都是走UART,现在很多车载蓝牙方案也还在用。为什么?因为UART简单、成本低、延迟可控。

UART上的HCI传输,有一个特殊的协议叫H4。H4协议就是在每个HCI包前面加一个字节的指示符,告诉接收方这个包是命令、事件还是数据。

// H4 包格式
// +------+------------------+
// | 类型 | HCI 包内容       |
// +------+------------------+
// | 1字节 | 可变长度         |
// +------+------------------+

// 类型指示符:
// 0x01: HCI Command 包
// 0x02: HCI ACL Data 包
// 0x03: HCI SCO Data 包
// 0x04: HCI Event 包

我在项目中遇到过一个问题:UART的波特率设置不对,导致HCI包频繁丢字节。当时排查了好久,最后发现是波特率漂移了。车载环境温度变化大,UART的晶振容易受影响。我建议做车载蓝牙的朋友,UART波特率尽量用1Mbps以上,并且加上硬件流控(RTS/CTS)。

3.1.2 USB传输

现在越来越多的蓝牙芯片走USB了,尤其是高通、博通这些大厂的方案。USB的好处是速度快、即插即用、不需要额外配置波特率。

USB上的HCI传输,用的是USB的Bulk传输端点。通常有三个端点:一个用于发送命令,一个用于接收事件,一个用于收发ACL数据。USB的HCI封包不需要H4那个类型字节,因为USB端点本身就区分了包的类型。

关键区别:

  • UART:需要H4协议,每个包加1字节类型头
  • USB:不需要H4,通过端点号区分包类型
  • UART:适合低功耗、低成本场景
  • USB:适合高吞吐、复杂场景

3.2 HCI Command包格式

HCI Command是主机发给控制器的指令。比如让控制器去扫描、去连接、去发数据,都是通过Command包下发的。

// HCI Command 包格式
// +--------+--------+--------+--------+--------+--------+
// | OpCode | OpCode | 参数总长 | 参数0  | 参数1  | ...    |
// | (低8位)| (高8位)| (1字节) |        |        |        |
// +--------+--------+--------+--------+--------+--------+
// |   OCF   |   OGF   |         |        |        |        |
// +--------+--------+--------+--------+--------+--------+

// 示例:HCI_Reset 命令
// OpCode = 0x0C03
// OGF = 0x03 (Controller & Baseband 命令组)
// OCF = 0x0003 (Reset 命令)
// 参数总长 = 0x00 (无参数)
// 实际发送:03 0C 00

OpCode由两部分组成:OGF(OpCode Group Field)和OCF(OpCode Command Field)。OGF占高6位,OCF占低10位。说白了,OGF是命令的大类,OCF是具体命令的编号。

我曾经犯过一个低级错误:把OpCode的字节序搞反了。蓝牙协议里OpCode是小端序,低字节在前。我第一次写HCI驱动时,直接按大端序发了,结果控制器完全不响应。查了两天才发现是字节序的问题。嗯,这种坑踩过一次就记住了。

3.3 HCI Event包格式

控制器执行完命令后,会给主机回复Event包。Event包是控制器主动发给主机的,主机不需要请求。

// HCI Event 包格式
// +--------+--------+--------+--------+
// | 事件码 | 参数总长 | 参数0  | 参数1  | ...
// | (1字节)| (1字节) |        |        |
// +--------+--------+--------+--------+--------+

// 示例:HCI_Command_Complete 事件
// 事件码 = 0x0E
// 参数总长 = 0x04
// 参数内容:Num_HCI_Command_Packets(1字节) + Command_Opcode(2字节) + Return_Parameters(1字节)
// 实际接收:0E 04 01 03 0C 00

Event包的事件码从0x01到0xFF。最常用的是0x0E(Command Complete)和0x0F(Command Status)。你想想看,主机发了一个命令,控制器什么时候回复?大部分命令是同步执行的,控制器做完后回复Command Complete。但有些耗时操作(比如建立连接),控制器先回复Command Status表示“我收到了,正在处理”,等处理完了再发一个专门的Event(比如Connection Complete)。

个人经验:调试HCI时,我最常用的工具就是抓HCI log。Android系统可以通过adb抓取btsnoop log,里面记录了所有HCI包的收发。遇到蓝牙连不上、断连等问题,先看HCI log,基本能定位80%的问题。

3.4 HCI Data包格式

Data包是真正传输用户数据的。分为ACL Data和SCO Data两种。ACL用于普通数据传输(比如A2DP音乐、ATT属性读写),SCO用于同步数据(比如通话音频)。

// HCI ACL Data 包格式
// +--------+--------+--------+--------+--------+--------+
// | 句柄   | 句柄   | 数据总长 | 数据总长 | 数据   | ...    |
// | (低8位)| (高8位)| (低8位) | (高8位) |        |        |
// +--------+--------+--------+--------+--------+--------+
// | 包含PB,BC标志  |         |         |        |        |
// +--------+--------+--------+--------+--------+--------+

// 示例:ACL Data 包
// 句柄 = 0x0042 (连接句柄,包含PB=10, BC=00)
// 数据总长 = 0x0010 (16字节数据)
// 实际发送:42 20 10 00 [16字节数据]

这里有个容易搞混的地方:连接句柄(Connection Handle)不是蓝牙地址,而是控制器分配给每个连接的本地标识符。同一个蓝牙设备,主机和控制器两端的连接句柄可能不一样。我在做多设备连接时,就曾经因为搞混了句柄,把数据发错了设备。

3.5 HCI流控机制

HCI层还有一个很重要的东西——流控。主机不能无限制地往控制器发命令和数据,控制器处理不过来就会丢包。

HCI的流控是通过Command Complete事件里的Num_HCI_Command_Packets字段实现的。这个字段告诉主机:“你还可以发多少个命令给我”。主机每发一个命令,就把这个计数减1,收到Command Complete后再加回来。如果计数为0,主机就得等着。

避坑指南:我曾经遇到过一个坑:某个蓝牙芯片的固件有bug,Num_HCI_Command_Packets字段一直返回0。结果主机以为控制器忙,一直不发命令,蓝牙功能就卡死了。后来我在驱动里加了一个超时机制,如果连续3秒没有收到有效的Command Complete,就强制复位HCI层。

ACL数据的流控更复杂一些。控制器通过HCI_Set_Controller_To_Host_Flow_Control命令告诉主机自己的接收缓冲区大小。主机发数据时不能超过这个限制。车载场景下,如果同时播放音乐和打电话,ACL和SCO数据会竞争带宽,这时候流控就特别重要。

3.6 实战:HCI层的调试技巧

最后分享几个我常用的HCI调试技巧:

  1. 抓HCI log:Android上通过adb shell dumpsys bluetooth_manager获取btsnoop log,然后用Wireshark分析。
  2. 手动构造HCI包:用hcitool或bluetoothctl可以直接发送HCI命令,适合验证控制器行为。
  3. 检查UART波形:如果怀疑UART传输有问题,用示波器看TX/RX引脚波形,确认波特率和电平是否正常。
  4. USB抓包:用usbmon或Wireshark的USB捕获功能,可以看到USB总线上的HCI包。

好了,HCI层的内容就讲到这里。下一章我们会深入L2CAP层,看看数据是怎么在HCI之上进行分段和重组的。记住一句话:搞懂了HCI,你就掌握了蓝牙协议栈的半壁江山。