3. HCI 接口详解:HCI 命令、事件、数据包格式,以及驱动层初始化流程
好,咱们今天来聊聊 HCI。这是蓝牙驱动开发里最基础、也最绕不开的一层。说白了,HCI 就是主机(Host)和控制器(Controller)之间的“翻译官”。你想想看,你的手机芯片(主机)怎么告诉蓝牙芯片(控制器)去扫描、去连接?就是靠 HCI。
我个人习惯把 HCI 比作一个“协议管道”。主机这边发命令,控制器那边回事件。数据要传了,就走 ACL 或 SCO 通道。就这么简单,但细节里全是坑。
3.1 HCI 数据包格式:四种“信使”
HCI 定义了四种数据包。每种都有自己的包头,用来区分“你是谁、要去哪”。
| 包类型 | 用途 | 包指示符(字节) |
|---|---|---|
| HCI Command | 主机 → 控制器,发指令 | 0x01 |
| HCI ACL Data | 双向,传普通数据 | 0x02 |
| HCI SCO Data | 双向,传语音数据 | 0x03 |
| HCI Event | 控制器 → 主机,通知结果 | 0x04 |
嗯,这里要注意:包指示符是 HCI 层用来区分包类型的第一个字节。如果你在调试时看到数据流里第一个字节是 0x01,那后面跟的就是一条命令。
3.1.1 HCI 命令包
命令包的格式很固定:
| 包指示符 (1B) | OpCode (2B) | 参数总长度 (1B) | 参数 (N B) |
OpCode 又分成两部分:高 6 位是 OGF(操作组字段),低 10 位是 OCF(操作命令字段)。比如经典的 HCI_Reset 命令,它的 OpCode 是 0x0C03。OGF=0x03(控制器与基带命令),OCF=0x0003。
我在项目中遇到过一个问题:有个同事写驱动时,把 OpCode 的字节序搞反了。发出去的 Reset 命令,控制器根本不认。查了半天,才发现是大小端没处理好。所以,OpCode 在内存里是小端存储,低字节在前。
3.1.2 HCI 事件包
事件包是控制器主动上报的。格式如下:
| 包指示符 (1B) | 事件码 (1B) | 参数总长度 (1B) | 参数 (N B) |
最常见的两个事件:
- Command Complete (0x0E):命令执行完了,带返回参数。
- Command Status (0x0F):命令已经开始执行,但还没完。
我曾经踩过一个坑:发了一条 HCI_Inquiry 命令,结果只收到了 Command Status 事件,就以为命令失败了。其实不是,Command Status 只是告诉你“命令已接收,正在执行”。真正的结果要等 Inquiry Complete 事件。所以,千万别把 Command Status 当最终结果。
3.1.3 HCI ACL 数据包
ACL 包用来传普通数据,比如 A2DP 音频流。格式:
| 包指示符 (1B) | 句柄 (2B) | 数据总长度 (2B) | 数据 (N B) |
句柄里还藏着两个标志位:PB(Packet Boundary)和 BC(Broadcast)。PB 标志用来告诉接收方“这个包是不是一个 L2CAP 分片的开始或结束”。
我建议你在调试音频卡顿问题时,重点看 ACL 包的 PB 标志。如果 PB 标志乱跳,说明上层协议栈的分片逻辑有问题,音频肯定断断续续。
3.2 驱动层初始化流程:从零到能干活
好了,格式看完了。咱们来走一遍驱动初始化的流程。这是每个蓝牙驱动工程师的“必修课”。
初始化流程,说白了就是让主机和控制器“握手”成功,然后配置好各种参数。我把它拆成 5 步:
- 传输层初始化:打开 UART、USB 或 SDIO 接口。
- 发送 HCI_Reset:让控制器回到已知状态。
- 读取本地版本信息:确认控制器支持什么特性。
- 配置控制器参数:比如设置事件过滤、缓冲区大小。
- 启动扫描/广播:进入工作模式。
3.2.1 第一步:传输层初始化
这一步跟硬件强相关。如果是 UART,需要配置波特率、流控。我习惯先设成 115200,等 HCI_Reset 完成后,再通过 HCI_Write_BD_ADDR 之类的命令切换到高速模式。
3.2.2 第二步:发送 HCI_Reset
代码很简单:
// 构造 HCI_Reset 命令
uint8_t reset_cmd[] = {0x01, 0x03, 0x0C, 0x00};
// 发送
uart_send(reset_cmd, sizeof(reset_cmd));
// 等待 Command Complete 事件
uint8_t event[10];
uart_recv(event, sizeof(event));
// 检查事件码是否为 0x0E,且状态为 0x00
if (event[1] == 0x0E && event[4] == 0x00) {
// Reset 成功
}
嗯,这里要注意:Reset 命令没有参数,所以参数总长度是 0。收到 Command Complete 事件后,要检查第 4 个字节(Status)是不是 0x00。如果不是,说明控制器没复位成功,可能是硬件问题。
3.2.3 第三步:读取本地版本信息
Reset 之后,我建议先发一条 HCI_Read_Local_Version_Information。这条命令能告诉你控制器的芯片型号、固件版本。我在项目中遇到过,有些山寨芯片会返回假的版本号,导致上层协议栈加载了错误的配置。
命令 OpCode 是 0x1001。返回的事件里包含:
- HCI 版本
- LMP 版本
- 制造商 ID
如果制造商 ID 是 0x0000,那就要小心了。这通常意味着控制器没有正确初始化。
3.2.4 第四步:配置控制器参数
这一步因人而异。但有几个参数是通用的:
- 事件过滤:默认情况下,控制器会报告所有事件。我习惯设置成只报告我关心的事件,减少中断频率。
- ACL 缓冲区大小:告诉主机“我能收多少数据”。如果主机发的数据包太大,控制器会丢包。
- 连接超时时间:默认值通常太长。我习惯设成 5 秒,避免死连接。
3.2.5 第五步:启动工作模式
最后一步,根据你的设备角色来:
- 如果是手机(主机),就发
HCI_Write_Scan_Enable开启扫描。 - 如果是耳机(从机),就发
HCI_LE_Set_Advertising_Data和HCI_LE_Set_Advertising_Enable开始广播。
我个人习惯在启动工作模式之前,先发一条 HCI_Read_Buffer_Size,确认控制器的收发缓冲区大小。这样后面发数据时,心里有数。
3.3 避坑指南:我踩过的三个雷
最后,分享几个我实际项目中遇到的坑,希望能帮你省点时间。
- 事件包长度不对:有些控制器会返回错误的事件长度。比如 Command Complete 事件,标准长度是 6 字节(含状态码),但有些芯片会多返回几个字节。我的处理方式是:只解析固定长度的字段,多余字节忽略。
- ACL 包分片乱序:当音频数据量大的时候,ACL 包可能会被分片。如果分片到达的顺序乱了,音频就会爆音。我建议在驱动层加一个 分片重组超时机制,超过 100ms 没收到完整包,就直接丢弃。
- HCI 命令超时:有些命令(比如 Inquiry)执行时间很长。如果驱动层没有超时机制,就会一直卡在那里。我习惯给每条命令设一个 5 秒的超时定时器,超时后直接报错并重试。
好了,HCI 接口这部分就聊到这儿。说白了,HCI 就是一套“你问我答”的协议。只要你把命令包和事件包的格式搞清楚了,初始化流程走顺了,后面的事情就水到渠成。下一章,咱们会深入 L2CAP 层,看看数据是怎么被封装和分片的。