4、BLE协议栈入门:BLE GAP与GATT协议剖析,广播、扫描、连接、服务与特征值的概念

做物联网通信这么多年,我见过不少工程师一上来就扎进BLE的代码细节里,结果被广播包格式、MTU协商、属性权限这些东西搞得晕头转向。其实BLE协议栈没那么玄乎,你只要抓住两个核心角色——GAP和GATT,整个框架就清晰了。

打个比方:GAP负责“怎么找到人、怎么打招呼”,GATT负责“找到人之后聊什么、怎么聊”。今天我们就从这两个层面,把BLE的底层逻辑拆开来看。

核心认知:GAP定义设备角色和连接流程,GATT定义连接后的数据交互方式。两者缺一不可。

4.1 GAP——通用访问协议

GAP的全称是Generic Access Profile。说白了,它管的是设备怎么被发现、怎么建立连接。我刚开始做BLE项目时,总把GAP和GATT混在一起,后来踩了坑才明白——GAP是连接前的“社交礼仪”,GATT是连接后的“商务谈判”。

4.1.1 设备角色

GAP定义了四种设备角色,但在实际项目中,最常见的是前两种:

角色 说明 典型设备
Broadcaster(广播者) 只发广播,不接收连接 信标、温度传感器
Observer(观察者) 只扫描广播,不发起连接 扫描仪、网关
Peripheral(外设) 可被连接,通常作为从设备 手环、心率带
Central(中心) 主动扫描并发起连接 手机、网关

我记得第一次做智能手环项目时,把手机APP配成了Peripheral角色,结果手环怎么也连不上。后来才发现——手机必须是Central,手环才是Peripheral。角色搞反了,连一辈子都连不上。

4.1.2 广播与扫描

广播是BLE设备“刷存在感”的方式。外设会周期性地发送广播包,中心设备则在扫描信道上监听。

广播包的结构其实很简单:

广播包结构:
- 前导码(1字节)
- 接入地址(4字节,固定为0x8E89BED6)
- PDU(2~39字节)
  - 头部(2字节):包含PDU类型、长度等
  - 有效载荷(0~37字节):包含设备名称、UUID、厂商自定义数据等
- CRC(3字节)

这里有个坑:广播包最大只有31字节的有效载荷。我见过有人想把整包JSON数据塞进广播包,结果怎么都发不出去。后来改用扫描响应包(Scan Response)才解决——扫描响应包也是31字节,可以和广播包配合使用。

实战技巧:广播间隔建议设在100ms~1s之间。间隔太短(比如20ms)会大量耗电,间隔太长(比如2s以上)会导致发现延迟。我一般用200ms作为默认值,兼顾功耗和响应速度。

4.1.3 连接建立

当中心设备扫描到外设后,会发送连接请求。连接请求里包含了连接参数:

  • 连接间隔:两个连接事件之间的时间,范围7.5ms~4s
  • 从设备延迟:外设可以跳过多少个连接事件
  • 监督超时:超过这个时间没收到包,就认为连接断开

我曾经在一个项目中,把连接间隔设成了7.5ms,结果功耗高得吓人,手环半天就没电了。后来改成30ms,功耗降了60%,数据吞吐量也够用。你想想看,连接间隔不是越小越好,得根据实际业务来权衡。

4.2 GATT——通用属性协议

连接建立之后,就该GATT上场了。GATT定义了一种树形数据结构,用来组织设备间的数据交换。这个结构分为三层:服务(Service)、特征值(Characteristic)、描述符(Descriptor)。

4.2.1 服务与特征值

服务是一组相关功能的集合,特征值是具体的数据点。举个例子:

  • 电池服务(0x180F):包含一个特征值“电池电量”(0x2A19)
  • 设备信息服务(0x180A):包含“制造商名称”、“型号”等特征值

每个特征值都有属性:

属性 说明 常见用途
Read 中心设备可以读取 读取设备状态
Write 中心设备可以写入 下发配置命令
Notify 外设主动推送数据 实时传感器数据
Indicate 带确认的Notify 重要事件上报

我个人习惯把Notify用在频繁变化的数据上,比如心率、加速度计数据。Indicate则用在需要确认的关键事件上,比如报警、低电量提醒。因为Indicate需要中心设备回复确认包,会多一次交互,所以延迟比Notify高一些。

4.2.2 UUID与Profile

服务和特征值都用UUID来标识。16位UUID是蓝牙联盟定义的,比如0x180F是电池服务。128位UUID是自定义的,用于厂商私有协议。

我建议你在设计私有协议时,用128位UUID,别去蹭16位的保留段。我曾经见过有人用0xFFF0做自定义服务,结果和某款手机的蓝牙协议栈冲突,导致连接后频繁断连。后来改成128位UUID,问题就解决了。

4.2.3 属性协议(ATT)

GATT底层跑的是ATT(Attribute Protocol)。ATT负责把服务、特征值、描述符组织成一张属性表,然后通过读写操作来访问。

ATT的操作类型包括:

  • Read Request/Response:中心读取属性值
  • Write Request/Response:中心写入属性值(带确认)
  • Write Command:中心写入属性值(无确认)
  • Handle Value Notification:外设推送数据(无确认)
  • Handle Value Indication:外设推送数据(带确认)

这里有个性能要点:MTU(最大传输单元)默认是23字节,其中3字节是ATT头部,实际有效载荷只有20字节。如果需要传输大数据,可以通过MTU协商把MTU扩大到512字节甚至更大。

避坑指南:我曾经在一个项目中,没有做MTU协商就直接发大包,结果数据被截断,接收端解析出错。后来在连接建立后立即发起MTU协商请求,把MTU扩到247字节,问题才解决。记住:MTU协商是连接后的第一步,别跳过。

4.3 GAP与GATT的协同关系

为了让你更直观地理解GAP和GATT在整个通信流程中的位置,我画了一张图:

BLE通信流程:GAP与GATT协同 GAP阶段:发现与连接 外设(Peripheral) 广播 中心(Central) 连接请求 连接建立 连接参数协商:连接间隔、从设备延迟、监督超时 GATT阶段:数据交互 服务发现 MTU协商 读写/通知 中心设备:Read/Write Request → 外设:Response/Notification 外设:Notification/Indication → 中心设备:确认(可选) GAP管连接,GATT管数据——两者协同完成一次完整的BLE通信

从这张图可以看得很清楚:GAP负责把两个设备拉到一起,GATT负责让它们开始“聊天”。没有GAP,设备之间互相看不见;没有GATT,连上了也不知道该说什么。

4.4 实战中的常见问题

最后分享几个我在项目中踩过的坑,希望能帮你少走弯路:

问题1:广播包太长导致扫描不到

我曾经把设备名称设成了32个字符,结果广播包超长,部分手机扫描不到。后来把名称缩短到8个字符,剩下的信息放在扫描响应包里,问题解决。

问题2:连接参数不匹配导致断连

中心设备要求连接间隔30ms,外设只支持50ms以上。结果连接后频繁断连。后来在连接请求中主动协商参数,双方取一个都能接受的中间值。

问题3:Notify频率太高导致丢包

传感器数据10ms发一次Notify,但BLE的带宽有限,导致数据堆积丢包。后来把频率降到50ms一次,或者用多个连接事件分摊,数据就稳定了。

嗯,GAP和GATT的内容就讲到这里。你只要记住:GAP管“怎么连”,GATT管“连上后怎么聊”。这两个概念搞清楚了,BLE协议栈的骨架你就抓住了。剩下的细节,比如安全配对、L2CAP、属性权限这些,都是在骨架上的血肉,后面用到的时候再深入也不迟。