第1章:HCI接口详解

各位同学,咱们今天聊聊HCI。说实话,HCI是蓝牙协议栈里最接地气的一层。为什么这么说?因为它是主机和控制器之间的桥梁。你想想看,没有HCI,你的蓝牙芯片就是个黑盒子,你根本不知道它在干什么。

我在做蓝牙项目那会儿,最头疼的就是HCI层出问题。有一次设备连不上,我查了三天,最后发现是HCI命令的payload长度写错了。嗯,从那以后,我对HCI的每个字节都格外敏感。

4.1 HCI数据包格式

HCI数据包分三种:Command、Event和Data。每种包都有自己的格式,咱们一个一个看。

4.1.1 Command包

Command包是主机发给控制器的。格式很简单:

| OpCode (2字节) | Parameter Total Length (1字节) | Parameters (可变) |

OpCode又分两部分:OGF(OpCode Group Field,高6位)和OCF(OpCode Command Field,低10位)。OGF用来分组,比如0x01是Link Control命令,0x04是Controller & Baseband命令。OCF是具体命令的编号。

举个例子,HCI_Reset命令的OpCode是0x0C03。OGF=0x03(Controller & Baseband),OCF=0x0003。Parameter Total Length是0,因为Reset命令不需要参数。

重点:Command包的最大长度是255字节(Parameter Total Length是1字节)。我遇到过有人把参数长度写超了,结果控制器直接忽略整个包。

4.1.2 Event包

Event包是控制器回复主机的。格式:

| Event Code (1字节) | Parameter Total Length (1字节) | Parameters (可变) |

Event Code 0x0E是Command Complete事件,0x0F是Command Status事件。这两个最常用。Command Complete表示命令执行完了,带返回值。Command Status表示命令已经开始执行,但还没结束。

我个人习惯,调试时先看Command Complete。如果没收到,说明命令根本没执行。有一次我发了个HCI_Inquiry命令,等了半天没反应,一查发现是Event包被UART的FIFO吞了。嗯,硬件问题有时候就是这么坑。

4.1.3 Data包

Data包分两种:ACL Data和SCO Data。ACL是异步无连接数据,SCO是同步面向连接数据。格式:

| Handle (2字节) | Data Total Length (2字节) | Data (可变) |

Handle的低12位是连接句柄,高4位是PB(Packet Boundary)标志和BC(Broadcast)标志。PB标志用来指示数据包是分片还是完整包。

小技巧:抓HCI日志时,看到Data包的PB标志是0x00,表示这是完整L2CAP包。如果是0x01,表示这是分片的第一包。我曾经因为没注意PB标志,把分片数据当完整包解析,结果L2CAP层一直报错。

4.2 HCI Transport层

HCI数据包怎么传输?靠Transport层。常见的Transport有UART、USB、SDIO。咱们一个一个说。

4.2.1 UART Transport

UART是最常见的HCI Transport。为什么?便宜、简单、通用。很多低功耗蓝牙芯片都用UART。

UART HCI的包格式有个特殊的地方:每个包前面加一个包指示符(Packet Indicator)。

| Packet Indicator (1字节) | HCI Packet |

Packet Indicator的值:0x01表示Command包,0x02表示ACL Data包,0x03表示SCO Data包,0x04表示Event包。

我在项目中遇到过一个问题:UART的波特率设置不对,导致HCI包经常丢字节。后来我加了CRC校验,才定位到问题。嗯,UART虽然简单,但波特率、流控这些细节不能马虎。

4.2.2 USB Transport

USB Transport用在蓝牙适配器上。它用USB的Bulk端点传输HCI数据。USB HCI的包格式和UART不一样,它不需要包指示符,因为USB的端点已经区分了类型。

USB Transport有四个端点:Control端点(用于配置)、Bulk IN端点(用于接收Event和Data)、Bulk OUT端点(用于发送Command和Data)、Interrupt IN端点(用于接收Event)。

注意:USB Transport的Event包必须走Interrupt IN端点,不能走Bulk IN。我见过有人把Event配置到Bulk端点,结果主机收不到任何事件。这个坑我踩过,所以特别提醒你。

4.2.3 SDIO Transport

SDIO Transport用在蓝牙/WiFi combo芯片上。它通过SDIO协议传输HCI数据。SDIO的吞吐量比UART高,适合高速数据传输。

SDIO HCI的包格式和UART类似,也有包指示符。但SDIO的传输机制更复杂,需要处理中断和DMA。

说实话,SDIO Transport我接触得不多。但我知道一点:SDIO的时钟频率不能太高,否则信号完整性会出问题。我曾经帮同事调试一个SDIO蓝牙模块,发现时钟频率设到50MHz就丢包,降到25MHz就稳定了。

4.3 HCI Command与Event交互流程

主机发一个Command,控制器回一个Event。这是最基本的交互流程。但具体怎么交互?咱们看个例子。

假设主机要查询本地蓝牙地址,发HCI_Read_BD_ADDR命令:

  1. 主机构造Command包:OpCode=0x1009(OGF=0x04,OCF=0x0009),Parameter Total Length=0。
  2. 主机通过Transport层发送Command包。
  3. 控制器收到后,执行命令,读取本地BD_ADDR。
  4. 控制器构造Event包:Event Code=0x0E(Command Complete),Parameters包含Status和BD_ADDR。
  5. 控制器通过Transport层发送Event包。
  6. 主机收到Event包,解析BD_ADDR。

这个流程看起来简单,但有个细节要注意:主机发完Command后,不能立即发下一个Command。必须等收到Event后,才能发下一个。这叫命令串行化。

关键点:HCI规范要求,主机一次只能发一个Command。如果连续发多个,控制器可能只处理第一个,后面的都忽略。我刚开始做蓝牙时,就因为这个bug,设备初始化总是不完整。

不过,有些控制器支持Command Packet Boundary Flag,允许主机发多个Command。但我不建议依赖这个特性。老老实实等Event,最保险。

4.4 实战:使用hcitool与btmon抓取HCI日志

理论讲完了,咱们来点实战。用hcitool和btmon抓HCI日志,是调试蓝牙问题最常用的方法。

4.4.1 安装工具

在Linux系统上,安装bluez工具包:

sudo apt-get install bluez bluez-tools bluez-hcidump

hcitool是bluez自带的命令行工具。btmon是bluez 5.x新增的监控工具,比hcidump更好用。

4.4.2 使用hcitool

hcitool可以发送HCI命令。比如查询本地蓝牙地址:

sudo hcitool cmd 0x04 0x0009

0x04是OGF,0x0009是OCF。hcitool会自动构造Command包并发送。

查看结果:

< HCI Command: ogf 0x04, ocf 0x0009, plen 0
> HCI Event: 0x0e plen 6
  01 00 09 04 01 12 34 56 78 9A BC

Event包里的最后6个字节就是BD_ADDR:12:34:56:78:9A:BC。

小技巧:hcitool cmd后面可以加参数。比如发HCI_Reset命令:sudo hcitool cmd 0x03 0x0003。我调试时经常用Reset命令,因为它不需要参数,而且能重置控制器状态。

4.4.3 使用btmon

btmon是实时监控工具。启动:

sudo btmon

然后执行其他蓝牙操作,btmon会打印所有HCI数据包。输出格式很清晰:

< HCI Command: Read BD ADDR (0x04|0x0009) plen 0
> HCI Event: Command Complete (0x0e) plen 6
  Read BD ADDR (0x04|0x0009) ncmd 1
  Status: Success (0x00)
  BD ADDR: 12:34:56:78:9A:BC (OUI 12-34-56)

btmon还会解析包内容,比如BD_ADDR的OUI。这个功能很实用,省得自己手动解析。

btmon还支持过滤。比如只看某个连接的ACL数据:

sudo btmon --filter acl

我个人习惯,调试时先开btmon,再执行操作。这样能抓到完整的交互过程。有一次设备配对失败,我通过btmon发现是PIN码交换时Event包丢了。嗯,btmon真是调试利器。

4.4.4 保存日志

btmon可以保存日志到文件:

sudo btmon -w hci_log.btsnoop

btsnoop格式可以用Wireshark打开。Wireshark对HCI协议有完整的解析,比btmon更详细。

注意:btmon需要root权限。如果不用sudo,可能抓不到任何包。我刚开始用btmon时,忘了加sudo,折腾了半天才发现。

小结

这一章咱们讲了HCI的方方面面。从数据包格式,到Transport层,再到交互流程,最后实战抓包。HCI是蓝牙协议栈的基石,搞懂它,后面的L2CAP、GATT就好理解了。

下一章,咱们聊L2CAP。L2CAP是蓝牙的数据传输层,很多高级功能都依赖它。到时候我会分享一些我在项目中遇到的L2CAP问题,保证让你少走弯路。

好了,今天就到这里。有什么问题,欢迎在课程群里讨论。咱们下章见。