第2章:HCI协议基础

好,咱们开始聊HCI协议。说实话,这是整个蓝牙驱动开发中最核心的一环。你想想看,Host和Controller之间怎么通信?全靠HCI这个桥梁。我当年刚接触蓝牙时,就是被HCI的各种包格式搞得晕头转向,后来踩了不少坑才摸清楚门道。

2.1 HCI数据包格式

HCI定义了四种数据包类型。每种包都有自己的使命,千万别搞混。我个人习惯在代码里用枚举来区分它们:

// HCI Packet Types
#define HCI_COMMAND_PKT     0x01
#define HCI_ACL_DATA_PKT    0x02
#define HCI_SCO_DATA_PKT    0x03
#define HCI_EVENT_PKT       0x04

嗯,这里要注意:包类型标识符是HCI传输层用来区分数据流的第一个字节。说白了,你收到一个字节0x01,就知道后面跟着的是命令包;收到0x04,那就是事件包。

核心要点:HCI数据包在Host和Controller之间传输时,第一个字节永远是包类型指示符。这是HCI传输层的约定,不能改。

2.2 HCI命令

HCI命令是Host发给Controller的指令。每个命令都有固定的格式:

typedef struct {
    uint16_t opcode;      // 操作码,高6位是OGF,低10位是OCF
    uint8_t  param_len;   // 参数长度
    uint8_t  param[255];  // 参数内容
} hci_command_t;

操作码(Opcode)怎么理解?我打个比方:OGF就像是大类编号,比如"链路控制命令"是0x01,"链路策略命令"是0x02;OCF就是具体命令编号。两者组合起来,就能唯一定位一条命令。

举个例子,HCI_Reset命令的OGF=0x03,OCF=0x0003,组合起来opcode就是0x0C03。我在项目中调试时,经常用逻辑分析仪抓HCI总线,看到0x0C03就知道是复位命令。

个人经验:写驱动时,建议把常用命令的opcode定义成宏。我习惯这样写:

#define HCI_OP_RESET            0x0C03
#define HCI_OP_READ_BD_ADDR     0x1009
#define HCI_OP_LE_SET_SCAN_EN   0x200C

这样代码可读性高,也不容易写错。

2.3 HCI事件

事件是Controller主动发给Host的。说白了,就是Controller告诉你:"刚才那个命令执行完了"或者"有设备连上来了"。

事件包格式很简单:

typedef struct {
    uint8_t  event_code;  // 事件码
    uint8_t  param_len;   // 参数长度
    uint8_t  param[255];  // 参数内容
} hci_event_t;

最常见的几个事件码:

  • 0x0E - Command Complete:命令执行完成,带返回参数
  • 0x0F - Command Status:命令已接收,状态反馈
  • 0x3E - LE Meta Event:LE相关的事件,比如连接完成、广告报告

我曾经踩过一个坑:以为所有命令都会返回Command Complete事件。其实有些命令只返回Command Status,比如那些需要长时间执行的命令。如果你在驱动里死等Command Complete,程序就卡死了。

避坑指南:我曾经在写HCI驱动时,没有区分Command Complete和Command Status,结果在发送LE Create Connection命令后,程序一直阻塞等待。后来查了三天才发现,这个命令只返回Command Status,真正的连接结果是通过LE Connection Complete事件通知的。

2.4 HCI数据流

HCI数据流分为两种:ACL数据SCO数据

ACL(Asynchronous Connection-Less)用于普通数据传输,比如你手机传文件给蓝牙音箱。SCO(Synchronous Connection-Oriented)用于实时语音数据,比如蓝牙耳机通话。

ACL数据包格式:

typedef struct {
    uint16_t handle;      // 连接句柄,含PB/BC标志
    uint16_t data_len;    // 数据长度
    uint8_t  data[0];     // 数据内容
} hci_acl_data_t;

这里有个细节:连接句柄的高4位被用来存放PB(Packet Boundary)和BC(Broadcast)标志。PB标志告诉Controller这个数据包是分片的第一包还是中间包。我刚开始写驱动时,没注意这个位域,结果传大文件时总是丢包。

数据流处理要点:

  • ACL数据支持分片重组,大包会被拆成多个HCI包传输
  • SCO数据是固定间隔的同步数据,不支持分片
  • Host端需要维护一个重组缓冲区,把分片拼回完整L2CAP包

2.5 HCI传输层概述

HCI传输层,说白了就是Host和Controller之间物理传输的通道。常见的传输方式有:

传输类型 特点 典型应用
UART 简单、低成本、速率有限 经典蓝牙模块、低功耗嵌入式
USB 高速、即插即用、复杂 蓝牙适配器、PC蓝牙
SDIO 高速、适合WiFi+蓝牙组合 手机、平板
SPI 中等速率、低功耗 IoT设备

我个人最常用的是UART传输。为什么?因为调试方便。你拿个串口工具就能看到HCI数据流。但UART有个坑:流控。如果不启用硬件流控(RTS/CTS),高速传输时很容易丢数据。

我记得有一次,客户反馈蓝牙连接不稳定。我抓了HCI日志一看,全是CRC错误。最后发现是UART波特率设得太高,又没有流控,导致Controller来不及处理数据。降到115200并启用硬件流控后,问题就解决了。

调试技巧:如果你用UART做HCI传输,建议在驱动层加一个超时重传机制。我曾经写过一个简单的HCI UART驱动,每次发送命令后启动一个定时器,如果500ms内没收到事件,就重发一次。这个机制帮我抓到了不少硬件问题。

嗯,HCI协议基础就聊这么多。下一章我们会深入HCI命令的具体实现,包括如何构造命令、解析事件、处理超时等。到时候我会分享更多实战中的踩坑经验。