3. 常见蓝牙协议栈分析:HCI层、L2CAP层、ATT/GATT层、SM层的作用与交互流程

做蓝牙网关开发,说白了就是跟协议栈打交道。很多初学者一上来就扎进代码里,结果被各种层之间的调用关系搞得晕头转向。我个人习惯是先把协议栈的骨架搭清楚——每一层负责什么,数据怎么流,这样后面写代码才有底气。

蓝牙协议栈从下往上大致分为:HCI层、L2CAP层、ATT/GATT层和SM层。今天我们就一层层剥开来看。

核心观点:协议栈的分层设计,本质上是为了解耦。每一层只关心自己的事,上层不用管底层是UART还是USB,底层也不用管上层传的是心率数据还是温度值。

蓝牙协议栈分层架构与数据流 蓝牙协议栈分层架构与数据流 应用层(Profiles / Application) GATT / ATT 层 L2CAP 层 HCI 层 Controller(链路层/物理层) 数据流向 应用数据 ↓ 逐层封装 ↓ 逐层解析 SM层(安全) 配对/绑定 加密/密钥分发

3.1 HCI层——主机与控制器的翻译官

HCI,全称Host Controller Interface。它干的事很纯粹:把Host(主机)的命令翻译给Controller(蓝牙芯片),再把Controller的响应翻译回来。

你想想看,你的Linux板子跑着BlueZ协议栈,蓝牙芯片通过UART或者USB连在上面。HCI就是它们之间的通信协议。我早期做项目时,遇到过HCI命令超时的问题,查了半天发现是UART流控没配好。嗯,这里要注意——HCI的物理传输一旦出问题,上层所有逻辑都白搭。

HCI层主要处理三类数据:

  • 命令(Command):Host发给Controller,比如「开始扫描」、「建立连接」。
  • 事件(Event):Controller回复Host,比如「扫描到设备」、「连接已断开」。
  • 数据(Data):真正的用户数据包,走ACL(异步无连接)或SCO(同步面向连接)通道。

实战技巧:调试HCI层时,我习惯用 hcitool 和 btmon 抓原始HCI日志。有一次设备连上就断,看日志发现是Controller返回了Disconnection Complete事件,原因是Remote User Terminated Connection——对端主动断的。顺着这个线索才找到是配对超时配置太短。

3.2 L2CAP层——数据拆包与协议复用

L2CAP,Logical Link Control and Adaptation Protocol。名字很长,但核心功能就两个:拆包组包和通道复用。

为什么需要拆包?因为底层蓝牙包有长度限制(经典蓝牙是341字节,BLE是27字节)。应用层可能一次发几百字节,L2CAP就得把它切成小块,到对端再拼回去。

通道复用是什么意思?你想想看,一个蓝牙连接上可能同时跑着多个服务——A2DP听歌、HID键盘输入、ATT传数据。L2CAP用CID(Channel Identifier)来区分这些逻辑通道。每个通道对应一个CID,数据包到了L2CAP层,根据CID就知道该往哪个上层协议栈送。

CID范围 用途 说明
0x0001 L2CAP信令通道 用于建立/释放通道、协商MTU
0x0004 ATT通道 BLE属性协议专用
0x0005 LE L2CAP信令 BLE专属信令通道
0x0040-0x007F 动态分配 应用层协议使用

我在项目中遇到过一个问题:两个BLE设备通信,偶尔丢包。查到最后发现是L2CAP层的MTU协商没做好。发送方默认用23字节MTU,但接收方实际能支持更大的包,结果数据被截断。后来我强制在连接建立后做一次MTU交换,问题就解决了。

注意:L2CAP层不保证可靠传输。它只负责拆包和复用,丢包重传是上层的事。如果你需要可靠传输,要么用L2CAP的增强重传模式(ERTM),要么在GATT层自己做确认机制。

3.3 ATT/GATT层——蓝牙数据的读写规范

ATT,Attribute Protocol。它定义了最基本的操作:读、写、通知、指示。GATT,Generic Attribute Profile,是在ATT之上搭的一套数据组织框架。

你可以这么理解:ATT是邮递员,负责把信送到;GATT是信封上的地址格式,规定了怎么写地址才能让邮递员看懂。

GATT把数据组织成Service和Characteristic的树形结构:

  • Service:一组相关功能的集合。比如「电池服务」包含电量百分比和电量状态。
  • Characteristic:具体的数据点。比如「电量百分比」是一个Characteristic,它的值就是0-100的数字。
  • Descriptor:对Characteristic的描述或配置。比如「客户端特征配置描述符(CCCD)」用来开关通知。

做网关开发时,GATT层是你打交道最多的地方。我举个例子,一个温湿度传感器,它的GATT结构大概是这样:

服务: 环境传感服务 (UUID: 0x181A)
  ├── 特征: 温度值 (UUID: 0x2A6E)
  │   └── 描述符: 客户端特征配置 (UUID: 0x2902)
  └── 特征: 湿度值 (UUID: 0x2A6F)
      └── 描述符: 客户端特征配置 (UUID: 0x2902)

网关要读温度,就发ATT Read Request,带上温度特征的UUID。传感器回复ATT Read Response,带上温度数据。如果传感器支持通知,网关先写CCCD开启通知,之后传感器温度变了就主动推过来。

关键点:GATT层定义了数据的「形状」,但不定义数据的「含义」。含义由更高层的Profile(比如健康设备Profile、环境传感Profile)来规定。所以你在做网关适配时,一定要先搞清楚设备用的是哪个Profile,否则读出来的字节串你根本不知道该怎么解析。

3.4 SM层——蓝牙安全的核心

SM,Security Manager。它负责蓝牙设备的配对、绑定和加密。

配对过程分三个阶段:

  1. 配对特征交换:双方告诉对方自己支持什么IO能力、什么加密算法。
  2. 认证:根据IO能力选择合适的配对方式。有Just Works(无交互)、Passkey Entry(输入PIN码)、Numeric Comparison(数字比对)、Out of Band(NFC等带外方式)。
  3. 密钥分发:双方交换加密密钥、身份密钥等,用于后续加密通信。

绑定就是把配对信息存下来,下次连接不用再配一次。我做过一个项目,网关每天要连上百个传感器,如果不绑定,每次连接都要重新配对,功耗和时延都受不了。后来我改成首次配对后保存Long Term Key(LTK),后续重连直接用LTK加密,速度快了很多。

避坑指南:我曾经在SM层踩过一个坑——配对方式选错了。网关没有屏幕和键盘,IO能力是NoInputNoOutput,结果配对上用了Just Works。Just Works不提供中间人攻击防护,这在某些安全要求高的场景下是不行的。后来我改用OOB方式,通过NFC交换临时密钥,既保证了安全,又不需要用户交互。

3.5 各层交互流程——数据从应用到空中的完整旅程

现在我们把四层串起来,看一个完整的数据流。假设网关要读取传感器的温度值:

  1. 应用层调用GATT API,发起读温度特征操作。
  2. GATT层构造一个ATT Read Request,目标UUID是0x2A6E,交给L2CAP层。
  3. L2CAP层把ATT包封装成L2CAP帧,加上CID=0x0004(ATT通道),如果包太大就分片。
  4. HCI层把L2CAP帧封装成HCI数据包,加上连接句柄(Connection Handle),通过UART发给蓝牙Controller。
  5. Controller把数据通过空中的BLE链路层发出去。
  6. 传感器收到后,逆向走一遍:Controller→HCI→L2CAP→ATT→GATT→应用层。

反过来,传感器回复温度数据时,流程完全对称。如果传感器开启了通知,它不需要等网关来读,直接主动推数据,流程也一样。

调试建议:我习惯在每一层都加日志。HCI层看命令和事件是否匹配,L2CAP层看CID和MTU是否正确,ATT层看操作码和句柄是否合法。有一次设备连上后读不到数据,查HCI日志发现连接句柄是0x000,说明连接根本没建立成功。顺着这个线索才发现是扫描参数配错了,导致连到了错误的设备。

好了,这四层的关系和交互流程就讲到这里。你想想看,理解了这些,后面写蓝牙网关的适配代码时,心里就有底了——你知道数据从哪来、到哪去、每一层做了什么处理。遇到问题也能快速定位到具体层,而不是盲目地改代码碰运气。