4. BLE协议基础(下):L2CAP、ATT、GATT、GAP层详解

上一讲我们把物理层和链路层聊透了。今天继续往下走,看看协议栈中间那几层——L2CAP、ATT、GATT、GAP。说实话,这几层才是真正跟应用开发打交道的部分。我刚开始做BLE项目时,总觉得GATT和GAP长得像双胞胎,搞混了好几次。后来踩了坑才明白,它们分工完全不同。

先给你一张总览图,看看这几层在协议栈里站什么位置:

BLE 协议栈分层结构(核心层) 应用层(Profiles / Application) GAP(通用访问规范) GATT(通用属性规范) ATT(属性协议) L2CAP(逻辑链路控制与适配协议) HCI(主机控制器接口) 链路层(LL) + 物理层(PHY) 说明:GAP 和 GATT 并列位于应用层之下,ATT 为 GATT 提供底层数据传输能力

4.1 L2CAP——数据搬运工

L2CAP的全称是逻辑链路控制与适配协议。名字挺长,但活儿其实很单纯——它负责把上层的数据包拆成链路层能吃的MTU大小,再传过去。反过来,收到链路层的数据后,它再拼回去交给上层。

我打个比方:L2CAP就像快递中转站。你寄一个大箱子,中转站发现货车装不下,就帮你拆成几个小箱子分别运。到了目的地再拼回去。你作为寄件人根本不用操心拆箱的事。

L2CAP有几个关键点:

  • MTU协商:连接建立后,双方会协商一个最大传输单元。默认是23字节,但可以协商到更大。我做过一个OTA升级项目,MTU没协商好,固件分包传得乱七八糟。后来把MTU提到247字节,速度直接翻倍。
  • 通道复用:L2CAP支持多个上层协议共用同一个链路层连接。ATT、SMP(安全管理协议)都跑在L2CAP的不同通道上。
  • 分段重组:这是它最核心的活儿。上层发一个100字节的包,链路层MTU只有27字节,L2CAP就自动切成4段,每段带个序号,接收方再拼回去。
小技巧:实际开发中,MTU协商最好在连接建立后尽快做。我习惯在连接回调里直接发起MTU请求,别等到要传大数据时才想起来,那时候可能已经浪费了好几轮交互。

4.2 ATT——属性协议,数据的搬运规则

ATT全称是属性协议。它定义了一台设备怎么去读写另一台设备上的“属性”。属性是什么?说白了就是一小块数据,带个句柄、UUID、权限描述。

ATT的角色是服务器-客户端模型:

  • GATT服务器:持有属性数据,响应读写请求。
  • GATT客户端:发起读写请求,接收通知。

注意,一个设备可以同时是服务器和客户端。比如手机连手环,手机是客户端,手环是服务器。但手环也可以主动发通知给手机,这时候手环在ATT层面还是服务器,只是它主动推送了数据。

ATT定义了6种操作类型:

操作 方向 说明
Request 客户端→服务器 请求读/写,服务器必须回复
Response 服务器→客户端 对Request的应答
Command 客户端→服务器 写操作,不需要回复
Notification 服务器→客户端 服务器主动推送,不需要确认
Indication 服务器→客户端 服务器主动推送,需要客户端确认
Confirm 客户端→服务器 对Indication的确认

这里有个坑:Notification和Indication看起来差不多,但Indication有确认机制,可靠性更高。不过Indication的吞吐量比Notification低,因为每次都要等确认。我做过一个心率监测项目,用Notification每秒传10个数据点,换成Indication后只能传3个。所以对可靠性要求不高的场景,用Notification更划算。

4.3 GATT——属性协议的“上层建筑”

GATT全称是通用属性规范。它建立在ATT之上,定义了属性的组织方式。说白了,ATT只负责怎么读写数据,GATT负责数据长什么样、怎么分组。

GATT的核心概念有三个:

  • Service(服务):一组相关功能的集合。比如电池服务、设备信息服务。
  • Characteristic(特征):一个具体的数据点。比如电池电量百分比、设备序列号。
  • Descriptor(描述符):对特征的额外描述。比如配置通知开关、设置数据格式。

它们的层级关系是这样的:

GATT 层级结构 Profile(规范) Service(服务) Service(服务) Characteristic Characteristic Characteristic Descriptor Descriptor Descriptor 一个Profile包含多个Service,一个Service包含多个Characteristic,一个Characteristic包含多个Descriptor

每个Characteristic都有UUID来标识。标准UUID是16位的,比如0x2A19代表电池电量。自定义服务用128位UUID。我建议你尽量复用标准UUID,别自己造轮子。蓝牙SIG官网上有完整的标准服务列表,省时省力。

注意:GATT的层级关系是固定的,不能乱套。Service下面只能挂Characteristic,Characteristic下面只能挂Descriptor。你不能在Service下面直接挂Descriptor,那样不符合规范,很多BLE库会直接报错。

4.4 GAP——设备怎么被发现和连接

GAP全称是通用访问规范。它管的是设备怎么广播自己、怎么被扫描到、怎么建立连接。说白了,GAP是设备在“社交”层面的行为规范。

GAP定义了四种角色:

角色 说明 典型设备
Broadcaster(广播者) 只发广播,不接受连接 信标、温度标签
Observer(观察者) 只扫描广播,不发起连接 扫描仪、网关
Peripheral(外设) 发广播,接受连接 手环、传感器
Central(中心) 扫描广播,发起连接 手机、平板

GAP还定义了连接建立的流程:

  1. 广播阶段:外设周期性地发广播包,里面包含设备名称、服务UUID等信息。
  2. 扫描阶段:中心设备扫描到广播包,可以发起扫描请求获取更多信息。
  3. 连接阶段:中心设备发送连接请求,双方建立连接。
  4. 参数协商:连接建立后,双方可以协商连接间隔、延迟、超时等参数。

这里有个经验:广播间隔和连接间隔的配置直接影响功耗。广播间隔越短,被发现越快,但功耗越高。我做过一个纽扣电池供电的设备,广播间隔设成100ms,电池撑了不到一周。后来改成500ms,电池撑了一个月。你想想看,如果设备不需要频繁被发现,把广播间隔拉大一点,省电效果立竿见影。

4.5 它们之间的关系

这几层的关系,我用一句话总结:GAP管社交,GATT管数据,ATT管传输,L2CAP管搬运。

具体来说:

  • GAP决定设备能不能被找到、能不能连上。
  • 连上之后,GATT定义数据长什么样(服务、特征、描述符)。
  • ATT负责具体怎么读写这些数据(请求、响应、通知)。
  • L2CAP在底层把数据包拆好、拼好,保证ATT能顺畅通信。

举个例子:你用手环测心率。

  • GAP:手环广播“我是心率手环”,手机扫描到并发起连接。
  • GATT:手机发现手环有心率服务(UUID 0x180D),里面有心率测量特征(UUID 0x2A37)。
  • ATT:手机发请求读心率特征的值,手环回复数据。或者手环主动发Notification推送心率数据。
  • L2CAP:心率数据包如果太大,L2CAP负责分段传输,接收方再重组。
我的习惯:调试BLE问题时,我一般先看GAP层——设备能不能广播、能不能扫描到。GAP没问题了再看GATT——服务有没有暴露、特征能不能读写。ATT和L2CAP的问题通常表现为数据传不过去或者传不全,这时候抓个空口包一看就明白了。

嗯,这几层的关系捋清楚了,后面搭建GATT服务就顺理成章了。你只要记住:GAP是门卫,GATT是前台,ATT是快递员,L2CAP是分拣中心。各司其职,缺一不可。