第4章 BLE协议栈核心 - HCI层(主机控制接口)

各位同学,今天我们来聊聊BLE协议栈里一个非常关键的“翻译官”——HCI层。说实话,很多刚入行的朋友觉得HCI就是个传数据的通道,没什么技术含量。但我告诉你,这个想法大错特错。HCI层要是没搞明白,你整个蓝牙系统都可能跑不起来。

HCI的全称是Host Controller Interface,主机控制接口。它把蓝牙协议栈分成了两半:上面是Host(主机),下面是Controller(控制器)。为什么要这么分?说白了,就是为了灵活。你可以把Host跑在一个高性能的CPU上,Controller跑在另一个低功耗的MCU上,中间通过HCI通信。我做过一个智能穿戴项目,就是这么干的——主芯片跑应用和协议栈上层,蓝牙射频芯片只跑Link Layer,中间走UART HCI。

核心要点:HCI层定义了Host和Controller之间通信的标准化接口。不管你是用UART、USB还是SDIO,只要遵循HCI协议,Host和Controller就能互相理解。

4.1 HCI数据包格式

HCI的数据包分三种:Command(命令)、Event(事件)、Data(数据)。嗯,这里要注意,Data又分两种——ACL Data(异步无连接数据)和SCO Data(同步面向连接数据)。BLE主要用ACL Data,SCO是经典蓝牙的音频通道。

4.1.1 HCI Command Packet

Host发给Controller的命令包。格式很简单,但每个字段都有讲究:

| 0      | 1      | 2      | 3      | 4 ... n          |
| OpCode | OpCode | Param  | Param  | Parameter(s)     |
| (低8位)| (高8位)| Total  | Total  | (最多255字节)    |
| OCF    | OGF    | Length | Length |                  |

OpCode是2字节的操作码,分成两部分:

  • OGF(OpCode Group Field):高6位,表示命令组。比如0x01是Link Control命令,0x04是Host Controller & Baseband命令。
  • OCF(OpCode Command Field):低10位,表示具体命令。比如HCI_Reset的OCF是0x0003。

我刚开始写HCI驱动时,犯过一个低级错误——把OGF和OCF的位宽搞反了。结果发出去的Reset命令,Controller根本不认。查了两天,最后用逻辑分析仪抓波形才发现,OpCode的高6位和低10位颠倒了。从那以后,我每次写HCI命令都会先画个位图确认一下。

4.1.2 HCI Event Packet

Controller收到命令后,会返回Event包告诉Host结果。格式如下:

| 0      | 1      | 2 ... n          |
| Event  | Param  | Parameter(s)     |
| Code   | Total  | (最多255字节)    |
|        | Length |                  |

常见的Event Code有:

Event Code 事件名称 说明
0x0E Command Complete 命令执行完成(同步命令用)
0x0F Command Status 命令已接收(异步命令用)
0x3E LE Meta Event LE相关的事件(如连接完成、广告报告)

你想想看,为什么要有Command Complete和Command Status两种?其实很简单:有些命令执行很快,比如HCI_Reset,Controller马上就能搞定,直接返回Complete。但有些命令需要时间,比如建立连接,Controller先返回Status告诉你“我收到了,正在处理”,等连接建立后再发一个LE Connection Complete事件。我在项目中就遇到过,如果只等Complete事件,异步命令永远收不到响应。

4.1.3 HCI ACL Data Packet

数据包用于传输实际的蓝牙数据。格式:

| 0      | 1      | 2      | 3      | 4 ... n          |
| Handle | Handle | Data   | Data   | Data             |
| (低8位)| (高4位)| Total  | Total  | (最多251字节)    |
| +PB+BC |        | Length | Length |                  |

这里有个坑:Handle字段的低12位是Connection Handle,高4位是PB(Packet Boundary Flag)和BC(Broadcast Flag)。PB标志很重要,它告诉接收方这个数据包是完整L2CAP帧的开始、中间还是结束。我见过有人把PB标志写错了,导致对端收不到完整的L2CAP包,连接直接断掉。

个人经验:调试HCI数据包时,我习惯先用一个简单的抓包工具(比如Ellisys或Frontline)看看标准的数据流是什么样的。等自己写代码时,再对照着抓包结果逐字节比对。这个方法帮我省了不少时间。

4.2 UART/USB传输层实现

HCI层的数据最终要通过物理传输层发送。最常用的两种是UART和USB。

4.2.1 UART HCI传输

UART HCI也叫H4协议。它用Packet Indicator来区分三种包类型:

  • 0x01:HCI Command Packet
  • 0x02:HCI ACL Data Packet
  • 0x04:HCI Event Packet

发送时,先发一个字节的Indicator,再发包体。接收时,先读Indicator,再根据类型解析后续数据。我做过一个低功耗蓝牙模块,UART波特率设成了115200,结果发现Event包经常丢。后来查出来是Controller的UART FIFO太小,Host来不及读就溢出了。解决办法是把波特率提到921600,同时加了个硬件流控(RTS/CTS)。

避坑指南:我曾经在UART HCI上栽过跟头——没有处理分包和粘包。UART是流式传输,一个完整的HCI包可能被拆成多次发送,也可能多个包连在一起。一定要实现一个状态机来组包,不能假设一次recv就能收到完整的一包。

4.2.2 USB HCI传输

USB HCI用不同的Endpoint来区分包类型:

  • Bulk OUT Endpoint:Host发送Command和ACL Data
  • Bulk IN Endpoint:Controller返回Event和ACL Data
  • Interrupt IN Endpoint:Controller发送Event(低延迟场景)

USB的好处是速度快、带宽大,适合做高速数据传输。但坏处是协议栈复杂,需要处理USB描述符、控制传输、批量传输等。我建议初学者先从UART入手,等把HCI逻辑搞清楚了再上USB。

4.3 HCI命令与事件处理流程

理解了数据包格式和传输层,我们来看看完整的处理流程。我画了一张图,帮你理清思路:

HCI命令与事件处理流程 Host(主机) Controller(控制器) 1. 应用层发起请求 (如:发起扫描、建立连接) 2. Host构造HCI命令 (填写OpCode、参数) 3. 通过传输层发送 (UART/USB发送) HCI Command 4. Controller接收并解析 (解析OpCode,执行命令) 5. 执行命令 (如:启动扫描、建立连接) 6. 构造Event包 (Command Complete / Event) HCI Event 7. Host接收Event (解析Event,通知应用层) 注:异步命令(如LE Create Connection)会先返回Command Status,再返回LE Connection Complete事件

流程其实不复杂,我总结成四步:

  1. 构造命令:Host根据应用需求,构造HCI Command包。比如要开始扫描,就构造LE Set Scan Parameters和LE Set Scan Enable命令。
  2. 发送命令:通过UART或USB把命令包发给Controller。
  3. 执行命令:Controller解析命令,执行对应的操作。比如启动射频扫描。
  4. 返回事件:Controller执行完毕后,构造Event包返回给Host。Host解析Event,通知上层应用。

这里有个细节:同步命令和异步命令的处理方式不同。同步命令(如HCI_Reset)执行完直接返回Command Complete。异步命令(如LE Create Connection)先返回Command Status表示“收到”,等真正的事件发生后再返回具体的事件(如LE Connection Complete)。

调试技巧:我写HCI驱动时,会在Host端维护一个“待处理命令队列”。每发一个命令,就把命令的OpCode和期望的事件类型记录下来。收到Event时,去队列里匹配。如果匹配不上,说明有异常。这个方法帮我快速定位过好几个协议栈的bug。

好了,HCI层的内容就讲到这里。记住,HCI是Host和Controller之间的桥梁,搞懂了它,你就掌握了BLE协议栈的“交通要道”。下一节我们会深入L2CAP层,看看数据是怎么被封装和分片的。


公众号:蓝海资料掘金营,微信deep3321