第四章 蓝牙协议栈入门:HCI层、L2CAP层、SDP/GATT层、RFCOMM层

各位好,欢迎来到第四章。这一章我们聊聊蓝牙协议栈。很多刚入行的朋友,看到协议栈那几层就头大。说实话,我当年也一样。但后来我发现,搞TWS耳机开发,你不需要把每一层都吃透。你只需要抓住几个关键点:数据怎么下去,事件怎么上来,中间经过了哪些关卡。

今天我们就重点讲HCI层。这是Host和Controller之间的桥梁。你想想看,你的手机芯片(Host)和蓝牙芯片(Controller)之间怎么通信?就是靠HCI。我习惯把HCI比作一个翻译官,它把上层的高大上指令,翻译成底层硬件能懂的字节流。

4.1 HCI层:命令与事件的交响曲

HCI层定义了两种核心交互:Command(命令)和Event(事件)。

  • Command:Host发给Controller的指令。比如“开始扫描”、“建立连接”、“设置功率”。
  • Event:Controller回复给Host的通知。比如“扫描到设备了”、“连接断开了”、“命令执行完毕”。

这里有个关键点:Command和Event是异步的。你发一个命令下去,Controller不会立刻阻塞等你。它先回一个“Command Status”事件告诉你“收到,正在处理”。等真正干完了,再发一个“Command Complete”事件告诉你“搞定了”。

核心交互流程:

  1. Host 发送 HCI Command 包(例如:HCI_Inquiry)
  2. Controller 收到后,立即回复 HCI Command Status Event(状态:Pending)
  3. Controller 执行具体操作(例如:开始射频扫描)
  4. 操作完成后,Controller 发送 HCI Command Complete Event(携带结果数据)

我在项目中遇到过一个问题:耳机连接不稳定,总是断连。查了很久,最后用逻辑分析仪抓HCI包才发现,是Host发送了“断开连接”命令后,Controller还没来得及处理,又收到了新的连接请求。这就是典型的异步时序问题。后来我加了一个状态机,确保命令队列不会乱掉。

4.2 HCI Command 包结构

一个HCI Command包长什么样?说白了就是一段固定格式的字节流。我建议你把这个结构背下来,调试时经常要用到。

HCI Command Packet 格式:
| OpCode (2字节) | Parameter Total Length (1字节) | Parameters (0-255字节) |

OpCode = OGF (6位) + OCF (10位)
- OGF: OpCode Group Field,命令组
- OCF: OpCode Command Field,命令编号

举个例子,HCI_Inquiry命令:

  • OGF = 0x01 (Link Control)
  • OCF = 0x0001
  • OpCode = (0x01 << 10) | 0x0001 = 0x0401
  • Parameters: LAP (3字节) + Inquiry_Length (1字节) + Num_Responses (1字节)

调试小技巧:我习惯在代码里打印HCI Command的原始字节流。比如用printf("HCI Cmd: %02x %02x %02x ...", opcode[0], opcode[1], len, ...)。这样一旦出问题,直接对比抓包数据,定位很快。

4.3 HCI Event 包结构

Event包的结构更简单一些:

HCI Event Packet 格式:
| Event Code (1字节) | Parameter Total Length (1字节) | Parameters (0-255字节) |

常见的Event Code:

Event Code 事件名称 说明
0x01 Inquiry Complete 扫描完成
0x02 Inquiry Result 扫描到设备(可能多次触发)
0x0E Command Complete 命令执行完毕
0x0F Command Status 命令已收到,状态反馈
0x04 Disconnection Complete 连接断开完成

嗯,这里要注意:Command Complete事件里会携带对应的OpCode。这样你才能知道这个完成事件是回应哪个命令的。我曾经踩过一个坑:连续发了两个命令,结果只收到一个Complete事件,代码里没做匹配,导致逻辑错乱。

4.4 L2CAP层:数据的分包与重组

L2CAP层在HCI上面。它的主要工作是:把上层的大数据包,切成HCI能传输的小包;或者把HCI收上来的小包,拼回完整的大包。

为什么需要L2CAP?因为HCI层一次能传的数据有限(通常最多255字节)。你想想看,一个A2DP音频包可能几百字节,一个ATT数据包也可能超过这个限制。所以L2CAP就负责拆包和组包。

L2CAP核心概念:

  • CID (Channel Identifier):通道标识符。每个L2CAP通道都有一个唯一的CID。
  • MTU (Maximum Transmission Unit):最大传输单元。双方协商一个都能接受的最大包长。
  • 分段与重组:发送方分段,接收方重组。

我个人习惯在调试L2CAP时,重点关注MTU协商。很多连接问题,其实就是MTU没协商好。比如一方说我能收1000字节,另一方说我只发200字节,那没问题。但如果一方说我能收1000,另一方说我能发2000,那接收方就溢出了。

4.5 SDP/GATT层:服务发现与属性操作

SDP(Service Discovery Protocol)是传统蓝牙(BR/EDR)用的。GATT(Generic Attribute Profile)是低功耗蓝牙(BLE)用的。两者目的类似:让设备知道对方支持什么服务。

在TWS耳机里,我们主要用GATT。因为现在大部分TWS耳机都是BLE连接。GATT把数据组织成“属性”(Attribute),每个属性有UUID、句柄、权限和值。

举个例子,耳机电量服务:

  • Service UUID: 0x180F (Battery Service)
  • Characteristic UUID: 0x2A19 (Battery Level)
  • 属性值:0-100,表示电量百分比

手机通过GATT的Read/Write/Notify操作来读写这些属性。我在项目中遇到过一个问题:耳机上报电量时,手机收不到Notify。后来发现是GATT的CCCD(Client Characteristic Configuration Descriptor)没有正确配置。说白了,就是手机没告诉耳机“我要收通知”。

避坑指南:我曾经在GATT的MTU协商上栽过跟头。默认MTU是23字节,但很多应用层需要传更大的数据。如果你不主动协商MTU,数据就会被截断。记得在连接建立后,主动发起MTU Exchange Request。

4.6 RFCOMM层:串口模拟

RFCOMM层,说白了就是模拟串口。它基于L2CAP,提供串口通信的抽象。在TWS耳机里,RFCOMM主要用于一些传统蓝牙服务,比如SPP(串口协议)或某些厂商自定义的协议。

RFCOMM的调试相对简单。你只要关注几个参数:

  • Server Channel:服务端通道号
  • DLCI (Data Link Connection Identifier):数据链路连接标识
  • 波特率、数据位、停止位:这些在RFCOMM里是虚拟的,但需要协商

我记得有一次,客户反馈耳机连上手机后,APP发指令没反应。我抓了HCI日志,发现RFCOMM连接建立成功了,但数据收发不正常。最后定位到是DLCI分配冲突。两个服务用了同一个DLCI,导致数据串了。

4.7 各层交互实战:一次完整的连接建立

我们来看一个完整的流程。假设手机要连接TWS耳机:

  1. HCI层:手机发HCI_Inquiry命令,扫描周围设备。耳机回复Inquiry Result事件。
  2. HCI层:手机发HCI_Create_Connection命令,请求建立ACL连接。耳机回复Connection Complete事件。
  3. L2CAP层:双方建立L2CAP通道。通常先建立Signaling通道(CID=0x0001),用于MTU协商。
  4. SDP/GATT层:手机通过GATT发现耳机的服务。比如找到电量服务、音频控制服务等。
  5. RFCOMM层(可选):如果需要串口通信,再建立RFCOMM连接。

这个过程中,每一层都有对应的Command和Event。我建议你在调试时,用抓包工具(比如Ellisys或Frontline)把HCI日志抓下来,逐层分析。你会发现,很多问题其实就出在某一层的交互上。

我的调试习惯:先看HCI层有没有异常Event(比如Error Code不为0)。如果HCI正常,再看L2CAP的MTU协商。如果L2CAP也正常,再看GATT的读写操作。一层一层排查,效率最高。

好了,这一章的内容就到这里。蓝牙协议栈虽然层数多,但每一层的职责很清晰。你只要抓住HCI的Command/Event交互,再理解L2CAP的分包重组,基本就能应付大部分TWS耳机的开发调试了。下一章我们讲音频数据的传输路径,到时候你会看到这些层是怎么协同工作的。